From e0a6ca64fac5bd4f10139321604031816e90adb4 Mon Sep 17 00:00:00 2001 From: Mike Lockwood Date: Thu, 4 Jun 2015 13:43:56 -0700 Subject: MidiManager: proxy all requests to open devices through MidiService Previously when a MidiManager client opened a virtual or Bluetooth device, the client bound directly to the virtual device's MidiDeviceService or BluetoothMidiDevice's IMidiDeviceServer for the given BluetoothDevice. Only USB devices were opened in MidiService. Now opening any type of MIDI device is done via IMidiManager.openDevice() or IMidiManager.openBluetoothDevice(). MidiService tracks all connnections between clients and devices. Services that implement virtual devices must now require android.permission.BIND_MIDI_DEVICE_SERVICE so only MidiService can bind to these services. Bug: 21044677 Change-Id: I7172f7b1e0cbfe4a2a87dff376c32dc9b41aa563 --- Android.mk | 1 + api/current.txt | 2 + api/system-current.txt | 2 + core/java/android/app/SystemServiceRegistry.java | 2 +- core/res/AndroidManifest.xml | 5 + .../media/midi/IMidiDeviceOpenCallback.aidl | 25 ++ .../java/android/media/midi/IMidiDeviceServer.aidl | 1 + media/java/android/media/midi/IMidiManager.aidl | 15 +- media/java/android/media/midi/MidiDevice.java | 27 +- .../java/android/media/midi/MidiDeviceServer.java | 16 +- .../java/android/media/midi/MidiDeviceService.java | 11 + media/java/android/media/midi/MidiManager.java | 118 ++---- media/java/android/media/midi/package.html | 6 +- .../BluetoothMidiService/AndroidManifest.xml | 3 +- .../bluetoothmidiservice/BluetoothMidiDevice.java | 26 +- .../java/com/android/server/midi/MidiService.java | 434 ++++++++++++++++++--- 16 files changed, 520 insertions(+), 174 deletions(-) create mode 100644 media/java/android/media/midi/IMidiDeviceOpenCallback.aidl diff --git a/Android.mk b/Android.mk index d6dac53..047254f 100644 --- a/Android.mk +++ b/Android.mk @@ -344,6 +344,7 @@ LOCAL_SRC_FILES += \ media/java/android/media/IVolumeController.aidl \ media/java/android/media/audiopolicy/IAudioPolicyCallback.aidl \ media/java/android/media/midi/IMidiDeviceListener.aidl \ + media/java/android/media/midi/IMidiDeviceOpenCallback.aidl \ media/java/android/media/midi/IMidiDeviceServer.aidl \ media/java/android/media/midi/IMidiManager.aidl \ media/java/android/media/projection/IMediaProjection.aidl \ diff --git a/api/current.txt b/api/current.txt index 31f4cff..4de7ed1 100644 --- a/api/current.txt +++ b/api/current.txt @@ -27,6 +27,7 @@ package android { field public static final java.lang.String BIND_DREAM_SERVICE = "android.permission.BIND_DREAM_SERVICE"; field public static final java.lang.String BIND_INCALL_SERVICE = "android.permission.BIND_INCALL_SERVICE"; field public static final java.lang.String BIND_INPUT_METHOD = "android.permission.BIND_INPUT_METHOD"; + field public static final java.lang.String BIND_MIDI_DEVICE_SERVICE = "android.permission.BIND_MIDI_DEVICE_SERVICE"; field public static final java.lang.String BIND_NFC_SERVICE = "android.permission.BIND_NFC_SERVICE"; field public static final java.lang.String BIND_NOTIFICATION_LISTENER_SERVICE = "android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"; field public static final java.lang.String BIND_PRINT_SERVICE = "android.permission.BIND_PRINT_SERVICE"; @@ -17312,6 +17313,7 @@ package android.media.midi { method public final android.media.midi.MidiDeviceInfo getDeviceInfo(); method public final android.media.midi.MidiReceiver[] getOutputPortReceivers(); method public android.os.IBinder onBind(android.content.Intent); + method public void onClose(); method public void onDeviceStatusChanged(android.media.midi.MidiDeviceStatus); method public abstract android.media.midi.MidiReceiver[] onGetInputPortReceivers(); field public static final java.lang.String SERVICE_INTERFACE = "android.media.midi.MidiDeviceService"; diff --git a/api/system-current.txt b/api/system-current.txt index 187edc0..38d4c08 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -39,6 +39,7 @@ package android { field public static final java.lang.String BIND_INCALL_SERVICE = "android.permission.BIND_INCALL_SERVICE"; field public static final java.lang.String BIND_INPUT_METHOD = "android.permission.BIND_INPUT_METHOD"; field public static final java.lang.String BIND_KEYGUARD_APPWIDGET = "android.permission.BIND_KEYGUARD_APPWIDGET"; + field public static final java.lang.String BIND_MIDI_DEVICE_SERVICE = "android.permission.BIND_MIDI_DEVICE_SERVICE"; field public static final java.lang.String BIND_NFC_SERVICE = "android.permission.BIND_NFC_SERVICE"; field public static final java.lang.String BIND_NOTIFICATION_LISTENER_SERVICE = "android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"; field public static final java.lang.String BIND_PRINT_SERVICE = "android.permission.BIND_PRINT_SERVICE"; @@ -18618,6 +18619,7 @@ package android.media.midi { method public final android.media.midi.MidiDeviceInfo getDeviceInfo(); method public final android.media.midi.MidiReceiver[] getOutputPortReceivers(); method public android.os.IBinder onBind(android.content.Intent); + method public void onClose(); method public void onDeviceStatusChanged(android.media.midi.MidiDeviceStatus); method public abstract android.media.midi.MidiReceiver[] onGetInputPortReceivers(); field public static final java.lang.String SERVICE_INTERFACE = "android.media.midi.MidiDeviceService"; diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index 0d00908..10e8a53 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -691,7 +691,7 @@ final class SystemServiceRegistry { @Override public MidiManager createService(ContextImpl ctx) { IBinder b = ServiceManager.getService(Context.MIDI_SERVICE); - return new MidiManager(ctx, IMidiManager.Stub.asInterface(b)); + return new MidiManager(IMidiManager.Stub.asInterface(b)); }}); registerService(Context.RADIO_SERVICE, RadioManager.class, diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index ba91bce..3bced5a 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -1828,6 +1828,11 @@ + + +

An app can provide a MIDI Service that can be used by other apps. For example, -an app can provide a custom synthesizer that other apps can send messages to.

+an app can provide a custom synthesizer that other apps can send messages to. +The service must be guarded with permission "android.permission.BIND_MIDI_DEVICE_SERVICE".

Manifest Files

@@ -250,7 +251,8 @@ an app can provide a custom synthesizer that other apps can send messages to.
-<service android:name="MySynthDeviceService">
+<service android:name="MySynthDeviceService"
+  android:permission="android.permission.BIND_MIDI_DEVICE_SERVICE">
   <intent-filter>
     <action android:name="android.media.midi.MidiDeviceService" />
   </intent-filter>
diff --git a/media/packages/BluetoothMidiService/AndroidManifest.xml b/media/packages/BluetoothMidiService/AndroidManifest.xml
index 15aa581..b0b389a 100644
--- a/media/packages/BluetoothMidiService/AndroidManifest.xml
+++ b/media/packages/BluetoothMidiService/AndroidManifest.xml
@@ -8,7 +8,8 @@
 
     
-        
+        
             
                 
             
diff --git a/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiDevice.java b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiDevice.java
index 60c6570..e6d59e4 100644
--- a/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiDevice.java
+++ b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiDevice.java
@@ -24,10 +24,11 @@ import android.bluetooth.BluetoothGattDescriptor;
 import android.bluetooth.BluetoothGattService;
 import android.bluetooth.BluetoothProfile;
 import android.content.Context;
-import android.media.midi.MidiReceiver;
-import android.media.midi.MidiManager;
-import android.media.midi.MidiDeviceServer;
 import android.media.midi.MidiDeviceInfo;
+import android.media.midi.MidiDeviceServer;
+import android.media.midi.MidiDeviceStatus;
+import android.media.midi.MidiManager;
+import android.media.midi.MidiReceiver;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.util.Log;
@@ -81,6 +82,18 @@ public final class BluetoothMidiDevice {
     private final BluetoothPacketDecoder mPacketDecoder
             = new BluetoothPacketDecoder(MAX_PACKET_SIZE);
 
+    private final MidiDeviceServer.Callback mDeviceServerCallback
+            = new MidiDeviceServer.Callback() {
+        @Override
+        public void onDeviceStatusChanged(MidiDeviceServer server, MidiDeviceStatus status) {
+        }
+
+        @Override
+        public void onClose() {
+            close();
+        }
+    };
+
     private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
         @Override
         public void onConnectionStateChange(BluetoothGatt gatt, int status,
@@ -213,7 +226,7 @@ public final class BluetoothMidiDevice {
         inputPortReceivers[0] = mEventScheduler.getReceiver();
 
         mDeviceServer = mMidiManager.createDeviceServer(inputPortReceivers, 1,
-                null, null, properties, MidiDeviceInfo.TYPE_BLUETOOTH, null);
+                null, null, properties, MidiDeviceInfo.TYPE_BLUETOOTH, mDeviceServerCallback);
 
         mOutputReceiver = mDeviceServer.getOutputPortReceivers()[0];
 
@@ -248,11 +261,12 @@ public final class BluetoothMidiDevice {
 
     private void close() {
         synchronized (mBluetoothDevice) {
-        mEventScheduler.close();
+            mEventScheduler.close();
+            mService.deviceClosed(mBluetoothDevice);
+
             if (mDeviceServer != null) {
                 IoUtils.closeQuietly(mDeviceServer);
                 mDeviceServer = null;
-                mService.deviceClosed(mBluetoothDevice);
             }
             if (mBluetoothGatt != null) {
                 mBluetoothGatt.close();
diff --git a/services/midi/java/com/android/server/midi/MidiService.java b/services/midi/java/com/android/server/midi/MidiService.java
index 370f125..d1bbbfc 100644
--- a/services/midi/java/com/android/server/midi/MidiService.java
+++ b/services/midi/java/com/android/server/midi/MidiService.java
@@ -16,8 +16,11 @@
 
 package com.android.server.midi;
 
+import android.bluetooth.BluetoothDevice;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.ServiceConnection;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
@@ -25,11 +28,13 @@ import android.content.pm.ResolveInfo;
 import android.content.pm.ServiceInfo;
 import android.content.res.XmlResourceParser;
 import android.media.midi.IMidiDeviceListener;
+import android.media.midi.IMidiDeviceOpenCallback;
 import android.media.midi.IMidiDeviceServer;
 import android.media.midi.IMidiManager;
 import android.media.midi.MidiDeviceInfo;
 import android.media.midi.MidiDeviceService;
 import android.media.midi.MidiDeviceStatus;
+import android.media.midi.MidiManager;
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.IBinder;
@@ -78,6 +83,10 @@ public class MidiService extends IMidiManager.Stub {
     private final HashMap mDevicesByInfo
             = new HashMap();
 
+    // list of all Bluetooth devices, keyed by BluetoothDevice
+     private final HashMap mBluetoothDevices
+            = new HashMap();
+
     // list of all devices, keyed by IMidiDeviceServer
     private final HashMap mDevicesByServer = new HashMap();
 
@@ -86,6 +95,9 @@ public class MidiService extends IMidiManager.Stub {
 
     private final PackageManager mPackageManager;
 
+    // UID of BluetoothMidiService
+    private final int mBluetoothServiceUid;
+
     // PackageMonitor for listening to package changes
     private final PackageMonitor mPackageMonitor = new PackageMonitor() {
         @Override
@@ -115,6 +127,9 @@ public class MidiService extends IMidiManager.Stub {
         // List of all receivers for this client
         private final ArrayList mListeners
                 = new ArrayList();
+        // List of all device connections for this client
+        private final HashMap mDeviceConnections
+                = new HashMap();
 
         public Client(IBinder token) {
             mToken = token;
@@ -132,8 +147,33 @@ public class MidiService extends IMidiManager.Stub {
 
         public void removeListener(IMidiDeviceListener listener) {
             mListeners.remove(listener);
-            if (mListeners.size() == 0) {
-                removeClient(mToken);
+            if (mListeners.size() == 0 && mDeviceConnections.size() == 0) {
+                close();
+            }
+        }
+
+        public void addDeviceConnection(Device device, IMidiDeviceOpenCallback callback) {
+            DeviceConnection connection = new DeviceConnection(device, this, callback);
+            mDeviceConnections.put(connection.getToken(), connection);
+            device.addDeviceConnection(connection);
+        }
+
+        // called from MidiService.closeDevice()
+        public void removeDeviceConnection(IBinder token) {
+            DeviceConnection connection = mDeviceConnections.remove(token);
+            if (connection != null) {
+                connection.getDevice().removeDeviceConnection(connection);
+            }
+            if (mListeners.size() == 0 && mDeviceConnections.size() == 0) {
+                close();
+            }
+        }
+
+        // called from Device.close()
+        public void removeDeviceConnection(DeviceConnection connection) {
+            mDeviceConnections.remove(connection.getToken());
+            if (mListeners.size() == 0 && mDeviceConnections.size() == 0) {
+                close();
             }
         }
 
@@ -178,8 +218,21 @@ public class MidiService extends IMidiManager.Stub {
             }
         }
 
+        private void close() {
+            synchronized (mClients) {
+                mClients.remove(mToken);
+                mToken.unlinkToDeath(this, 0);
+            }
+
+            for (DeviceConnection connection : mDeviceConnections.values()) {
+                connection.getDevice().removeDeviceConnection(connection);
+            }
+        }
+
+        @Override
         public void binderDied() {
-            removeClient(mToken);
+            Log.d(TAG, "Client died: " + this);
+            close();
         }
 
         @Override
@@ -190,6 +243,12 @@ public class MidiService extends IMidiManager.Stub {
             sb.append(mPid);
             sb.append(" listener count: ");
             sb.append(mListeners.size());
+            sb.append(" Device Connections:");
+            for (DeviceConnection connection : mDeviceConnections.values()) {
+                sb.append(" ");
+            }
             return sb.toString();
         }
     }
@@ -211,57 +270,96 @@ public class MidiService extends IMidiManager.Stub {
         }
     }
 
-    private void removeClient(IBinder token) {
-        mClients.remove(token);
-    }
-
     private final class Device implements IBinder.DeathRecipient {
-        private final IMidiDeviceServer mServer;
-        private final MidiDeviceInfo mDeviceInfo;
+        private IMidiDeviceServer mServer;
+        private MidiDeviceInfo mDeviceInfo;
+        private final BluetoothDevice mBluetoothDevice;
         private MidiDeviceStatus mDeviceStatus;
-        private IBinder mDeviceStatusToken;
+
         // ServiceInfo for the device's MidiDeviceServer implementation (virtual devices only)
         private final ServiceInfo mServiceInfo;
         // UID of device implementation
         private final int mUid;
 
+        // ServiceConnection for implementing Service (virtual devices only)
+        // mServiceConnection is non-null when connected or attempting to connect to the service
+        private ServiceConnection mServiceConnection;
+
+        // List of all device connections for this device
+        private final ArrayList mDeviceConnections
+                = new ArrayList();
+
         public Device(IMidiDeviceServer server, MidiDeviceInfo deviceInfo,
                 ServiceInfo serviceInfo, int uid) {
-            mServer = server;
             mDeviceInfo = deviceInfo;
             mServiceInfo = serviceInfo;
             mUid = uid;
+            mBluetoothDevice = (BluetoothDevice)deviceInfo.getProperties().getParcelable(
+                    MidiDeviceInfo.PROPERTY_BLUETOOTH_DEVICE);;
+            setDeviceServer(server);
+        }
+
+        public Device(BluetoothDevice bluetoothDevice) {
+            mBluetoothDevice = bluetoothDevice;
+            mServiceInfo = null;
+            mUid = mBluetoothServiceUid;
+        }
+
+        private void setDeviceServer(IMidiDeviceServer server) {
+            if (server != null) {
+                if (mServer != null) {
+                    Log.e(TAG, "mServer already set in setDeviceServer");
+                    return;
+                }
+                IBinder binder = server.asBinder();
+                try {
+                    if (mDeviceInfo == null) {
+                        mDeviceInfo = server.getDeviceInfo();
+                    }
+                    binder.linkToDeath(this, 0);
+                    mServer = server;
+                } catch (RemoteException e) {
+                    mServer = null;
+                    return;
+                }
+                mDevicesByServer.put(binder, this);
+            } else if (mServer != null) {
+                server = mServer;
+                mServer = null;
+
+                IBinder binder = server.asBinder();
+                mDevicesByServer.remove(binder);
+
+                try {
+                    server.closeDevice();
+                    binder.unlinkToDeath(this, 0);
+                } catch (RemoteException e) {
+                    // nothing to do here
+                }
+            }
+
+            if (mDeviceConnections != null) {
+                for (DeviceConnection connection : mDeviceConnections) {
+                    connection.notifyClient(server);
+                }
+            }
         }
 
         public MidiDeviceInfo getDeviceInfo() {
             return mDeviceInfo;
         }
 
+        // only used for bluetooth devices, which are created before we have a MidiDeviceInfo
+        public void setDeviceInfo(MidiDeviceInfo deviceInfo) {
+            mDeviceInfo = deviceInfo;
+        }
+
         public MidiDeviceStatus getDeviceStatus() {
             return mDeviceStatus;
         }
 
-        public void setDeviceStatus(IBinder token, MidiDeviceStatus status) {
+        public void setDeviceStatus(MidiDeviceStatus status) {
             mDeviceStatus = status;
-
-            if (mDeviceStatusToken == null && token != null) {
-                // register a death recipient so we can clear the status when the device dies
-                try {
-                    token.linkToDeath(new IBinder.DeathRecipient() {
-                        @Override
-                        public void binderDied() {
-                            // reset to default status and clear the token
-                            mDeviceStatus = new MidiDeviceStatus(mDeviceInfo);
-                            mDeviceStatusToken = null;
-                            notifyDeviceStatusChanged(Device.this, mDeviceStatus);
-                        }
-                    }, 0);
-                    mDeviceStatusToken = token;
-                } catch (RemoteException e) {
-                    // reset to default status
-                    mDeviceStatus = new MidiDeviceStatus(mDeviceInfo);
-                }
-            }
         }
 
         public IMidiDeviceServer getDeviceServer() {
@@ -284,11 +382,102 @@ public class MidiService extends IMidiManager.Stub {
             return (!mDeviceInfo.isPrivate() || mUid == uid);
         }
 
+        public void addDeviceConnection(DeviceConnection connection) {
+            synchronized (mDeviceConnections) {
+                if (mServer != null) {
+                    mDeviceConnections.add(connection);
+                    connection.notifyClient(mServer);
+                } else if (mServiceConnection == null &&
+                    (mServiceInfo != null || mBluetoothDevice != null)) {
+                    mDeviceConnections.add(connection);
+
+                    mServiceConnection = new ServiceConnection() {
+                        @Override
+                        public void onServiceConnected(ComponentName name, IBinder service) {
+                            IMidiDeviceServer server = IMidiDeviceServer.Stub.asInterface(service);
+                            setDeviceServer(server);
+                        }
+
+                        @Override
+                        public void onServiceDisconnected(ComponentName name) {
+                            setDeviceServer(null);
+                            mServiceConnection = null;
+                        }
+                    };
+
+                    Intent intent;
+                    if (mBluetoothDevice != null) {
+                        intent = new Intent(MidiManager.BLUETOOTH_MIDI_SERVICE_INTENT);
+                        intent.setComponent(new ComponentName(
+                                MidiManager.BLUETOOTH_MIDI_SERVICE_PACKAGE,
+                                MidiManager.BLUETOOTH_MIDI_SERVICE_CLASS));
+                        intent.putExtra("device", mBluetoothDevice);
+                    } else {
+                        intent = new Intent(MidiDeviceService.SERVICE_INTERFACE);
+                        intent.setComponent(
+                                new ComponentName(mServiceInfo.packageName, mServiceInfo.name));
+                    }
+
+                    if (!mContext.bindService(intent, mServiceConnection,
+                            Context.BIND_AUTO_CREATE)) {
+                        Log.e(TAG, "Unable to bind service: " + intent);
+                        setDeviceServer(null);
+                        mServiceConnection = null;
+                    }
+                } else {
+                    Log.e(TAG, "No way to connect to device in addDeviceConnection");
+                    connection.notifyClient(null);
+                }
+            }
+        }
+
+        public void removeDeviceConnection(DeviceConnection connection) {
+            synchronized (mDeviceConnections) {
+                mDeviceConnections.remove(connection);
+
+                if (mDeviceConnections.size() == 0 && mServiceConnection != null) {
+                    mContext.unbindService(mServiceConnection);
+                    mServiceConnection = null;
+                    if (mBluetoothDevice != null) {
+                        // Bluetooth devices are ephemeral - remove when no clients exist
+                        synchronized (mDevicesByInfo) {
+                            closeLocked();
+                        }
+                    } else {
+                        setDeviceServer(null);
+                    }
+                }
+            }
+        }
+
+        // synchronize on mDevicesByInfo
+        public void closeLocked() {
+            synchronized (mDeviceConnections) {
+                for (DeviceConnection connection : mDeviceConnections) {
+                    connection.getClient().removeDeviceConnection(connection);
+                }
+                mDeviceConnections.clear();
+            }
+            setDeviceServer(null);
+
+            // closed virtual devices should not be removed from mDevicesByInfo
+            // since they can be restarted on demand
+            if (mServiceInfo == null) {
+                removeDeviceLocked(this);
+            } else {
+                mDeviceStatus = new MidiDeviceStatus(mDeviceInfo);
+            }
+
+            if (mBluetoothDevice != null) {
+                mBluetoothDevices.remove(mBluetoothDevice);
+            }
+        }
+
+        @Override
         public void binderDied() {
+            Log.d(TAG, "Device died: " + this);
             synchronized (mDevicesByInfo) {
-                if (mDevicesByInfo.remove(mDeviceInfo) != null) {
-                    removeDeviceLocked(this);
-                }
+                closeLocked();
             }
         }
 
@@ -300,10 +489,56 @@ public class MidiService extends IMidiManager.Stub {
             sb.append(mDeviceStatus);
             sb.append(" UID: ");
             sb.append(mUid);
+            sb.append(" DeviceConnection count: ");
+            sb.append(mDeviceConnections.size());
+            sb.append(" mServiceConnection: ");
+            sb.append(mServiceConnection);
             return sb.toString();
         }
     }
 
+    // Represents a connection between a client and a device
+    private final class DeviceConnection {
+        private final IBinder mToken = new Binder();
+        private final Device mDevice;
+        private final Client mClient;
+        private IMidiDeviceOpenCallback mCallback;
+
+        public DeviceConnection(Device device, Client client, IMidiDeviceOpenCallback callback) {
+            mDevice = device;
+            mClient = client;
+            mCallback = callback;
+        }
+
+        public Device getDevice() {
+            return mDevice;
+        }
+
+        public Client getClient() {
+            return mClient;
+        }
+
+        public IBinder getToken() {
+            return mToken;
+        }
+
+        public void notifyClient(IMidiDeviceServer deviceServer) {
+            if (mCallback != null) {
+                try {
+                    mCallback.onDeviceOpened(deviceServer, (deviceServer == null ? null : mToken));
+                } catch (RemoteException e) {
+                    // Client binderDied() method will do necessary cleanup, so nothing to do here
+                }
+                mCallback = null;
+            }
+        }
+
+        @Override
+        public String toString() {
+            return "DeviceConnection Device ID: " + mDevice.getDeviceInfo().getId();
+        }
+    }
+
     public MidiService(Context context) {
         mContext = context;
         mPackageManager = context.getPackageManager();
@@ -321,6 +556,18 @@ public class MidiService extends IMidiManager.Stub {
                 }
             }
         }
+
+        PackageInfo info;
+        try {
+            info = mPackageManager.getPackageInfo(MidiManager.BLUETOOTH_MIDI_SERVICE_PACKAGE, 0);
+        } catch (PackageManager.NameNotFoundException e) {
+            info = null;
+        }
+        if (info != null && info.applicationInfo != null) {
+            mBluetoothServiceUid = info.applicationInfo.uid;
+        } else {
+            mBluetoothServiceUid = -1;
+        }
    }
 
     @Override
@@ -355,18 +602,61 @@ public class MidiService extends IMidiManager.Stub {
     }
 
     @Override
-    public IMidiDeviceServer openDevice(IBinder token, MidiDeviceInfo deviceInfo) {
-        Device device = mDevicesByInfo.get(deviceInfo);
-        if (device == null) {
-            Log.e(TAG, "device not found in openDevice: " + deviceInfo);
-            return null;
+    public void openDevice(IBinder token, MidiDeviceInfo deviceInfo,
+            IMidiDeviceOpenCallback callback) {
+        Client client = getClient(token);
+        if (client == null) return;
+
+        Device device;
+        synchronized (mDevicesByInfo) {
+            device = mDevicesByInfo.get(deviceInfo);
+            if (device == null) {
+                throw new IllegalArgumentException("device does not exist: " + deviceInfo);
+            }
+            if (!device.isUidAllowed(Binder.getCallingUid())) {
+                throw new SecurityException("Attempt to open private device with wrong UID");
+            }
+        }
+
+        // clear calling identity so bindService does not fail
+        long identity = Binder.clearCallingIdentity();
+        try {
+            client.addDeviceConnection(device, callback);
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    @Override
+    public void openBluetoothDevice(IBinder token, BluetoothDevice bluetoothDevice,
+            IMidiDeviceOpenCallback callback) {
+        Client client = getClient(token);
+        if (client == null) return;
+
+        // Bluetooth devices are created on demand
+        Device device;
+        synchronized (mDevicesByInfo) {
+            device = mBluetoothDevices.get(bluetoothDevice);
+            if (device == null) {
+                device = new Device(bluetoothDevice);
+                mBluetoothDevices.put(bluetoothDevice, device);
+            }
         }
 
-        if (!device.isUidAllowed(Binder.getCallingUid())) {
-            throw new SecurityException("Attempt to open private device with wrong UID");
+        // clear calling identity so bindService does not fail
+        long identity = Binder.clearCallingIdentity();
+        try {
+            client.addDeviceConnection(device, callback);
+        } finally {
+            Binder.restoreCallingIdentity(identity);
         }
+    }
 
-        return device.getDeviceServer();
+    @Override
+    public void closeDevice(IBinder clientToken, IBinder deviceToken) {
+        Client client = getClient(clientToken);
+        if (client == null) return;
+        client.removeDeviceConnection(deviceToken);
     }
 
     @Override
@@ -376,6 +666,8 @@ public class MidiService extends IMidiManager.Stub {
         int uid = Binder.getCallingUid();
         if (type == MidiDeviceInfo.TYPE_USB && uid != Process.SYSTEM_UID) {
             throw new SecurityException("only system can create USB devices");
+        } else if (type == MidiDeviceInfo.TYPE_BLUETOOTH && uid != mBluetoothServiceUid) {
+            throw new SecurityException("only MidiBluetoothService can create Bluetooth devices");
         }
 
         synchronized (mDevicesByInfo) {
@@ -389,8 +681,7 @@ public class MidiService extends IMidiManager.Stub {
         synchronized (mDevicesByInfo) {
             Device device = mDevicesByServer.get(server.asBinder());
             if (device != null) {
-                mDevicesByInfo.remove(device.getDeviceInfo());
-                removeDeviceLocked(device);
+                device.closeLocked();
             }
         }
     }
@@ -420,19 +711,16 @@ public class MidiService extends IMidiManager.Stub {
     }
 
     @Override
-    public void setDeviceStatus(IBinder token, MidiDeviceStatus status) {
-        MidiDeviceInfo deviceInfo = status.getDeviceInfo();
-        Device device = mDevicesByInfo.get(deviceInfo);
-        if (device == null) {
-            // Just return quietly here if device no longer exists
-            return;
-        }
-        if (Binder.getCallingUid() != device.getUid()) {
-            throw new SecurityException("setDeviceStatus() caller UID " + Binder.getCallingUid()
-                    + " does not match device's UID " + device.getUid());
+    public void setDeviceStatus(IMidiDeviceServer server, MidiDeviceStatus status) {
+        Device device = mDevicesByServer.get(server.asBinder());
+        if (device != null) {
+            if (Binder.getCallingUid() != device.getUid()) {
+                throw new SecurityException("setDeviceStatus() caller UID " + Binder.getCallingUid()
+                        + " does not match device's UID " + device.getUid());
+            }
+            device.setDeviceStatus(status);
+            notifyDeviceStatusChanged(device, status);
         }
-        device.setDeviceStatus(token, status);
-        notifyDeviceStatusChanged(device, status);
     }
 
     private void notifyDeviceStatusChanged(Device device, MidiDeviceStatus status) {
@@ -452,18 +740,24 @@ public class MidiService extends IMidiManager.Stub {
         int id = mNextDeviceId++;
         MidiDeviceInfo deviceInfo = new MidiDeviceInfo(type, id, numInputPorts, numOutputPorts,
                 inputPortNames, outputPortNames, properties, isPrivate);
-        Device device = new Device(server, deviceInfo, serviceInfo, uid);
 
-        if (server != null) {
-            IBinder binder = server.asBinder();
-            try {
-                binder.linkToDeath(device, 0);
-            } catch (RemoteException e) {
-                return null;
+        Device device = null;
+        BluetoothDevice bluetoothDevice = null;
+        if (type == MidiDeviceInfo.TYPE_BLUETOOTH) {
+            bluetoothDevice = (BluetoothDevice)properties.getParcelable(
+                    MidiDeviceInfo.PROPERTY_BLUETOOTH_DEVICE);
+            device = mBluetoothDevices.get(bluetoothDevice);
+            if (device != null) {
+                device.setDeviceInfo(deviceInfo);
             }
-            mDevicesByServer.put(binder, device);
+        }
+        if (device == null) {
+            device = new Device(server, deviceInfo, serviceInfo, uid);
         }
         mDevicesByInfo.put(deviceInfo, device);
+        if (bluetoothDevice != null) {
+            mBluetoothDevices.put(bluetoothDevice, device);
+        }
 
         synchronized (mClients) {
             for (Client c : mClients.values()) {
@@ -478,8 +772,9 @@ public class MidiService extends IMidiManager.Stub {
     private void removeDeviceLocked(Device device) {
         IMidiDeviceServer server = device.getDeviceServer();
         if (server != null) {
-            mDevicesByServer.remove(server);
+            mDevicesByServer.remove(server.asBinder());
         }
+        mDevicesByInfo.remove(device.getDeviceInfo());
 
         synchronized (mClients) {
             for (Client c : mClients.values()) {
@@ -516,6 +811,15 @@ public class MidiService extends IMidiManager.Stub {
                     MidiDeviceService.SERVICE_INTERFACE);
             if (parser == null) return;
 
+            // ignore virtual device servers that do not require the correct permission
+            if (!android.Manifest.permission.BIND_MIDI_DEVICE_SERVICE.equals(
+                    serviceInfo.permission)) {
+                Log.w(TAG, "Skipping MIDI device service " + serviceInfo.packageName
+                        + ": it does not require the permission "
+                        + android.Manifest.permission.BIND_MIDI_DEVICE_SERVICE);
+                return;
+            }
+
             Bundle properties = null;
             int numInputPorts = 0;
             int numOutputPorts = 0;
-- 
cgit v1.1