|  |     //==============================================================================
    public class BluetoothManager extends ScanCallback
    {
        BluetoothManager()
        {
        }
        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 void startStopScan (boolean shouldStart)
        {
            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;
            }
            if (shouldStart)
            {
                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);
                bluetoothLeScanner.startScan (Arrays.asList (scanFilterBuilder.build()),
                                              scanSettingsBuilder.build(),
                                              this);
            }
            else
            {
                bluetoothLeScanner.stopScan (this);
            }
        }
        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<ScanResult> 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<String> bluetoothMidiDevices = new HashSet<String>();
    }
    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<MidiPortID, JuceMidiPort>();
            handle = null;
        }
        public MidiDeviceInfo.PortInfo[] getPorts()
        {
            return info.getPorts();
        }
        public String getHumanReadableNameForPort (MidiDeviceInfo.PortInfo port, int portIndexToUseInName, boolean addPortNumberToName)
        {
            if (addPortNumberToName)
            {
                String portName = port.getName();
                if (portName.equals(""))
                    portName = ((port.getType() == MidiDeviceInfo.PortInfo.TYPE_OUTPUT) ? "Out " : "In ")
                            + Integer.toString(portIndexToUseInName);
                return getHumanReadableDeviceName() + " " + portName;
            }
            return getHumanReadableDeviceName();
        }
        public String getHumanReadableNameForPort (int portType, int androidPortID, int portIndexToUseInName)
        {
            MidiDeviceInfo.PortInfo[] ports = info.getPorts();
            int numTotalPorts = 0;
            for (MidiDeviceInfo.PortInfo port : ports)
            {
                if (port.getType() == portType)
                {
                    numTotalPorts++;
                }
            }
            for (MidiDeviceInfo.PortInfo port : ports)
            {
                if (port.getType() == portType)
                {
                    if (port.getPortNumber() == androidPortID)
                        return getHumanReadableNameForPort (port, portIndexToUseInName, (numTotalPorts > 1));
                }
            }
            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<MidiPortID, JuceMidiPort> 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<MidiPortPath> inPorts = new ArrayList<MidiPortPath>();
            public ArrayList<MidiPortPath> outPorts = new ArrayList<MidiPortPath>();
        }
        // We need to keep a thread local copy of the devices
        // which we returned the last time
        // getJuceAndroidMidiIn/OutputDevices() was called
        private final ThreadLocal<JuceDeviceList> lastDevicesReturned =
            new ThreadLocal<JuceDeviceList>()
            {
                @Override protected JuceDeviceList initialValue()
                {
                    return new JuceDeviceList();
                }
            };
        public MidiDeviceManager()
        {
            physicalMidiDevices = new ArrayList<PhysicalMidiDevice>();
            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<MidiPortPath> listOfReturnedDevices = new ArrayList<MidiPortPath>();
            List<String> deviceNames = new ArrayList<String>();
            for (PhysicalMidiDevice physicalMidiDevice : physicalMidiDevices)
            {
                int portIdx = 0;
                MidiDeviceInfo.PortInfo[] ports = physicalMidiDevice.getPorts();
                int numberOfPorts = 0;
                for (MidiDeviceInfo.PortInfo port : ports)
                {
                    if (port.getType() == portType)
                    {
                        numberOfPorts++;
                    }
                }
                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,
                                                                                         (numberOfPorts > 1)));
                    }
                }
            }
            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<MidiPortPath> 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<MidiPortPath> 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<MidiPortPath> 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<MidiPortPath> 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<PhysicalMidiDevice> 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;
    }
 |