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.

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