/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2015 - ROLI Ltd. Permission is granted to use this software under the terms of either: a) the GPL v2 (or any later version) b) the Affero GPL v3 Details of these licenses can be found at: www.gnu.org/licenses JUCE is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. ------------------------------------------------------------------------------ To release a closed-source product which uses JUCE, commercial licenses are available: visit www.juce.com for more information. ============================================================================== */ //============================================================================== #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ METHOD (getMidiBluetoothAddresses, "getMidiBluetoothAddresses", "()[Ljava/lang/String;") \ METHOD (pairBluetoothMidiDevice, "pairBluetoothMidiDevice", "(Ljava/lang/String;)Z") \ METHOD (unpairBluetoothMidiDevice, "unpairBluetoothMidiDevice", "(Ljava/lang/String;)V") \ METHOD (getHumanReadableStringForBluetoothAddress, "getHumanReadableStringForBluetoothAddress", "(Ljava/lang/String;)Ljava/lang/String;") \ METHOD (isBluetoothDevicePaired, "isBluetoothDevicePaired", "(Ljava/lang/String;)Z") DECLARE_JNI_CLASS (AndroidBluetoothManager, JUCE_ANDROID_ACTIVITY_CLASSPATH "$BluetoothManager"); #undef JNI_CLASS_MEMBERS //============================================================================== struct AndroidBluetoothMidiInterface { static StringArray getBluetoothMidiDevicesNearby() { StringArray retval; JNIEnv* env = getEnv(); LocalRef btManager (android.activity.callObjectMethod (JuceAppActivity.getAndroidBluetoothManager)); // if this is null then bluetooth is not enabled if (btManager.get() == nullptr) return StringArray(); jobjectArray jDevices = (jobjectArray) env->CallObjectMethod (btManager.get(), AndroidBluetoothManager.getMidiBluetoothAddresses); LocalRef devices (jDevices); const int count = env->GetArrayLength (devices.get()); for (int i = 0; i < count; ++i) { LocalRef string ((jstring) env->GetObjectArrayElement (devices.get(), i)); retval.add (juceString (string)); } return retval; } //============================================================================== static bool pairBluetoothMidiDevice (const String& bluetoothAddress) { JNIEnv* env = getEnv(); LocalRef btManager (android.activity.callObjectMethod (JuceAppActivity.getAndroidBluetoothManager)); if (btManager.get() == nullptr) return false; jboolean result = env->CallBooleanMethod (btManager.get(), AndroidBluetoothManager.pairBluetoothMidiDevice, javaString (bluetoothAddress).get()); return result; } static void unpairBluetoothMidiDevice (const String& bluetoothAddress) { JNIEnv* env = getEnv(); LocalRef btManager (android.activity.callObjectMethod (JuceAppActivity.getAndroidBluetoothManager)); if (btManager.get() != nullptr) env->CallVoidMethod (btManager.get(), AndroidBluetoothManager.unpairBluetoothMidiDevice, javaString (bluetoothAddress).get()); } //============================================================================== static String getHumanReadableStringForBluetoothAddress (const String& address) { JNIEnv* env = getEnv(); LocalRef btManager (android.activity.callObjectMethod (JuceAppActivity.getAndroidBluetoothManager)); if (btManager.get() == nullptr) return address; LocalRef string ((jstring) env->CallObjectMethod (btManager.get(), AndroidBluetoothManager.getHumanReadableStringForBluetoothAddress, javaString (address).get())); if (string.get() == nullptr) return address; return juceString (string); } //============================================================================== static bool isBluetoothDevicePaired (const String& address) { JNIEnv* env = getEnv(); LocalRef btManager (android.activity.callObjectMethod (JuceAppActivity.getAndroidBluetoothManager)); if (btManager.get() == nullptr) return false; return env->CallBooleanMethod (btManager.get(), AndroidBluetoothManager.isBluetoothDevicePaired, javaString (address).get()); } }; //============================================================================== struct AndroidBluetoothMidiDevice { enum ConnectionStatus { offline, connected, disconnected, connecting, disconnecting }; AndroidBluetoothMidiDevice (String deviceName, String address, ConnectionStatus status) : name (deviceName), bluetoothAddress (address), connectionStatus (status) { // can't create a device without a valid name and bluetooth address! jassert (! name.isEmpty()); jassert (! bluetoothAddress.isEmpty()); } bool operator== (const AndroidBluetoothMidiDevice& other) const noexcept { return bluetoothAddress == other.bluetoothAddress; } bool operator!= (const AndroidBluetoothMidiDevice& other) const noexcept { return ! operator== (other); } const String name, bluetoothAddress; ConnectionStatus connectionStatus; }; //============================================================================== class AndroidBluetoothMidiDevicesListBox : public ListBox, private ListBoxModel, private Timer { public: //============================================================================== AndroidBluetoothMidiDevicesListBox() : timerPeriodInMs (1000) { setRowHeight (40); setModel (this); setOutlineThickness (1); updateDeviceList(); startTimer (timerPeriodInMs); } void pairDeviceThreadFinished() // callback from PairDeviceThread { updateDeviceList(); startTimer (timerPeriodInMs); } private: //============================================================================== typedef AndroidBluetoothMidiDevice::ConnectionStatus DeviceStatus; int getNumRows() override { return devices.size(); } void paintListBoxItem (int rowNumber, Graphics& g, int width, int height, bool rowIsSelected) override { if (isPositiveAndBelow (rowNumber, devices.size())) { const AndroidBluetoothMidiDevice& device = devices.getReference (rowNumber); const String statusString (getDeviceStatusString (device.connectionStatus)); g.fillAll (Colours::white); const float xmargin = 3.0f; const float ymargin = 3.0f; const float fontHeight = 0.4f * height; const float deviceNameWidth = 0.6f * width; g.setFont (fontHeight); g.setColour (getDeviceNameFontColour (device.connectionStatus)); g.drawText (device.name, xmargin, ymargin, deviceNameWidth - (2.0f * xmargin), height - (2.0f * ymargin), Justification::topLeft, true); g.setColour (getDeviceStatusFontColour (device.connectionStatus)); g.drawText (statusString, deviceNameWidth + xmargin, ymargin, width - deviceNameWidth - (2.0f * xmargin), height - (2.0f * ymargin), Justification::topRight, true); g.setColour (Colours::grey); g.drawHorizontalLine (height - 1, xmargin, width); } } //============================================================================== static Colour getDeviceNameFontColour (DeviceStatus deviceStatus) noexcept { if (deviceStatus == AndroidBluetoothMidiDevice::offline) return Colours::grey; return Colours::black; } static Colour getDeviceStatusFontColour (DeviceStatus deviceStatus) noexcept { if (deviceStatus == AndroidBluetoothMidiDevice::offline || deviceStatus == AndroidBluetoothMidiDevice::connecting || deviceStatus == AndroidBluetoothMidiDevice::disconnecting) return Colours::grey; if (deviceStatus == AndroidBluetoothMidiDevice::connected) return Colours::green; return Colours::black; } static String getDeviceStatusString (DeviceStatus deviceStatus) noexcept { if (deviceStatus == AndroidBluetoothMidiDevice::offline) return "Offline"; if (deviceStatus == AndroidBluetoothMidiDevice::connected) return "Connected"; if (deviceStatus == AndroidBluetoothMidiDevice::disconnected) return "Not connected"; if (deviceStatus == AndroidBluetoothMidiDevice::connecting) return "Connecting..."; if (deviceStatus == AndroidBluetoothMidiDevice::disconnecting) return "Disconnecting..."; // unknown device state! jassertfalse; return "Status unknown"; } //============================================================================== void listBoxItemClicked (int row, const MouseEvent&) override { const AndroidBluetoothMidiDevice& device = devices.getReference (row); if (device.connectionStatus == AndroidBluetoothMidiDevice::disconnected) disconnectedDeviceClicked (row); else if (device.connectionStatus == AndroidBluetoothMidiDevice::connected) connectedDeviceClicked (row); } void timerCallback() override { updateDeviceList(); } //============================================================================== struct PairDeviceThread : public Thread, private AsyncUpdater { PairDeviceThread (const String& bluetoothAddressOfDeviceToPair, AndroidBluetoothMidiDevicesListBox& ownerListBox) : Thread ("JUCE Bluetooth MIDI Device Pairing Thread"), bluetoothAddress (bluetoothAddressOfDeviceToPair), owner (&ownerListBox) { startThread(); } void run() override { AndroidBluetoothMidiInterface::pairBluetoothMidiDevice (bluetoothAddress); triggerAsyncUpdate(); } void handleAsyncUpdate() override { if (owner != nullptr) owner->pairDeviceThreadFinished(); delete this; } private: String bluetoothAddress; Component::SafePointer owner; }; //============================================================================== void disconnectedDeviceClicked (int row) { stopTimer(); AndroidBluetoothMidiDevice& device = devices.getReference (row); device.connectionStatus = AndroidBluetoothMidiDevice::connecting; updateContent(); repaint(); new PairDeviceThread (device.bluetoothAddress, *this); } void connectedDeviceClicked (int row) { AndroidBluetoothMidiDevice& device = devices.getReference (row); device.connectionStatus = AndroidBluetoothMidiDevice::disconnecting; updateContent(); repaint(); AndroidBluetoothMidiInterface::unpairBluetoothMidiDevice (device.bluetoothAddress); } //============================================================================== void updateDeviceList() { StringArray bluetoothAddresses = AndroidBluetoothMidiInterface::getBluetoothMidiDevicesNearby(); Array newDevices; for (String* address = bluetoothAddresses.begin(); address != bluetoothAddresses.end(); ++address) { String name = AndroidBluetoothMidiInterface::getHumanReadableStringForBluetoothAddress (*address); DeviceStatus status = AndroidBluetoothMidiInterface::isBluetoothDevicePaired (*address) ? AndroidBluetoothMidiDevice::connected : AndroidBluetoothMidiDevice::disconnected; newDevices.add (AndroidBluetoothMidiDevice (name, *address, status)); } devices.swapWith (newDevices); updateContent(); repaint(); } Array devices; const int timerPeriodInMs; }; //============================================================================== class BluetoothMidiSelectorOverlay : public Component { public: BluetoothMidiSelectorOverlay (ModalComponentManager::Callback* exitCallbackToUse) { ScopedPointer exitCallback (exitCallbackToUse); setAlwaysOnTop (true); setVisible (true); addToDesktop (ComponentPeer::windowHasDropShadow); setBounds (0, 0, getParentWidth(), getParentHeight()); toFront (true); addAndMakeVisible (bluetoothDevicesList); enterModalState (true, exitCallback.release(), true); } void paint (Graphics& g) override { g.fillAll (Colours::black.withAlpha (0.6f)); g.setColour (Colour (0xffdfdfdf)); Rectangle overlayBounds = getOverlayBounds(); g.fillRect (overlayBounds); g.setColour (Colours::black); g.setFont (16); g.drawText ("Bluetooth MIDI Devices", overlayBounds.removeFromTop (20).reduced (3, 3), Justification::topLeft, true); overlayBounds.removeFromTop (2); g.setFont (12); g.drawText ("tap to connect/disconnect", overlayBounds.removeFromTop (18).reduced (3, 3), Justification::topLeft, true); } void inputAttemptWhenModal() override { exitModalState (0); } void mouseDrag (const MouseEvent&) override {} void mouseDown (const MouseEvent&) override { exitModalState (0); } void resized() override { update(); } void parentSizeChanged() override { update(); } private: void update() { setBounds (0, 0, getParentWidth(), getParentHeight()); bluetoothDevicesList.setBounds (getOverlayBounds().withTrimmedTop (40)); } Rectangle getOverlayBounds() const noexcept { const int pw = getParentWidth(); const int ph = getParentHeight(); return Rectangle (pw, ph).withSizeKeepingCentre (jmin (400, pw - 14), jmin (300, ph - 40)); } AndroidBluetoothMidiDevicesListBox bluetoothDevicesList; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BluetoothMidiSelectorOverlay) }; //============================================================================== bool BluetoothMidiDevicePairingDialogue::open (ModalComponentManager::Callback* exitCallbackPtr) { ScopedPointer exitCallback (exitCallbackPtr); if (! RuntimePermissions::isGranted (RuntimePermissions::bluetoothMidi)) { // If you hit this assert, you probably forgot to get RuntimePermissions::bluetoothMidi. // This is not going to work, boo! The pairing dialogue won't be able to scan for or // find any devices, it will just display an empty list, so don't bother opening it. jassertfalse; return false; } BluetoothMidiSelectorOverlay* overlay = new BluetoothMidiSelectorOverlay (exitCallback.release()); return true; } bool BluetoothMidiDevicePairingDialogue::isAvailable() { jobject btManager (android.activity.callObjectMethod (JuceAppActivity.getAndroidBluetoothManager)); return btManager != nullptr; }