1 /*
2 ==============================================================================
3
4 This file is part of the JUCE library.
5 Copyright (c) 2020 - Raw Material Software Limited
6
7 JUCE is an open source library subject to commercial or open-source
8 licensing.
9
10 By using JUCE, you agree to the terms of both the JUCE 6 End-User License
11 Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
12
13 End User License Agreement: www.juce.com/juce-6-licence
14 Privacy Policy: www.juce.com/juce-privacy-policy
15
16 Or: You may also use this code under the terms of the GPL v3 (see
17 www.gnu.org/licenses).
18
19 JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
20 EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
21 DISCLAIMED.
22
23 ==============================================================================
24 */
25
26 namespace juce
27 {
28
29 #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
30 STATICMETHOD (getAndroidBluetoothManager, "getAndroidBluetoothManager", "(Landroid/content/Context;)Lcom/rmsl/juce/JuceMidiSupport$BluetoothManager;")
31
32 DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidJuceMidiSupport, "com/rmsl/juce/JuceMidiSupport", 23)
33 #undef JNI_CLASS_MEMBERS
34
35 #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
36 METHOD (getMidiBluetoothAddresses, "getMidiBluetoothAddresses", "()[Ljava/lang/String;") \
37 METHOD (pairBluetoothMidiDevice, "pairBluetoothMidiDevice", "(Ljava/lang/String;)Z") \
38 METHOD (unpairBluetoothMidiDevice, "unpairBluetoothMidiDevice", "(Ljava/lang/String;)V") \
39 METHOD (getHumanReadableStringForBluetoothAddress, "getHumanReadableStringForBluetoothAddress", "(Ljava/lang/String;)Ljava/lang/String;") \
40 METHOD (getBluetoothDeviceStatus, "getBluetoothDeviceStatus", "(Ljava/lang/String;)I") \
41 METHOD (startStopScan, "startStopScan", "(Z)V")
42
43 DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidBluetoothManager, "com/rmsl/juce/JuceMidiSupport$BluetoothManager", 23)
44 #undef JNI_CLASS_MEMBERS
45
46 //==============================================================================
47 struct AndroidBluetoothMidiInterface
48 {
startStopScanjuce::AndroidBluetoothMidiInterface49 static void startStopScan (bool startScanning)
50 {
51 JNIEnv* env = getEnv();
52 LocalRef<jobject> btManager (env->CallStaticObjectMethod (AndroidJuceMidiSupport, AndroidJuceMidiSupport.getAndroidBluetoothManager, getAppContext().get()));
53
54 if (btManager.get() != nullptr)
55 env->CallVoidMethod (btManager.get(), AndroidBluetoothManager.startStopScan, (jboolean) (startScanning ? 1 : 0));
56 }
57
getBluetoothMidiDevicesNearbyjuce::AndroidBluetoothMidiInterface58 static StringArray getBluetoothMidiDevicesNearby()
59 {
60 StringArray retval;
61
62 JNIEnv* env = getEnv();
63
64 LocalRef<jobject> btManager (env->CallStaticObjectMethod (AndroidJuceMidiSupport, AndroidJuceMidiSupport.getAndroidBluetoothManager, getAppContext().get()));
65
66 // if this is null then bluetooth is not enabled
67 if (btManager.get() == nullptr)
68 return {};
69
70 jobjectArray jDevices = (jobjectArray) env->CallObjectMethod (btManager.get(),
71 AndroidBluetoothManager.getMidiBluetoothAddresses);
72 LocalRef<jobjectArray> devices (jDevices);
73
74 const int count = env->GetArrayLength (devices.get());
75
76 for (int i = 0; i < count; ++i)
77 {
78 LocalRef<jstring> string ((jstring) env->GetObjectArrayElement (devices.get(), i));
79 retval.add (juceString (string));
80 }
81
82 return retval;
83 }
84
85 //==============================================================================
pairBluetoothMidiDevicejuce::AndroidBluetoothMidiInterface86 static bool pairBluetoothMidiDevice (const String& bluetoothAddress)
87 {
88 JNIEnv* env = getEnv();
89
90 LocalRef<jobject> btManager (env->CallStaticObjectMethod (AndroidJuceMidiSupport, AndroidJuceMidiSupport.getAndroidBluetoothManager, getAppContext().get()));
91 if (btManager.get() == nullptr)
92 return false;
93
94 jboolean result = env->CallBooleanMethod (btManager.get(), AndroidBluetoothManager.pairBluetoothMidiDevice,
95 javaString (bluetoothAddress).get());
96
97 return result;
98 }
99
unpairBluetoothMidiDevicejuce::AndroidBluetoothMidiInterface100 static void unpairBluetoothMidiDevice (const String& bluetoothAddress)
101 {
102 JNIEnv* env = getEnv();
103
104 LocalRef<jobject> btManager (env->CallStaticObjectMethod (AndroidJuceMidiSupport, AndroidJuceMidiSupport.getAndroidBluetoothManager, getAppContext().get()));
105
106 if (btManager.get() != nullptr)
107 env->CallVoidMethod (btManager.get(), AndroidBluetoothManager.unpairBluetoothMidiDevice,
108 javaString (bluetoothAddress).get());
109 }
110
111 //==============================================================================
getHumanReadableStringForBluetoothAddressjuce::AndroidBluetoothMidiInterface112 static String getHumanReadableStringForBluetoothAddress (const String& address)
113 {
114 JNIEnv* env = getEnv();
115
116 LocalRef<jobject> btManager (env->CallStaticObjectMethod (AndroidJuceMidiSupport, AndroidJuceMidiSupport.getAndroidBluetoothManager, getAppContext().get()));
117
118 if (btManager.get() == nullptr)
119 return address;
120
121 LocalRef<jstring> string ((jstring) env->CallObjectMethod (btManager.get(),
122 AndroidBluetoothManager.getHumanReadableStringForBluetoothAddress,
123 javaString (address).get()));
124
125
126 if (string.get() == nullptr)
127 return address;
128
129 return juceString (string);
130 }
131
132 //==============================================================================
133 enum PairStatus
134 {
135 unpaired = 0,
136 paired = 1,
137 pairing = 2
138 };
139
isBluetoothDevicePairedjuce::AndroidBluetoothMidiInterface140 static PairStatus isBluetoothDevicePaired (const String& address)
141 {
142 JNIEnv* env = getEnv();
143
144 LocalRef<jobject> btManager (env->CallStaticObjectMethod (AndroidJuceMidiSupport, AndroidJuceMidiSupport.getAndroidBluetoothManager, getAppContext().get()));
145
146 if (btManager.get() == nullptr)
147 return unpaired;
148
149 return static_cast<PairStatus> (env->CallIntMethod (btManager.get(), AndroidBluetoothManager.getBluetoothDeviceStatus,
150 javaString (address).get()));
151 }
152 };
153
154 //==============================================================================
155 struct AndroidBluetoothMidiDevice
156 {
157 enum ConnectionStatus
158 {
159 offline,
160 connected,
161 disconnected,
162 connecting,
163 disconnecting
164 };
165
AndroidBluetoothMidiDevicejuce::AndroidBluetoothMidiDevice166 AndroidBluetoothMidiDevice (String deviceName, String address, ConnectionStatus status)
167 : name (deviceName), bluetoothAddress (address), connectionStatus (status)
168 {
169 // can't create a device without a valid name and bluetooth address!
170 jassert (! name.isEmpty());
171 jassert (! bluetoothAddress.isEmpty());
172 }
173
operator ==juce::AndroidBluetoothMidiDevice174 bool operator== (const AndroidBluetoothMidiDevice& other) const noexcept
175 {
176 return bluetoothAddress == other.bluetoothAddress;
177 }
178
operator !=juce::AndroidBluetoothMidiDevice179 bool operator!= (const AndroidBluetoothMidiDevice& other) const noexcept
180 {
181 return ! operator== (other);
182 }
183
184 const String name, bluetoothAddress;
185 ConnectionStatus connectionStatus;
186 };
187
188 //==============================================================================
189 class AndroidBluetoothMidiDevicesListBox : public ListBox,
190 private ListBoxModel,
191 private Timer
192 {
193 public:
194 //==============================================================================
AndroidBluetoothMidiDevicesListBox()195 AndroidBluetoothMidiDevicesListBox()
196 : timerPeriodInMs (1000)
197 {
198 setRowHeight (40);
199 setModel (this);
200 setOutlineThickness (1);
201 startTimer (timerPeriodInMs);
202 }
203
pairDeviceThreadFinished()204 void pairDeviceThreadFinished() // callback from PairDeviceThread
205 {
206 updateDeviceList();
207 startTimer (timerPeriodInMs);
208 }
209
210 private:
211 //==============================================================================
212 typedef AndroidBluetoothMidiDevice::ConnectionStatus DeviceStatus;
213
getNumRows()214 int getNumRows() override
215 {
216 return devices.size();
217 }
218
paintListBoxItem(int rowNumber,Graphics & g,int width,int height,bool)219 void paintListBoxItem (int rowNumber, Graphics& g,
220 int width, int height, bool) override
221 {
222 if (isPositiveAndBelow (rowNumber, devices.size()))
223 {
224 const AndroidBluetoothMidiDevice& device = devices.getReference (rowNumber);
225 const String statusString (getDeviceStatusString (device.connectionStatus));
226
227 g.fillAll (Colours::white);
228
229 const float xmargin = 3.0f;
230 const float ymargin = 3.0f;
231 const float fontHeight = 0.4f * (float) height;
232 const float deviceNameWidth = 0.6f * (float) width;
233
234 g.setFont (fontHeight);
235
236 g.setColour (getDeviceNameFontColour (device.connectionStatus));
237 g.drawText (device.name,
238 Rectangle<float> (xmargin, ymargin, deviceNameWidth - (2.0f * xmargin), (float) height - (2.0f * ymargin)),
239 Justification::topLeft, true);
240
241 g.setColour (getDeviceStatusFontColour (device.connectionStatus));
242 g.drawText (statusString,
243 Rectangle<float> (deviceNameWidth + xmargin, ymargin,
244 (float) width - deviceNameWidth - (2.0f * xmargin), (float) height - (2.0f * ymargin)),
245 Justification::topRight, true);
246
247 g.setColour (Colours::grey);
248 g.drawHorizontalLine (height - 1, xmargin, (float) width);
249 }
250 }
251
252 //==============================================================================
getDeviceNameFontColour(DeviceStatus deviceStatus)253 static Colour getDeviceNameFontColour (DeviceStatus deviceStatus) noexcept
254 {
255 if (deviceStatus == AndroidBluetoothMidiDevice::offline)
256 return Colours::grey;
257
258 return Colours::black;
259 }
260
getDeviceStatusFontColour(DeviceStatus deviceStatus)261 static Colour getDeviceStatusFontColour (DeviceStatus deviceStatus) noexcept
262 {
263 if (deviceStatus == AndroidBluetoothMidiDevice::offline
264 || deviceStatus == AndroidBluetoothMidiDevice::connecting
265 || deviceStatus == AndroidBluetoothMidiDevice::disconnecting)
266 return Colours::grey;
267
268 if (deviceStatus == AndroidBluetoothMidiDevice::connected)
269 return Colours::green;
270
271 return Colours::black;
272 }
273
getDeviceStatusString(DeviceStatus deviceStatus)274 static String getDeviceStatusString (DeviceStatus deviceStatus) noexcept
275 {
276 if (deviceStatus == AndroidBluetoothMidiDevice::offline) return "Offline";
277 if (deviceStatus == AndroidBluetoothMidiDevice::connected) return "Connected";
278 if (deviceStatus == AndroidBluetoothMidiDevice::disconnected) return "Not connected";
279 if (deviceStatus == AndroidBluetoothMidiDevice::connecting) return "Connecting...";
280 if (deviceStatus == AndroidBluetoothMidiDevice::disconnecting) return "Disconnecting...";
281
282 // unknown device state!
283 jassertfalse;
284 return "Status unknown";
285 }
286
287 //==============================================================================
listBoxItemClicked(int row,const MouseEvent &)288 void listBoxItemClicked (int row, const MouseEvent&) override
289 {
290 const AndroidBluetoothMidiDevice& device = devices.getReference (row);
291
292 if (device.connectionStatus == AndroidBluetoothMidiDevice::disconnected)
293 disconnectedDeviceClicked (row);
294
295 else if (device.connectionStatus == AndroidBluetoothMidiDevice::connected)
296 connectedDeviceClicked (row);
297 }
298
timerCallback()299 void timerCallback() override
300 {
301 updateDeviceList();
302 }
303
304 //==============================================================================
305 struct PairDeviceThread : public Thread,
306 private AsyncUpdater
307 {
PairDeviceThreadjuce::AndroidBluetoothMidiDevicesListBox::PairDeviceThread308 PairDeviceThread (const String& bluetoothAddressOfDeviceToPair,
309 AndroidBluetoothMidiDevicesListBox& ownerListBox)
310 : Thread ("JUCE Bluetooth MIDI Device Pairing Thread"),
311 bluetoothAddress (bluetoothAddressOfDeviceToPair),
312 owner (&ownerListBox)
313 {
314 startThread();
315 }
316
runjuce::AndroidBluetoothMidiDevicesListBox::PairDeviceThread317 void run() override
318 {
319 AndroidBluetoothMidiInterface::pairBluetoothMidiDevice (bluetoothAddress);
320 triggerAsyncUpdate();
321 }
322
handleAsyncUpdatejuce::AndroidBluetoothMidiDevicesListBox::PairDeviceThread323 void handleAsyncUpdate() override
324 {
325 if (owner != nullptr)
326 owner->pairDeviceThreadFinished();
327
328 delete this;
329 }
330
331 private:
332 String bluetoothAddress;
333 Component::SafePointer<AndroidBluetoothMidiDevicesListBox> owner;
334 };
335
336 //==============================================================================
disconnectedDeviceClicked(int row)337 void disconnectedDeviceClicked (int row)
338 {
339 stopTimer();
340
341 AndroidBluetoothMidiDevice& device = devices.getReference (row);
342 device.connectionStatus = AndroidBluetoothMidiDevice::connecting;
343 updateContent();
344 repaint();
345
346 new PairDeviceThread (device.bluetoothAddress, *this);
347 }
348
connectedDeviceClicked(int row)349 void connectedDeviceClicked (int row)
350 {
351 AndroidBluetoothMidiDevice& device = devices.getReference (row);
352 device.connectionStatus = AndroidBluetoothMidiDevice::disconnecting;
353 updateContent();
354 repaint();
355 AndroidBluetoothMidiInterface::unpairBluetoothMidiDevice (device.bluetoothAddress);
356 }
357
358 //==============================================================================
updateDeviceList()359 void updateDeviceList()
360 {
361 StringArray bluetoothAddresses = AndroidBluetoothMidiInterface::getBluetoothMidiDevicesNearby();
362
363 Array<AndroidBluetoothMidiDevice> newDevices;
364
365 for (String* address = bluetoothAddresses.begin();
366 address != bluetoothAddresses.end(); ++address)
367 {
368 String name = AndroidBluetoothMidiInterface::getHumanReadableStringForBluetoothAddress (*address);
369
370 DeviceStatus status;
371 switch (AndroidBluetoothMidiInterface::isBluetoothDevicePaired (*address))
372 {
373 case AndroidBluetoothMidiInterface::pairing:
374 status = AndroidBluetoothMidiDevice::connecting;
375 break;
376 case AndroidBluetoothMidiInterface::paired:
377 status = AndroidBluetoothMidiDevice::connected;
378 break;
379 case AndroidBluetoothMidiInterface::unpaired:
380 default:
381 status = AndroidBluetoothMidiDevice::disconnected;
382 }
383
384 newDevices.add (AndroidBluetoothMidiDevice (name, *address, status));
385 }
386
387 devices.swapWith (newDevices);
388 updateContent();
389 repaint();
390 }
391
392 Array<AndroidBluetoothMidiDevice> devices;
393 const int timerPeriodInMs;
394 };
395
396 //==============================================================================
397 class BluetoothMidiSelectorOverlay : public Component
398 {
399 public:
BluetoothMidiSelectorOverlay(ModalComponentManager::Callback * exitCallbackToUse,const Rectangle<int> & boundsToUse)400 BluetoothMidiSelectorOverlay (ModalComponentManager::Callback* exitCallbackToUse,
401 const Rectangle<int>& boundsToUse)
402 : bounds (boundsToUse)
403 {
404 std::unique_ptr<ModalComponentManager::Callback> exitCallback (exitCallbackToUse);
405
406 AndroidBluetoothMidiInterface::startStopScan (true);
407
408 setAlwaysOnTop (true);
409 setVisible (true);
410 addToDesktop (ComponentPeer::windowHasDropShadow);
411
412 if (bounds.isEmpty())
413 setBounds (0, 0, getParentWidth(), getParentHeight());
414 else
415 setBounds (bounds);
416
417 toFront (true);
418 setOpaque (! bounds.isEmpty());
419
420 addAndMakeVisible (bluetoothDevicesList);
421 enterModalState (true, exitCallback.release(), true);
422 }
423
~BluetoothMidiSelectorOverlay()424 ~BluetoothMidiSelectorOverlay() override
425 {
426 AndroidBluetoothMidiInterface::startStopScan (false);
427 }
428
paint(Graphics & g)429 void paint (Graphics& g) override
430 {
431 g.fillAll (bounds.isEmpty() ? Colours::black.withAlpha (0.6f) : Colours::black);
432
433 g.setColour (Colour (0xffdfdfdf));
434 Rectangle<int> overlayBounds = getOverlayBounds();
435 g.fillRect (overlayBounds);
436
437 g.setColour (Colours::black);
438 g.setFont (16);
439 g.drawText ("Bluetooth MIDI Devices",
440 overlayBounds.removeFromTop (20).reduced (3, 3),
441 Justification::topLeft, true);
442
443 overlayBounds.removeFromTop (2);
444
445 g.setFont (12);
446 g.drawText ("tap to connect/disconnect",
447 overlayBounds.removeFromTop (18).reduced (3, 3),
448 Justification::topLeft, true);
449 }
450
inputAttemptWhenModal()451 void inputAttemptWhenModal() override { exitModalState (0); }
mouseDrag(const MouseEvent &)452 void mouseDrag (const MouseEvent&) override {}
mouseDown(const MouseEvent &)453 void mouseDown (const MouseEvent&) override { exitModalState (0); }
resized()454 void resized() override { update(); }
parentSizeChanged()455 void parentSizeChanged() override { update(); }
456
457 private:
458 Rectangle<int> bounds;
459
update()460 void update()
461 {
462 if (bounds.isEmpty())
463 setBounds (0, 0, getParentWidth(), getParentHeight());
464 else
465 setBounds (bounds);
466
467 bluetoothDevicesList.setBounds (getOverlayBounds().withTrimmedTop (40));
468 }
469
getOverlayBounds() const470 Rectangle<int> getOverlayBounds() const noexcept
471 {
472 if (bounds.isEmpty())
473 {
474 const int pw = getParentWidth();
475 const int ph = getParentHeight();
476
477 return Rectangle<int> (pw, ph).withSizeKeepingCentre (jmin (400, pw - 14),
478 jmin (300, ph - 40));
479 }
480
481 return bounds.withZeroOrigin();
482 }
483
484 AndroidBluetoothMidiDevicesListBox bluetoothDevicesList;
485
486 JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BluetoothMidiSelectorOverlay)
487 };
488
489 //==============================================================================
open(ModalComponentManager::Callback * exitCallbackPtr,Rectangle<int> * btBounds)490 bool BluetoothMidiDevicePairingDialogue::open (ModalComponentManager::Callback* exitCallbackPtr,
491 Rectangle<int>* btBounds)
492 {
493 std::unique_ptr<ModalComponentManager::Callback> exitCallback (exitCallbackPtr);
494
495 if (getAndroidSDKVersion() < 23)
496 return false;
497
498 auto boundsToUse = (btBounds != nullptr ? *btBounds : Rectangle<int> {});
499
500 if (! RuntimePermissions::isGranted (RuntimePermissions::bluetoothMidi))
501 {
502 // If you hit this assert, you probably forgot to get RuntimePermissions::bluetoothMidi.
503 // This is not going to work, boo! The pairing dialogue won't be able to scan for or
504 // find any devices, it will just display an empty list, so don't bother opening it.
505 jassertfalse;
506 return false;
507 }
508
509 new BluetoothMidiSelectorOverlay (exitCallback.release(), boundsToUse);
510 return true;
511 }
512
isAvailable()513 bool BluetoothMidiDevicePairingDialogue::isAvailable()
514 {
515 if (getAndroidSDKVersion() < 23)
516 return false;
517
518 auto* env = getEnv();
519
520 LocalRef<jobject> btManager (env->CallStaticObjectMethod (AndroidJuceMidiSupport, AndroidJuceMidiSupport.getAndroidBluetoothManager, getAppContext().get()));
521 return btManager != nullptr;
522 }
523
524 } // namespace juce
525