The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

466 lines
17KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2015 - ROLI Ltd.
  5. Permission is granted to use this software under the terms of either:
  6. a) the GPL v2 (or any later version)
  7. b) the Affero GPL v3
  8. Details of these licenses can be found at: www.gnu.org/licenses
  9. JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
  10. WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
  11. A PARTICULAR PURPOSE. See the GNU General Public License for more details.
  12. ------------------------------------------------------------------------------
  13. To release a closed-source product which uses JUCE, commercial licenses are
  14. available: visit www.juce.com for more information.
  15. ==============================================================================
  16. */
  17. //==============================================================================
  18. #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
  19. METHOD (getMidiBluetoothAddresses, "getMidiBluetoothAddresses", "()[Ljava/lang/String;") \
  20. METHOD (pairBluetoothMidiDevice, "pairBluetoothMidiDevice", "(Ljava/lang/String;)Z") \
  21. METHOD (unpairBluetoothMidiDevice, "unpairBluetoothMidiDevice", "(Ljava/lang/String;)V") \
  22. METHOD (getHumanReadableStringForBluetoothAddress, "getHumanReadableStringForBluetoothAddress", "(Ljava/lang/String;)Ljava/lang/String;") \
  23. METHOD (isBluetoothDevicePaired, "isBluetoothDevicePaired", "(Ljava/lang/String;)Z") \
  24. METHOD (startStopScan, "startStopScan", "(Z)V")
  25. DECLARE_JNI_CLASS (AndroidBluetoothManager, JUCE_ANDROID_ACTIVITY_CLASSPATH "$BluetoothManager");
  26. #undef JNI_CLASS_MEMBERS
  27. //==============================================================================
  28. struct AndroidBluetoothMidiInterface
  29. {
  30. static void startStopScan (bool startScanning)
  31. {
  32. JNIEnv* env = getEnv();
  33. LocalRef<jobject> btManager (android.activity.callObjectMethod (JuceAppActivity.getAndroidBluetoothManager));
  34. if (btManager.get() != nullptr)
  35. env->CallVoidMethod (btManager.get(), AndroidBluetoothManager.startStopScan, (jboolean) (startScanning ? 1 : 0));
  36. }
  37. static StringArray getBluetoothMidiDevicesNearby()
  38. {
  39. StringArray retval;
  40. JNIEnv* env = getEnv();
  41. LocalRef<jobject> btManager (android.activity.callObjectMethod (JuceAppActivity.getAndroidBluetoothManager));
  42. // if this is null then bluetooth is not enabled
  43. if (btManager.get() == nullptr)
  44. return StringArray();
  45. jobjectArray jDevices = (jobjectArray) env->CallObjectMethod (btManager.get(),
  46. AndroidBluetoothManager.getMidiBluetoothAddresses);
  47. LocalRef<jobjectArray> devices (jDevices);
  48. const int count = env->GetArrayLength (devices.get());
  49. for (int i = 0; i < count; ++i)
  50. {
  51. LocalRef<jstring> string ((jstring) env->GetObjectArrayElement (devices.get(), i));
  52. retval.add (juceString (string));
  53. }
  54. return retval;
  55. }
  56. //==============================================================================
  57. static bool pairBluetoothMidiDevice (const String& bluetoothAddress)
  58. {
  59. JNIEnv* env = getEnv();
  60. LocalRef<jobject> btManager (android.activity.callObjectMethod (JuceAppActivity.getAndroidBluetoothManager));
  61. if (btManager.get() == nullptr)
  62. return false;
  63. jboolean result = env->CallBooleanMethod (btManager.get(), AndroidBluetoothManager.pairBluetoothMidiDevice,
  64. javaString (bluetoothAddress).get());
  65. return result;
  66. }
  67. static void unpairBluetoothMidiDevice (const String& bluetoothAddress)
  68. {
  69. JNIEnv* env = getEnv();
  70. LocalRef<jobject> btManager (android.activity.callObjectMethod (JuceAppActivity.getAndroidBluetoothManager));
  71. if (btManager.get() != nullptr)
  72. env->CallVoidMethod (btManager.get(), AndroidBluetoothManager.unpairBluetoothMidiDevice,
  73. javaString (bluetoothAddress).get());
  74. }
  75. //==============================================================================
  76. static String getHumanReadableStringForBluetoothAddress (const String& address)
  77. {
  78. JNIEnv* env = getEnv();
  79. LocalRef<jobject> btManager (android.activity.callObjectMethod (JuceAppActivity.getAndroidBluetoothManager));
  80. if (btManager.get() == nullptr)
  81. return address;
  82. LocalRef<jstring> string ((jstring) env->CallObjectMethod (btManager.get(),
  83. AndroidBluetoothManager.getHumanReadableStringForBluetoothAddress,
  84. javaString (address).get()));
  85. if (string.get() == nullptr)
  86. return address;
  87. return juceString (string);
  88. }
  89. //==============================================================================
  90. static bool isBluetoothDevicePaired (const String& address)
  91. {
  92. JNIEnv* env = getEnv();
  93. LocalRef<jobject> btManager (android.activity.callObjectMethod (JuceAppActivity.getAndroidBluetoothManager));
  94. if (btManager.get() == nullptr)
  95. return false;
  96. return env->CallBooleanMethod (btManager.get(), AndroidBluetoothManager.isBluetoothDevicePaired,
  97. javaString (address).get());
  98. }
  99. };
  100. //==============================================================================
  101. struct AndroidBluetoothMidiDevice
  102. {
  103. enum ConnectionStatus
  104. {
  105. offline,
  106. connected,
  107. disconnected,
  108. connecting,
  109. disconnecting
  110. };
  111. AndroidBluetoothMidiDevice (String deviceName, String address, ConnectionStatus status)
  112. : name (deviceName), bluetoothAddress (address), connectionStatus (status)
  113. {
  114. // can't create a device without a valid name and bluetooth address!
  115. jassert (! name.isEmpty());
  116. jassert (! bluetoothAddress.isEmpty());
  117. }
  118. bool operator== (const AndroidBluetoothMidiDevice& other) const noexcept
  119. {
  120. return bluetoothAddress == other.bluetoothAddress;
  121. }
  122. bool operator!= (const AndroidBluetoothMidiDevice& other) const noexcept
  123. {
  124. return ! operator== (other);
  125. }
  126. const String name, bluetoothAddress;
  127. ConnectionStatus connectionStatus;
  128. };
  129. //==============================================================================
  130. class AndroidBluetoothMidiDevicesListBox : public ListBox,
  131. private ListBoxModel,
  132. private Timer
  133. {
  134. public:
  135. //==============================================================================
  136. AndroidBluetoothMidiDevicesListBox()
  137. : timerPeriodInMs (1000)
  138. {
  139. setRowHeight (40);
  140. setModel (this);
  141. setOutlineThickness (1);
  142. startTimer (timerPeriodInMs);
  143. }
  144. void pairDeviceThreadFinished() // callback from PairDeviceThread
  145. {
  146. updateDeviceList();
  147. startTimer (timerPeriodInMs);
  148. }
  149. private:
  150. //==============================================================================
  151. typedef AndroidBluetoothMidiDevice::ConnectionStatus DeviceStatus;
  152. int getNumRows() override
  153. {
  154. return devices.size();
  155. }
  156. void paintListBoxItem (int rowNumber, Graphics& g,
  157. int width, int height, bool) override
  158. {
  159. if (isPositiveAndBelow (rowNumber, devices.size()))
  160. {
  161. const AndroidBluetoothMidiDevice& device = devices.getReference (rowNumber);
  162. const String statusString (getDeviceStatusString (device.connectionStatus));
  163. g.fillAll (Colours::white);
  164. const float xmargin = 3.0f;
  165. const float ymargin = 3.0f;
  166. const float fontHeight = 0.4f * height;
  167. const float deviceNameWidth = 0.6f * width;
  168. g.setFont (fontHeight);
  169. g.setColour (getDeviceNameFontColour (device.connectionStatus));
  170. g.drawText (device.name,
  171. Rectangle<float> (xmargin, ymargin, deviceNameWidth - (2.0f * xmargin), height - (2.0f * ymargin)),
  172. Justification::topLeft, true);
  173. g.setColour (getDeviceStatusFontColour (device.connectionStatus));
  174. g.drawText (statusString,
  175. Rectangle<float> (deviceNameWidth + xmargin, ymargin,
  176. width - deviceNameWidth - (2.0f * xmargin), height - (2.0f * ymargin)),
  177. Justification::topRight, true);
  178. g.setColour (Colours::grey);
  179. g.drawHorizontalLine (height - 1, xmargin, width);
  180. }
  181. }
  182. //==============================================================================
  183. static Colour getDeviceNameFontColour (DeviceStatus deviceStatus) noexcept
  184. {
  185. if (deviceStatus == AndroidBluetoothMidiDevice::offline)
  186. return Colours::grey;
  187. return Colours::black;
  188. }
  189. static Colour getDeviceStatusFontColour (DeviceStatus deviceStatus) noexcept
  190. {
  191. if (deviceStatus == AndroidBluetoothMidiDevice::offline
  192. || deviceStatus == AndroidBluetoothMidiDevice::connecting
  193. || deviceStatus == AndroidBluetoothMidiDevice::disconnecting)
  194. return Colours::grey;
  195. if (deviceStatus == AndroidBluetoothMidiDevice::connected)
  196. return Colours::green;
  197. return Colours::black;
  198. }
  199. static String getDeviceStatusString (DeviceStatus deviceStatus) noexcept
  200. {
  201. if (deviceStatus == AndroidBluetoothMidiDevice::offline) return "Offline";
  202. if (deviceStatus == AndroidBluetoothMidiDevice::connected) return "Connected";
  203. if (deviceStatus == AndroidBluetoothMidiDevice::disconnected) return "Not connected";
  204. if (deviceStatus == AndroidBluetoothMidiDevice::connecting) return "Connecting...";
  205. if (deviceStatus == AndroidBluetoothMidiDevice::disconnecting) return "Disconnecting...";
  206. // unknown device state!
  207. jassertfalse;
  208. return "Status unknown";
  209. }
  210. //==============================================================================
  211. void listBoxItemClicked (int row, const MouseEvent&) override
  212. {
  213. const AndroidBluetoothMidiDevice& device = devices.getReference (row);
  214. if (device.connectionStatus == AndroidBluetoothMidiDevice::disconnected)
  215. disconnectedDeviceClicked (row);
  216. else if (device.connectionStatus == AndroidBluetoothMidiDevice::connected)
  217. connectedDeviceClicked (row);
  218. }
  219. void timerCallback() override
  220. {
  221. updateDeviceList();
  222. }
  223. //==============================================================================
  224. struct PairDeviceThread : public Thread,
  225. private AsyncUpdater
  226. {
  227. PairDeviceThread (const String& bluetoothAddressOfDeviceToPair,
  228. AndroidBluetoothMidiDevicesListBox& ownerListBox)
  229. : Thread ("JUCE Bluetooth MIDI Device Pairing Thread"),
  230. bluetoothAddress (bluetoothAddressOfDeviceToPair),
  231. owner (&ownerListBox)
  232. {
  233. startThread();
  234. }
  235. void run() override
  236. {
  237. AndroidBluetoothMidiInterface::pairBluetoothMidiDevice (bluetoothAddress);
  238. triggerAsyncUpdate();
  239. }
  240. void handleAsyncUpdate() override
  241. {
  242. if (owner != nullptr)
  243. owner->pairDeviceThreadFinished();
  244. delete this;
  245. }
  246. private:
  247. String bluetoothAddress;
  248. Component::SafePointer<AndroidBluetoothMidiDevicesListBox> owner;
  249. };
  250. //==============================================================================
  251. void disconnectedDeviceClicked (int row)
  252. {
  253. stopTimer();
  254. AndroidBluetoothMidiDevice& device = devices.getReference (row);
  255. device.connectionStatus = AndroidBluetoothMidiDevice::connecting;
  256. updateContent();
  257. repaint();
  258. new PairDeviceThread (device.bluetoothAddress, *this);
  259. }
  260. void connectedDeviceClicked (int row)
  261. {
  262. AndroidBluetoothMidiDevice& device = devices.getReference (row);
  263. device.connectionStatus = AndroidBluetoothMidiDevice::disconnecting;
  264. updateContent();
  265. repaint();
  266. AndroidBluetoothMidiInterface::unpairBluetoothMidiDevice (device.bluetoothAddress);
  267. }
  268. //==============================================================================
  269. void updateDeviceList()
  270. {
  271. StringArray bluetoothAddresses = AndroidBluetoothMidiInterface::getBluetoothMidiDevicesNearby();
  272. Array<AndroidBluetoothMidiDevice> newDevices;
  273. for (String* address = bluetoothAddresses.begin();
  274. address != bluetoothAddresses.end(); ++address)
  275. {
  276. String name = AndroidBluetoothMidiInterface::getHumanReadableStringForBluetoothAddress (*address);
  277. DeviceStatus status = AndroidBluetoothMidiInterface::isBluetoothDevicePaired (*address)
  278. ? AndroidBluetoothMidiDevice::connected
  279. : AndroidBluetoothMidiDevice::disconnected;
  280. newDevices.add (AndroidBluetoothMidiDevice (name, *address, status));
  281. }
  282. devices.swapWith (newDevices);
  283. updateContent();
  284. repaint();
  285. }
  286. Array<AndroidBluetoothMidiDevice> devices;
  287. const int timerPeriodInMs;
  288. };
  289. //==============================================================================
  290. class BluetoothMidiSelectorOverlay : public Component
  291. {
  292. public:
  293. BluetoothMidiSelectorOverlay (ModalComponentManager::Callback* exitCallbackToUse)
  294. {
  295. ScopedPointer<ModalComponentManager::Callback> exitCallback (exitCallbackToUse);
  296. AndroidBluetoothMidiInterface::startStopScan (true);
  297. setAlwaysOnTop (true);
  298. setVisible (true);
  299. addToDesktop (ComponentPeer::windowHasDropShadow);
  300. setBounds (0, 0, getParentWidth(), getParentHeight());
  301. toFront (true);
  302. addAndMakeVisible (bluetoothDevicesList);
  303. enterModalState (true, exitCallback.release(), true);
  304. }
  305. ~BluetoothMidiSelectorOverlay()
  306. {
  307. AndroidBluetoothMidiInterface::startStopScan (false);
  308. }
  309. void paint (Graphics& g) override
  310. {
  311. g.fillAll (Colours::black.withAlpha (0.6f));
  312. g.setColour (Colour (0xffdfdfdf));
  313. Rectangle<int> overlayBounds = getOverlayBounds();
  314. g.fillRect (overlayBounds);
  315. g.setColour (Colours::black);
  316. g.setFont (16);
  317. g.drawText ("Bluetooth MIDI Devices",
  318. overlayBounds.removeFromTop (20).reduced (3, 3),
  319. Justification::topLeft, true);
  320. overlayBounds.removeFromTop (2);
  321. g.setFont (12);
  322. g.drawText ("tap to connect/disconnect",
  323. overlayBounds.removeFromTop (18).reduced (3, 3),
  324. Justification::topLeft, true);
  325. }
  326. void inputAttemptWhenModal() override { exitModalState (0); }
  327. void mouseDrag (const MouseEvent&) override {}
  328. void mouseDown (const MouseEvent&) override { exitModalState (0); }
  329. void resized() override { update(); }
  330. void parentSizeChanged() override { update(); }
  331. private:
  332. void update()
  333. {
  334. setBounds (0, 0, getParentWidth(), getParentHeight());
  335. bluetoothDevicesList.setBounds (getOverlayBounds().withTrimmedTop (40));
  336. }
  337. Rectangle<int> getOverlayBounds() const noexcept
  338. {
  339. const int pw = getParentWidth();
  340. const int ph = getParentHeight();
  341. return Rectangle<int> (pw, ph).withSizeKeepingCentre (jmin (400, pw - 14),
  342. jmin (300, ph - 40));
  343. }
  344. AndroidBluetoothMidiDevicesListBox bluetoothDevicesList;
  345. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BluetoothMidiSelectorOverlay)
  346. };
  347. //==============================================================================
  348. bool BluetoothMidiDevicePairingDialogue::open (ModalComponentManager::Callback* exitCallbackPtr)
  349. {
  350. ScopedPointer<ModalComponentManager::Callback> exitCallback (exitCallbackPtr);
  351. if (! RuntimePermissions::isGranted (RuntimePermissions::bluetoothMidi))
  352. {
  353. // If you hit this assert, you probably forgot to get RuntimePermissions::bluetoothMidi.
  354. // This is not going to work, boo! The pairing dialogue won't be able to scan for or
  355. // find any devices, it will just display an empty list, so don't bother opening it.
  356. jassertfalse;
  357. return false;
  358. }
  359. new BluetoothMidiSelectorOverlay (exitCallback.release());
  360. return true;
  361. }
  362. bool BluetoothMidiDevicePairingDialogue::isAvailable()
  363. {
  364. jobject btManager (android.activity.callObjectMethod (JuceAppActivity.getAndroidBluetoothManager));
  365. return btManager != nullptr;
  366. }