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.

525 lines
19KB

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