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.

450 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) 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. Rectangle<float> (xmargin, ymargin, deviceNameWidth - (2.0f * xmargin), height - (2.0f * ymargin)),
  165. Justification::topLeft, true);
  166. g.setColour (getDeviceStatusFontColour (device.connectionStatus));
  167. g.drawText (statusString,
  168. Rectangle<float> (deviceNameWidth + xmargin, ymargin,
  169. width - deviceNameWidth - (2.0f * xmargin), height - (2.0f * ymargin)),
  170. Justification::topRight, true);
  171. g.setColour (Colours::grey);
  172. g.drawHorizontalLine (height - 1, xmargin, width);
  173. }
  174. }
  175. //==============================================================================
  176. static Colour getDeviceNameFontColour (DeviceStatus deviceStatus) noexcept
  177. {
  178. if (deviceStatus == AndroidBluetoothMidiDevice::offline)
  179. return Colours::grey;
  180. return Colours::black;
  181. }
  182. static Colour getDeviceStatusFontColour (DeviceStatus deviceStatus) noexcept
  183. {
  184. if (deviceStatus == AndroidBluetoothMidiDevice::offline
  185. || deviceStatus == AndroidBluetoothMidiDevice::connecting
  186. || deviceStatus == AndroidBluetoothMidiDevice::disconnecting)
  187. return Colours::grey;
  188. if (deviceStatus == AndroidBluetoothMidiDevice::connected)
  189. return Colours::green;
  190. return Colours::black;
  191. }
  192. static String getDeviceStatusString (DeviceStatus deviceStatus) noexcept
  193. {
  194. if (deviceStatus == AndroidBluetoothMidiDevice::offline) return "Offline";
  195. if (deviceStatus == AndroidBluetoothMidiDevice::connected) return "Connected";
  196. if (deviceStatus == AndroidBluetoothMidiDevice::disconnected) return "Not connected";
  197. if (deviceStatus == AndroidBluetoothMidiDevice::connecting) return "Connecting...";
  198. if (deviceStatus == AndroidBluetoothMidiDevice::disconnecting) return "Disconnecting...";
  199. // unknown device state!
  200. jassertfalse;
  201. return "Status unknown";
  202. }
  203. //==============================================================================
  204. void listBoxItemClicked (int row, const MouseEvent&) override
  205. {
  206. const AndroidBluetoothMidiDevice& device = devices.getReference (row);
  207. if (device.connectionStatus == AndroidBluetoothMidiDevice::disconnected)
  208. disconnectedDeviceClicked (row);
  209. else if (device.connectionStatus == AndroidBluetoothMidiDevice::connected)
  210. connectedDeviceClicked (row);
  211. }
  212. void timerCallback() override
  213. {
  214. updateDeviceList();
  215. }
  216. //==============================================================================
  217. struct PairDeviceThread : public Thread,
  218. private AsyncUpdater
  219. {
  220. PairDeviceThread (const String& bluetoothAddressOfDeviceToPair,
  221. AndroidBluetoothMidiDevicesListBox& ownerListBox)
  222. : Thread ("JUCE Bluetooth MIDI Device Pairing Thread"),
  223. bluetoothAddress (bluetoothAddressOfDeviceToPair),
  224. owner (&ownerListBox)
  225. {
  226. startThread();
  227. }
  228. void run() override
  229. {
  230. AndroidBluetoothMidiInterface::pairBluetoothMidiDevice (bluetoothAddress);
  231. triggerAsyncUpdate();
  232. }
  233. void handleAsyncUpdate() override
  234. {
  235. if (owner != nullptr)
  236. owner->pairDeviceThreadFinished();
  237. delete this;
  238. }
  239. private:
  240. String bluetoothAddress;
  241. Component::SafePointer<AndroidBluetoothMidiDevicesListBox> owner;
  242. };
  243. //==============================================================================
  244. void disconnectedDeviceClicked (int row)
  245. {
  246. stopTimer();
  247. AndroidBluetoothMidiDevice& device = devices.getReference (row);
  248. device.connectionStatus = AndroidBluetoothMidiDevice::connecting;
  249. updateContent();
  250. repaint();
  251. new PairDeviceThread (device.bluetoothAddress, *this);
  252. }
  253. void connectedDeviceClicked (int row)
  254. {
  255. AndroidBluetoothMidiDevice& device = devices.getReference (row);
  256. device.connectionStatus = AndroidBluetoothMidiDevice::disconnecting;
  257. updateContent();
  258. repaint();
  259. AndroidBluetoothMidiInterface::unpairBluetoothMidiDevice (device.bluetoothAddress);
  260. }
  261. //==============================================================================
  262. void updateDeviceList()
  263. {
  264. StringArray bluetoothAddresses = AndroidBluetoothMidiInterface::getBluetoothMidiDevicesNearby();
  265. Array<AndroidBluetoothMidiDevice> newDevices;
  266. for (String* address = bluetoothAddresses.begin();
  267. address != bluetoothAddresses.end(); ++address)
  268. {
  269. String name = AndroidBluetoothMidiInterface::getHumanReadableStringForBluetoothAddress (*address);
  270. DeviceStatus status = AndroidBluetoothMidiInterface::isBluetoothDevicePaired (*address)
  271. ? AndroidBluetoothMidiDevice::connected
  272. : AndroidBluetoothMidiDevice::disconnected;
  273. newDevices.add (AndroidBluetoothMidiDevice (name, *address, status));
  274. }
  275. devices.swapWith (newDevices);
  276. updateContent();
  277. repaint();
  278. }
  279. Array<AndroidBluetoothMidiDevice> devices;
  280. const int timerPeriodInMs;
  281. };
  282. //==============================================================================
  283. class BluetoothMidiSelectorOverlay : public Component
  284. {
  285. public:
  286. BluetoothMidiSelectorOverlay (ModalComponentManager::Callback* exitCallbackToUse)
  287. {
  288. ScopedPointer<ModalComponentManager::Callback> exitCallback (exitCallbackToUse);
  289. setAlwaysOnTop (true);
  290. setVisible (true);
  291. addToDesktop (ComponentPeer::windowHasDropShadow);
  292. setBounds (0, 0, getParentWidth(), getParentHeight());
  293. toFront (true);
  294. addAndMakeVisible (bluetoothDevicesList);
  295. enterModalState (true, exitCallback.release(), true);
  296. }
  297. void paint (Graphics& g) override
  298. {
  299. g.fillAll (Colours::black.withAlpha (0.6f));
  300. g.setColour (Colour (0xffdfdfdf));
  301. Rectangle<int> overlayBounds = getOverlayBounds();
  302. g.fillRect (overlayBounds);
  303. g.setColour (Colours::black);
  304. g.setFont (16);
  305. g.drawText ("Bluetooth MIDI Devices",
  306. overlayBounds.removeFromTop (20).reduced (3, 3),
  307. Justification::topLeft, true);
  308. overlayBounds.removeFromTop (2);
  309. g.setFont (12);
  310. g.drawText ("tap to connect/disconnect",
  311. overlayBounds.removeFromTop (18).reduced (3, 3),
  312. Justification::topLeft, true);
  313. }
  314. void inputAttemptWhenModal() override { exitModalState (0); }
  315. void mouseDrag (const MouseEvent&) override {}
  316. void mouseDown (const MouseEvent&) override { exitModalState (0); }
  317. void resized() override { update(); }
  318. void parentSizeChanged() override { update(); }
  319. private:
  320. void update()
  321. {
  322. setBounds (0, 0, getParentWidth(), getParentHeight());
  323. bluetoothDevicesList.setBounds (getOverlayBounds().withTrimmedTop (40));
  324. }
  325. Rectangle<int> getOverlayBounds() const noexcept
  326. {
  327. const int pw = getParentWidth();
  328. const int ph = getParentHeight();
  329. return Rectangle<int> (pw, ph).withSizeKeepingCentre (jmin (400, pw - 14),
  330. jmin (300, ph - 40));
  331. }
  332. AndroidBluetoothMidiDevicesListBox bluetoothDevicesList;
  333. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BluetoothMidiSelectorOverlay)
  334. };
  335. //==============================================================================
  336. bool BluetoothMidiDevicePairingDialogue::open (ModalComponentManager::Callback* exitCallbackPtr)
  337. {
  338. ScopedPointer<ModalComponentManager::Callback> exitCallback (exitCallbackPtr);
  339. if (! RuntimePermissions::isGranted (RuntimePermissions::bluetoothMidi))
  340. {
  341. // If you hit this assert, you probably forgot to get RuntimePermissions::bluetoothMidi.
  342. // This is not going to work, boo! The pairing dialogue won't be able to scan for or
  343. // find any devices, it will just display an empty list, so don't bother opening it.
  344. jassertfalse;
  345. return false;
  346. }
  347. new BluetoothMidiSelectorOverlay (exitCallback.release());
  348. return true;
  349. }
  350. bool BluetoothMidiDevicePairingDialogue::isAvailable()
  351. {
  352. jobject btManager (android.activity.callObjectMethod (JuceAppActivity.getAndroidBluetoothManager));
  353. return btManager != nullptr;
  354. }