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