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.

510 lines
18KB

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