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) 2020 - Raw Material Software Limited
  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 6 End-User License
  8. Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
  9. End User License Agreement: www.juce.com/juce-6-licence
  10. Privacy Policy: www.juce.com/juce-privacy-policy
  11. Or: You may also use this code under the terms of the GPL v3 (see
  12. www.gnu.org/licenses).
  13. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  14. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  15. DISCLAIMED.
  16. ==============================================================================
  17. */
  18. namespace juce
  19. {
  20. #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
  21. STATICMETHOD (getAndroidBluetoothManager, "getAndroidBluetoothManager", "(Landroid/content/Context;)Lcom/rmsl/juce/JuceMidiSupport$BluetoothManager;")
  22. DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidJuceMidiSupport, "com/rmsl/juce/JuceMidiSupport", 23)
  23. #undef JNI_CLASS_MEMBERS
  24. #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
  25. METHOD (getMidiBluetoothAddresses, "getMidiBluetoothAddresses", "()[Ljava/lang/String;") \
  26. METHOD (pairBluetoothMidiDevice, "pairBluetoothMidiDevice", "(Ljava/lang/String;)Z") \
  27. METHOD (unpairBluetoothMidiDevice, "unpairBluetoothMidiDevice", "(Ljava/lang/String;)V") \
  28. METHOD (getHumanReadableStringForBluetoothAddress, "getHumanReadableStringForBluetoothAddress", "(Ljava/lang/String;)Ljava/lang/String;") \
  29. METHOD (getBluetoothDeviceStatus, "getBluetoothDeviceStatus", "(Ljava/lang/String;)I") \
  30. METHOD (startStopScan, "startStopScan", "(Z)V")
  31. DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidBluetoothManager, "com/rmsl/juce/JuceMidiSupport$BluetoothManager", 23)
  32. #undef JNI_CLASS_MEMBERS
  33. //==============================================================================
  34. struct AndroidBluetoothMidiInterface
  35. {
  36. static void startStopScan (bool startScanning)
  37. {
  38. JNIEnv* env = getEnv();
  39. LocalRef<jobject> btManager (env->CallStaticObjectMethod (AndroidJuceMidiSupport, AndroidJuceMidiSupport.getAndroidBluetoothManager, getAppContext().get()));
  40. if (btManager.get() != nullptr)
  41. env->CallVoidMethod (btManager.get(), AndroidBluetoothManager.startStopScan, (jboolean) (startScanning ? 1 : 0));
  42. }
  43. static StringArray getBluetoothMidiDevicesNearby()
  44. {
  45. StringArray retval;
  46. JNIEnv* env = getEnv();
  47. LocalRef<jobject> btManager (env->CallStaticObjectMethod (AndroidJuceMidiSupport, AndroidJuceMidiSupport.getAndroidBluetoothManager, getAppContext().get()));
  48. // if this is null then bluetooth is not enabled
  49. if (btManager.get() == nullptr)
  50. return {};
  51. jobjectArray jDevices = (jobjectArray) env->CallObjectMethod (btManager.get(),
  52. AndroidBluetoothManager.getMidiBluetoothAddresses);
  53. LocalRef<jobjectArray> devices (jDevices);
  54. const int count = env->GetArrayLength (devices.get());
  55. for (int i = 0; i < count; ++i)
  56. {
  57. LocalRef<jstring> string ((jstring) env->GetObjectArrayElement (devices.get(), i));
  58. retval.add (juceString (string));
  59. }
  60. return retval;
  61. }
  62. //==============================================================================
  63. static bool pairBluetoothMidiDevice (const String& bluetoothAddress)
  64. {
  65. JNIEnv* env = getEnv();
  66. LocalRef<jobject> btManager (env->CallStaticObjectMethod (AndroidJuceMidiSupport, AndroidJuceMidiSupport.getAndroidBluetoothManager, getAppContext().get()));
  67. if (btManager.get() == nullptr)
  68. return false;
  69. jboolean result = env->CallBooleanMethod (btManager.get(), AndroidBluetoothManager.pairBluetoothMidiDevice,
  70. javaString (bluetoothAddress).get());
  71. return result;
  72. }
  73. static void unpairBluetoothMidiDevice (const String& bluetoothAddress)
  74. {
  75. JNIEnv* env = getEnv();
  76. LocalRef<jobject> btManager (env->CallStaticObjectMethod (AndroidJuceMidiSupport, AndroidJuceMidiSupport.getAndroidBluetoothManager, getAppContext().get()));
  77. if (btManager.get() != nullptr)
  78. env->CallVoidMethod (btManager.get(), AndroidBluetoothManager.unpairBluetoothMidiDevice,
  79. javaString (bluetoothAddress).get());
  80. }
  81. //==============================================================================
  82. static String getHumanReadableStringForBluetoothAddress (const String& address)
  83. {
  84. JNIEnv* env = getEnv();
  85. LocalRef<jobject> btManager (env->CallStaticObjectMethod (AndroidJuceMidiSupport, AndroidJuceMidiSupport.getAndroidBluetoothManager, getAppContext().get()));
  86. if (btManager.get() == nullptr)
  87. return address;
  88. LocalRef<jstring> string ((jstring) env->CallObjectMethod (btManager.get(),
  89. AndroidBluetoothManager.getHumanReadableStringForBluetoothAddress,
  90. javaString (address).get()));
  91. if (string.get() == nullptr)
  92. return address;
  93. return juceString (string);
  94. }
  95. //==============================================================================
  96. enum PairStatus
  97. {
  98. unpaired = 0,
  99. paired = 1,
  100. pairing = 2
  101. };
  102. static PairStatus isBluetoothDevicePaired (const String& address)
  103. {
  104. JNIEnv* env = getEnv();
  105. LocalRef<jobject> btManager (env->CallStaticObjectMethod (AndroidJuceMidiSupport, AndroidJuceMidiSupport.getAndroidBluetoothManager, getAppContext().get()));
  106. if (btManager.get() == nullptr)
  107. return unpaired;
  108. return static_cast<PairStatus> (env->CallIntMethod (btManager.get(), AndroidBluetoothManager.getBluetoothDeviceStatus,
  109. javaString (address).get()));
  110. }
  111. };
  112. //==============================================================================
  113. struct AndroidBluetoothMidiDevice
  114. {
  115. enum ConnectionStatus
  116. {
  117. offline,
  118. connected,
  119. disconnected,
  120. connecting,
  121. disconnecting
  122. };
  123. AndroidBluetoothMidiDevice (String deviceName, String address, ConnectionStatus status)
  124. : name (deviceName), bluetoothAddress (address), connectionStatus (status)
  125. {
  126. // can't create a device without a valid name and bluetooth address!
  127. jassert (! name.isEmpty());
  128. jassert (! bluetoothAddress.isEmpty());
  129. }
  130. bool operator== (const AndroidBluetoothMidiDevice& other) const noexcept
  131. {
  132. return bluetoothAddress == other.bluetoothAddress;
  133. }
  134. bool operator!= (const AndroidBluetoothMidiDevice& other) const noexcept
  135. {
  136. return ! operator== (other);
  137. }
  138. const String name, bluetoothAddress;
  139. ConnectionStatus connectionStatus;
  140. };
  141. //==============================================================================
  142. class AndroidBluetoothMidiDevicesListBox : public ListBox,
  143. private ListBoxModel,
  144. private Timer
  145. {
  146. public:
  147. //==============================================================================
  148. AndroidBluetoothMidiDevicesListBox()
  149. : timerPeriodInMs (1000)
  150. {
  151. setRowHeight (40);
  152. setModel (this);
  153. setOutlineThickness (1);
  154. startTimer (timerPeriodInMs);
  155. }
  156. void pairDeviceThreadFinished() // callback from PairDeviceThread
  157. {
  158. updateDeviceList();
  159. startTimer (timerPeriodInMs);
  160. }
  161. private:
  162. //==============================================================================
  163. typedef AndroidBluetoothMidiDevice::ConnectionStatus DeviceStatus;
  164. int getNumRows() override
  165. {
  166. return devices.size();
  167. }
  168. void paintListBoxItem (int rowNumber, Graphics& g,
  169. int width, int height, bool) override
  170. {
  171. if (isPositiveAndBelow (rowNumber, devices.size()))
  172. {
  173. const AndroidBluetoothMidiDevice& device = devices.getReference (rowNumber);
  174. const String statusString (getDeviceStatusString (device.connectionStatus));
  175. g.fillAll (Colours::white);
  176. const float xmargin = 3.0f;
  177. const float ymargin = 3.0f;
  178. const float fontHeight = 0.4f * (float) height;
  179. const float deviceNameWidth = 0.6f * (float) width;
  180. g.setFont (fontHeight);
  181. g.setColour (getDeviceNameFontColour (device.connectionStatus));
  182. g.drawText (device.name,
  183. Rectangle<float> (xmargin, ymargin, deviceNameWidth - (2.0f * xmargin), (float) height - (2.0f * ymargin)),
  184. Justification::topLeft, true);
  185. g.setColour (getDeviceStatusFontColour (device.connectionStatus));
  186. g.drawText (statusString,
  187. Rectangle<float> (deviceNameWidth + xmargin, ymargin,
  188. (float) width - deviceNameWidth - (2.0f * xmargin), (float) height - (2.0f * ymargin)),
  189. Justification::topRight, true);
  190. g.setColour (Colours::grey);
  191. g.drawHorizontalLine (height - 1, xmargin, (float) width);
  192. }
  193. }
  194. //==============================================================================
  195. static Colour getDeviceNameFontColour (DeviceStatus deviceStatus) noexcept
  196. {
  197. if (deviceStatus == AndroidBluetoothMidiDevice::offline)
  198. return Colours::grey;
  199. return Colours::black;
  200. }
  201. static Colour getDeviceStatusFontColour (DeviceStatus deviceStatus) noexcept
  202. {
  203. if (deviceStatus == AndroidBluetoothMidiDevice::offline
  204. || deviceStatus == AndroidBluetoothMidiDevice::connecting
  205. || deviceStatus == AndroidBluetoothMidiDevice::disconnecting)
  206. return Colours::grey;
  207. if (deviceStatus == AndroidBluetoothMidiDevice::connected)
  208. return Colours::green;
  209. return Colours::black;
  210. }
  211. static String getDeviceStatusString (DeviceStatus deviceStatus) noexcept
  212. {
  213. if (deviceStatus == AndroidBluetoothMidiDevice::offline) return "Offline";
  214. if (deviceStatus == AndroidBluetoothMidiDevice::connected) return "Connected";
  215. if (deviceStatus == AndroidBluetoothMidiDevice::disconnected) return "Not connected";
  216. if (deviceStatus == AndroidBluetoothMidiDevice::connecting) return "Connecting...";
  217. if (deviceStatus == AndroidBluetoothMidiDevice::disconnecting) return "Disconnecting...";
  218. // unknown device state!
  219. jassertfalse;
  220. return "Status unknown";
  221. }
  222. //==============================================================================
  223. void listBoxItemClicked (int row, const MouseEvent&) override
  224. {
  225. const AndroidBluetoothMidiDevice& device = devices.getReference (row);
  226. if (device.connectionStatus == AndroidBluetoothMidiDevice::disconnected)
  227. disconnectedDeviceClicked (row);
  228. else if (device.connectionStatus == AndroidBluetoothMidiDevice::connected)
  229. connectedDeviceClicked (row);
  230. }
  231. void timerCallback() override
  232. {
  233. updateDeviceList();
  234. }
  235. //==============================================================================
  236. struct PairDeviceThread : public Thread,
  237. private AsyncUpdater
  238. {
  239. PairDeviceThread (const String& bluetoothAddressOfDeviceToPair,
  240. AndroidBluetoothMidiDevicesListBox& ownerListBox)
  241. : Thread ("JUCE Bluetooth MIDI Device Pairing Thread"),
  242. bluetoothAddress (bluetoothAddressOfDeviceToPair),
  243. owner (&ownerListBox)
  244. {
  245. startThread();
  246. }
  247. void run() override
  248. {
  249. AndroidBluetoothMidiInterface::pairBluetoothMidiDevice (bluetoothAddress);
  250. triggerAsyncUpdate();
  251. }
  252. void handleAsyncUpdate() override
  253. {
  254. if (owner != nullptr)
  255. owner->pairDeviceThreadFinished();
  256. delete this;
  257. }
  258. private:
  259. String bluetoothAddress;
  260. Component::SafePointer<AndroidBluetoothMidiDevicesListBox> owner;
  261. };
  262. //==============================================================================
  263. void disconnectedDeviceClicked (int row)
  264. {
  265. stopTimer();
  266. AndroidBluetoothMidiDevice& device = devices.getReference (row);
  267. device.connectionStatus = AndroidBluetoothMidiDevice::connecting;
  268. updateContent();
  269. repaint();
  270. new PairDeviceThread (device.bluetoothAddress, *this);
  271. }
  272. void connectedDeviceClicked (int row)
  273. {
  274. AndroidBluetoothMidiDevice& device = devices.getReference (row);
  275. device.connectionStatus = AndroidBluetoothMidiDevice::disconnecting;
  276. updateContent();
  277. repaint();
  278. AndroidBluetoothMidiInterface::unpairBluetoothMidiDevice (device.bluetoothAddress);
  279. }
  280. //==============================================================================
  281. void updateDeviceList()
  282. {
  283. StringArray bluetoothAddresses = AndroidBluetoothMidiInterface::getBluetoothMidiDevicesNearby();
  284. Array<AndroidBluetoothMidiDevice> newDevices;
  285. for (String* address = bluetoothAddresses.begin();
  286. address != bluetoothAddresses.end(); ++address)
  287. {
  288. String name = AndroidBluetoothMidiInterface::getHumanReadableStringForBluetoothAddress (*address);
  289. DeviceStatus status;
  290. switch (AndroidBluetoothMidiInterface::isBluetoothDevicePaired (*address))
  291. {
  292. case AndroidBluetoothMidiInterface::pairing:
  293. status = AndroidBluetoothMidiDevice::connecting;
  294. break;
  295. case AndroidBluetoothMidiInterface::paired:
  296. status = AndroidBluetoothMidiDevice::connected;
  297. break;
  298. case AndroidBluetoothMidiInterface::unpaired:
  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() override
  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