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.

483 lines
18KB

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