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.

518 lines
19KB

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