//============================================================================== public class BluetoothManager extends ScanCallback { BluetoothManager() { ScanFilter.Builder scanFilterBuilder = new ScanFilter.Builder(); scanFilterBuilder.setServiceUuid (ParcelUuid.fromString (bluetoothLEMidiServiceUUID)); ScanSettings.Builder scanSettingsBuilder = new ScanSettings.Builder(); scanSettingsBuilder.setCallbackType (ScanSettings.CALLBACK_TYPE_ALL_MATCHES) .setScanMode (ScanSettings.SCAN_MODE_LOW_POWER) .setScanMode (ScanSettings.MATCH_MODE_STICKY); BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); if (bluetoothAdapter == null) { Log.d ("JUCE", "BluetoothManager error: could not get default Bluetooth adapter"); return; } BluetoothLeScanner bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner(); if (bluetoothLeScanner == null) { Log.d ("JUCE", "BluetoothManager error: could not get Bluetooth LE scanner"); return; } bluetoothLeScanner.startScan (Arrays.asList (scanFilterBuilder.build()), scanSettingsBuilder.build(), this); } public String[] getMidiBluetoothAddresses() { return bluetoothMidiDevices.toArray (new String[bluetoothMidiDevices.size()]); } public String getHumanReadableStringForBluetoothAddress (String address) { BluetoothDevice btDevice = BluetoothAdapter.getDefaultAdapter().getRemoteDevice (address); return btDevice.getName(); } public boolean isBluetoothDevicePaired (String address) { return getAndroidMidiDeviceManager().isBluetoothDevicePaired (address); } public boolean pairBluetoothMidiDevice(String address) { BluetoothDevice btDevice = BluetoothAdapter.getDefaultAdapter().getRemoteDevice (address); if (btDevice == null) { Log.d ("JUCE", "failed to create buletooth device from address"); return false; } MidiManager mm = (MidiManager) getSystemService (MIDI_SERVICE); PhysicalMidiDevice midiDevice = PhysicalMidiDevice.fromBluetoothLeDevice (btDevice, mm); if (midiDevice != null) { getAndroidMidiDeviceManager().addDeviceToList (midiDevice); return true; } return false; } public void unpairBluetoothMidiDevice (String address) { getAndroidMidiDeviceManager().unpairBluetoothDevice (address); } public void onScanFailed (int errorCode) { } public void onScanResult (int callbackType, ScanResult result) { if (callbackType == ScanSettings.CALLBACK_TYPE_ALL_MATCHES || callbackType == ScanSettings.CALLBACK_TYPE_FIRST_MATCH) { BluetoothDevice device = result.getDevice(); if (device != null) bluetoothMidiDevices.add (device.getAddress()); } if (callbackType == ScanSettings.CALLBACK_TYPE_MATCH_LOST) { Log.d ("JUCE", "ScanSettings.CALLBACK_TYPE_MATCH_LOST"); BluetoothDevice device = result.getDevice(); if (device != null) { bluetoothMidiDevices.remove (device.getAddress()); unpairBluetoothMidiDevice (device.getAddress()); } } } public void onBatchScanResults (List results) { for (ScanResult result : results) onScanResult (ScanSettings.CALLBACK_TYPE_ALL_MATCHES, result); } private BluetoothLeScanner scanner; private static final String bluetoothLEMidiServiceUUID = "03B80E5A-EDE8-4B33-A751-6CE34EC4C700"; private HashSet bluetoothMidiDevices = new HashSet(); } public static class JuceMidiInputPort extends MidiReceiver implements JuceMidiPort { private native void handleReceive (long host, byte[] msg, int offset, int count, long timestamp); public JuceMidiInputPort (PhysicalMidiDevice device, long host, MidiOutputPort midiPort) { parent = device; juceHost = host; port = midiPort; } @Override public boolean isInputPort() { return true; } @Override public void start() { port.connect (this); } @Override public void stop() { port.disconnect (this); } @Override public void close() { stop(); try { port.close(); } catch (IOException e) { Log.d ("JUCE", "JuceMidiInputPort::close: IOException = " + e.toString()); } if (parent != null) { parent.removePort (port.getPortNumber(), true); parent = null; } } public void onSend (byte[] msg, int offset, int count, long timestamp) { if (count > 0) handleReceive (juceHost, msg, offset, count, timestamp); } @Override public MidiPortID getPortId() { return new MidiPortID (port.getPortNumber(), true); } @Override public void sendMidi (byte[] msg, int offset, int count) { } private PhysicalMidiDevice parent = null; private long juceHost = 0; private MidiOutputPort port; } public static class JuceMidiOutputPort implements JuceMidiPort { public JuceMidiOutputPort (PhysicalMidiDevice device, MidiInputPort midiPort) { parent = device; port = midiPort; } @Override public boolean isInputPort() { return false; } @Override public void start() { } @Override public void stop() { } @Override public void sendMidi (byte[] msg, int offset, int count) { try { port.send(msg, offset, count); } catch (IOException e) { Log.d ("JUCE", "JuceMidiOutputPort::sendMidi: IOException = " + e.toString()); } } @Override public void close() { try { port.close(); } catch (IOException e) { Log.d ("JUCE", "JuceMidiOutputPort::close: IOException = " + e.toString()); } if (parent != null) { parent.removePort (port.getPortNumber(), false); parent = null; } } @Override public MidiPortID getPortId() { return new MidiPortID (port.getPortNumber(), false); } private PhysicalMidiDevice parent = null; private MidiInputPort port; } public static class PhysicalMidiDevice { private static class MidiDeviceThread extends Thread { public Handler handler = null; public Object sync = null; public MidiDeviceThread (Object syncrhonization) { sync = syncrhonization; } public void run() { Looper.prepare(); synchronized (sync) { handler = new Handler(); sync.notifyAll(); } Looper.loop(); } } private static class MidiDeviceOpenCallback implements MidiManager.OnDeviceOpenedListener { public Object sync = null; public boolean isWaiting = true; public android.media.midi.MidiDevice theDevice = null; public MidiDeviceOpenCallback (Object waiter) { sync = waiter; } public void onDeviceOpened (MidiDevice device) { synchronized (sync) { theDevice = device; isWaiting = false; sync.notifyAll(); } } } public static PhysicalMidiDevice fromBluetoothLeDevice (BluetoothDevice bluetoothDevice, MidiManager mm) { Object waitForCreation = new Object(); MidiDeviceThread thread = new MidiDeviceThread (waitForCreation); thread.start(); synchronized (waitForCreation) { while (thread.handler == null) { try { waitForCreation.wait(); } catch (InterruptedException e) { Log.d ("JUCE", "Wait was interrupted but we don't care"); } } } Object waitForDevice = new Object(); MidiDeviceOpenCallback openCallback = new MidiDeviceOpenCallback (waitForDevice); synchronized (waitForDevice) { mm.openBluetoothDevice (bluetoothDevice, openCallback, thread.handler); while (openCallback.isWaiting) { try { waitForDevice.wait(); } catch (InterruptedException e) { Log.d ("JUCE", "Wait was interrupted but we don't care"); } } } if (openCallback.theDevice == null) { Log.d ("JUCE", "openBluetoothDevice failed"); return null; } PhysicalMidiDevice device = new PhysicalMidiDevice(); device.handle = openCallback.theDevice; device.info = device.handle.getInfo(); device.bluetoothAddress = bluetoothDevice.getAddress(); device.midiManager = mm; return device; } public void unpair() { if (! bluetoothAddress.equals ("") && handle != null) { JuceMidiPort ports[] = new JuceMidiPort[0]; ports = juceOpenedPorts.values().toArray(ports); for (int i = 0; i < ports.length; ++i) ports[i].close(); juceOpenedPorts.clear(); try { handle.close(); } catch (IOException e) { Log.d ("JUCE", "handle.close(): IOException = " + e.toString()); } handle = null; } } public static PhysicalMidiDevice fromMidiDeviceInfo (MidiDeviceInfo info, MidiManager mm) { PhysicalMidiDevice device = new PhysicalMidiDevice(); device.info = info; device.midiManager = mm; return device; } public PhysicalMidiDevice() { bluetoothAddress = ""; juceOpenedPorts = new Hashtable(); handle = null; } public MidiDeviceInfo.PortInfo[] getPorts() { return info.getPorts(); } public String getHumanReadableNameForPort (MidiDeviceInfo.PortInfo port, int portIndexToUseInName) { String portName = port.getName(); if (portName.equals ("")) portName = ((port.getType() == MidiDeviceInfo.PortInfo.TYPE_OUTPUT) ? "Out " : "In ") + Integer.toString (portIndexToUseInName); return getHumanReadableDeviceName() + " " + portName; } public String getHumanReadableNameForPort (int portType, int androidPortID, int portIndexToUseInName) { MidiDeviceInfo.PortInfo[] ports = info.getPorts(); for (MidiDeviceInfo.PortInfo port : ports) { if (port.getType() == portType) { if (port.getPortNumber() == androidPortID) return getHumanReadableNameForPort (port, portIndexToUseInName); } } return "Unknown"; } public String getHumanReadableDeviceName() { Bundle bundle = info.getProperties(); return bundle.getString (MidiDeviceInfo.PROPERTY_NAME , "Unknown device"); } public void checkIfDeviceCanBeClosed() { if (juceOpenedPorts.size() == 0) { // never close bluetooth LE devices, otherwise they unpair and we have // no idea how many ports they have. // Only remove bluetooth devices when we specifically unpair if (bluetoothAddress.equals ("")) { try { handle.close(); handle = null; } catch (IOException e) { Log.d ("JUCE", "PhysicalMidiDevice::checkIfDeviceCanBeClosed: IOException = " + e.toString()); } } } } public void removePort (int portIdx, boolean isInput) { MidiPortID portID = new MidiPortID (portIdx, isInput); JuceMidiPort port = juceOpenedPorts.get (portID); if (port != null) { juceOpenedPorts.remove (portID); checkIfDeviceCanBeClosed(); return; } // tried to remove a port that was never added assert false; } public JuceMidiPort openPort (int portIdx, boolean isInput, long host) { open(); if (handle == null) { Log.d ("JUCE", "PhysicalMidiDevice::openPort: handle = null, device not open"); return null; } // make sure that the port is not already open if (findPortForIdx (portIdx, isInput) != null) { Log.d ("JUCE", "PhysicalMidiDevice::openInputPort: port already open, not opening again!"); return null; } JuceMidiPort retval = null; if (isInput) { MidiOutputPort androidPort = handle.openOutputPort (portIdx); if (androidPort == null) { Log.d ("JUCE", "PhysicalMidiDevice::openPort: MidiDevice::openOutputPort (portIdx = " + Integer.toString (portIdx) + ") failed!"); return null; } retval = new JuceMidiInputPort (this, host, androidPort); } else { MidiInputPort androidPort = handle.openInputPort (portIdx); if (androidPort == null) { Log.d ("JUCE", "PhysicalMidiDevice::openPort: MidiDevice::openInputPort (portIdx = " + Integer.toString (portIdx) + ") failed!"); return null; } retval = new JuceMidiOutputPort (this, androidPort); } juceOpenedPorts.put (new MidiPortID (portIdx, isInput), retval); return retval; } private JuceMidiPort findPortForIdx (int idx, boolean isInput) { return juceOpenedPorts.get (new MidiPortID (idx, isInput)); } // opens the device private synchronized void open() { if (handle != null) return; Object waitForCreation = new Object(); MidiDeviceThread thread = new MidiDeviceThread (waitForCreation); thread.start(); synchronized(waitForCreation) { while (thread.handler == null) { try { waitForCreation.wait(); } catch (InterruptedException e) { Log.d ("JUCE", "wait was interrupted but we don't care"); } } } Object waitForDevice = new Object(); MidiDeviceOpenCallback openCallback = new MidiDeviceOpenCallback (waitForDevice); synchronized (waitForDevice) { midiManager.openDevice (info, openCallback, thread.handler); while (openCallback.isWaiting) { try { waitForDevice.wait(); } catch (InterruptedException e) { Log.d ("JUCE", "wait was interrupted but we don't care"); } } } handle = openCallback.theDevice; } private MidiDeviceInfo info; private Hashtable juceOpenedPorts; public MidiDevice handle; public String bluetoothAddress; private MidiManager midiManager; } //============================================================================== public class MidiDeviceManager extends MidiManager.DeviceCallback { public class MidiPortPath { public PhysicalMidiDevice midiDevice; public int androidMidiPortID; public int portIndexToUseInName; } public class JuceDeviceList { public ArrayList inPorts = new ArrayList(); public ArrayList outPorts = new ArrayList(); } // We need to keep a thread local copy of the devices // which we returned the last time // getJuceAndroidMidiIn/OutputDevices() was called private final ThreadLocal lastDevicesReturned = new ThreadLocal() { @Override protected JuceDeviceList initialValue() { return new JuceDeviceList(); } }; public MidiDeviceManager() { physicalMidiDevices = new ArrayList(); manager = (MidiManager) getSystemService (MIDI_SERVICE); if (manager == null) { Log.d ("JUCE", "MidiDeviceManager error: could not get MidiManager system service"); return; } manager.registerDeviceCallback (this, null); MidiDeviceInfo[] foundDevices = manager.getDevices(); for (MidiDeviceInfo info : foundDevices) physicalMidiDevices.add (PhysicalMidiDevice.fromMidiDeviceInfo (info, manager)); } // specifically add a device to the list public void addDeviceToList (PhysicalMidiDevice device) { physicalMidiDevices.add (device); } public void unpairBluetoothDevice (String address) { for (int i = 0; i < physicalMidiDevices.size(); ++i) { PhysicalMidiDevice device = physicalMidiDevices.get(i); if (device.bluetoothAddress.equals (address)) { physicalMidiDevices.remove (i); device.unpair(); return; } } } public boolean isBluetoothDevicePaired (String address) { for (int i = 0; i < physicalMidiDevices.size(); ++i) { PhysicalMidiDevice device = physicalMidiDevices.get(i); if (device.bluetoothAddress.equals (address)) return true; } return false; } public String[] getJuceAndroidMidiInputDevices() { return getJuceAndroidMidiDevices (MidiDeviceInfo.PortInfo.TYPE_INPUT); } public String[] getJuceAndroidMidiOutputDevices() { return getJuceAndroidMidiDevices (MidiDeviceInfo.PortInfo.TYPE_OUTPUT); } private String[] getJuceAndroidMidiDevices (int portType) { ArrayList listOfReturnedDevices = new ArrayList(); List deviceNames = new ArrayList(); for (PhysicalMidiDevice physicalMidiDevice : physicalMidiDevices) { int portIdx = 0; MidiDeviceInfo.PortInfo[] ports = physicalMidiDevice.getPorts(); for (MidiDeviceInfo.PortInfo port : ports) { if (port.getType() == portType) { MidiPortPath path = new MidiPortPath(); path.midiDevice = physicalMidiDevice; path.androidMidiPortID = port.getPortNumber(); path.portIndexToUseInName = ++portIdx; listOfReturnedDevices.add (path); deviceNames.add (physicalMidiDevice.getHumanReadableNameForPort (port, path.portIndexToUseInName)); } } } String[] deviceNamesArray = new String[deviceNames.size()]; if (portType == MidiDeviceInfo.PortInfo.TYPE_INPUT) { lastDevicesReturned.get().inPorts.clear(); lastDevicesReturned.get().inPorts.addAll (listOfReturnedDevices); } else { lastDevicesReturned.get().outPorts.clear(); lastDevicesReturned.get().outPorts.addAll (listOfReturnedDevices); } return deviceNames.toArray(deviceNamesArray); } public JuceMidiPort openMidiInputPortWithJuceIndex (int index, long host) { ArrayList lastDevices = lastDevicesReturned.get().inPorts; if (index >= lastDevices.size() || index < 0) return null; MidiPortPath path = lastDevices.get (index); return path.midiDevice.openPort (path.androidMidiPortID, true, host); } public JuceMidiPort openMidiOutputPortWithJuceIndex (int index) { ArrayList lastDevices = lastDevicesReturned.get().outPorts; if (index >= lastDevices.size() || index < 0) return null; MidiPortPath path = lastDevices.get (index); return path.midiDevice.openPort (path.androidMidiPortID, false, 0); } public String getInputPortNameForJuceIndex (int index) { ArrayList lastDevices = lastDevicesReturned.get().inPorts; if (index >= lastDevices.size() || index < 0) return ""; MidiPortPath path = lastDevices.get (index); return path.midiDevice.getHumanReadableNameForPort (MidiDeviceInfo.PortInfo.TYPE_INPUT, path.androidMidiPortID, path.portIndexToUseInName); } public String getOutputPortNameForJuceIndex (int index) { ArrayList lastDevices = lastDevicesReturned.get().outPorts; if (index >= lastDevices.size() || index < 0) return ""; MidiPortPath path = lastDevices.get (index); return path.midiDevice.getHumanReadableNameForPort (MidiDeviceInfo.PortInfo.TYPE_OUTPUT, path.androidMidiPortID, path.portIndexToUseInName); } public void onDeviceAdded (MidiDeviceInfo info) { PhysicalMidiDevice device = PhysicalMidiDevice.fromMidiDeviceInfo (info, manager); // Do not add bluetooth devices as they are already added by the native bluetooth dialog if (info.getType() != MidiDeviceInfo.TYPE_BLUETOOTH) physicalMidiDevices.add (device); } public void onDeviceRemoved (MidiDeviceInfo info) { for (int i = 0; i < physicalMidiDevices.size(); ++i) { if (physicalMidiDevices.get(i).info.getId() == info.getId()) { physicalMidiDevices.remove (i); return; } } // Don't assert here as this may be called again after a bluetooth device is unpaired } public void onDeviceStatusChanged (MidiDeviceStatus status) { } private ArrayList physicalMidiDevices; private MidiManager manager; } public MidiDeviceManager getAndroidMidiDeviceManager() { if (getSystemService (MIDI_SERVICE) == null) return null; synchronized (JuceAppActivity.class) { if (midiDeviceManager == null) midiDeviceManager = new MidiDeviceManager(); } return midiDeviceManager; } public BluetoothManager getAndroidBluetoothManager() { BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); if (adapter == null) return null; if (adapter.getBluetoothLeScanner() == null) return null; synchronized (JuceAppActivity.class) { if (bluetoothManager == null) bluetoothManager = new BluetoothManager(); } return bluetoothManager; }