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.

451 lines
17KB

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