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
diff --git a/Android.mk b/Android.mk
index d6dac53..047254f 100644
--- a/Android.mk
+++ b/Android.mk
@@ -344,6 +344,7 @@
 	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 @@
     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 @@
     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 @@
     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 @@
     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 @@
             @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 @@
     <permission android:name="android.permission.BIND_INPUT_METHOD"
         android:protectionLevel="signature" />
 
+    <!-- Must be required by an {@link android.media.midi.MidiDeviceService},
+         to ensure that only the system can bind to it. -->
+    <permission android:name="android.permission.BIND_MIDI_DEVICE_SERVICE"
+        android:protectionLevel="signature" />
+
     <!-- Must be required by an {@link android.accessibilityservice.AccessibilityService},
          to ensure that only the system can bind to it. -->
     <permission android:name="android.permission.BIND_ACCESSIBILITY_SERVICE"
diff --git a/media/java/android/media/midi/IMidiDeviceOpenCallback.aidl b/media/java/android/media/midi/IMidiDeviceOpenCallback.aidl
new file mode 100644
index 0000000..6e3dbbf
--- /dev/null
+++ b/media/java/android/media/midi/IMidiDeviceOpenCallback.aidl
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.midi;
+
+import android.media.midi.IMidiDeviceServer;
+
+/** @hide */
+oneway interface IMidiDeviceOpenCallback
+{
+    void onDeviceOpened(in IMidiDeviceServer server, IBinder token);
+}
diff --git a/media/java/android/media/midi/IMidiDeviceServer.aidl b/media/java/android/media/midi/IMidiDeviceServer.aidl
index 96d12fd..e30796d 100644
--- a/media/java/android/media/midi/IMidiDeviceServer.aidl
+++ b/media/java/android/media/midi/IMidiDeviceServer.aidl
@@ -25,6 +25,7 @@
     ParcelFileDescriptor openInputPort(IBinder token, int portNumber);
     ParcelFileDescriptor openOutputPort(IBinder token, int portNumber);
     void closePort(IBinder token);
+    void closeDevice();
 
     // connects the input port pfd to the specified output port
     void connectPorts(IBinder token, in ParcelFileDescriptor pfd, int outputPortNumber);
diff --git a/media/java/android/media/midi/IMidiManager.aidl b/media/java/android/media/midi/IMidiManager.aidl
index fcd4aff..d3c8e0a 100644
--- a/media/java/android/media/midi/IMidiManager.aidl
+++ b/media/java/android/media/midi/IMidiManager.aidl
@@ -16,7 +16,10 @@
 
 package android.media.midi;
 
+import android.bluetooth.BluetoothDevice;
 import android.media.midi.IMidiDeviceListener;
+import android.media.midi.IMidiDeviceOpenCallback;
+import android.media.midi.IMidiDeviceServer;
 import android.media.midi.IMidiDeviceServer;
 import android.media.midi.MidiDeviceInfo;
 import android.media.midi.MidiDeviceStatus;
@@ -29,11 +32,13 @@
     MidiDeviceInfo[] getDevices();
 
     // for device creation & removal notifications
-    void registerListener(IBinder token, in IMidiDeviceListener listener);
-    void unregisterListener(IBinder token, in IMidiDeviceListener listener);
+    void registerListener(IBinder clientToken, in IMidiDeviceListener listener);
+    void unregisterListener(IBinder clientToken, in IMidiDeviceListener listener);
 
-    // for opening built-in MIDI devices
-    IMidiDeviceServer openDevice(IBinder token, in MidiDeviceInfo device);
+    void openDevice(IBinder clientToken, in MidiDeviceInfo device, in IMidiDeviceOpenCallback callback);
+    void openBluetoothDevice(IBinder clientToken, in BluetoothDevice bluetoothDevice,
+            in IMidiDeviceOpenCallback callback);
+    void closeDevice(IBinder clientToken, IBinder deviceToken);
 
     // for registering built-in MIDI devices
     MidiDeviceInfo registerDeviceServer(in IMidiDeviceServer server, int numInputPorts,
@@ -52,5 +57,5 @@
 
     // used by MIDI devices to report their status
     // the token is used by MidiService for death notification
-    void setDeviceStatus(IBinder token, in MidiDeviceStatus status);
+    void setDeviceStatus(in IMidiDeviceServer server, in MidiDeviceStatus status);
 }
diff --git a/media/java/android/media/midi/MidiDevice.java b/media/java/android/media/midi/MidiDevice.java
index 6526adc..7998a92 100644
--- a/media/java/android/media/midi/MidiDevice.java
+++ b/media/java/android/media/midi/MidiDevice.java
@@ -16,8 +16,6 @@
 
 package android.media.midi;
 
-import android.content.Context;
-import android.content.ServiceConnection;
 import android.os.Binder;
 import android.os.IBinder;
 import android.os.ParcelFileDescriptor;
@@ -40,9 +38,9 @@
 
     private final MidiDeviceInfo mDeviceInfo;
     private final IMidiDeviceServer mDeviceServer;
-    private Context mContext;
-    private ServiceConnection mServiceConnection;
-
+    private final IMidiManager mMidiManager;
+    private final IBinder mClientToken;
+    private final IBinder mDeviceToken;
 
     private final CloseGuard mGuard = CloseGuard.get();
 
@@ -71,16 +69,13 @@
         }
     }
 
-    /* package */ MidiDevice(MidiDeviceInfo deviceInfo, IMidiDeviceServer server) {
-        this(deviceInfo, server, null, null);
-    }
-
     /* package */ MidiDevice(MidiDeviceInfo deviceInfo, IMidiDeviceServer server,
-            Context context, ServiceConnection serviceConnection) {
+            IMidiManager midiManager, IBinder clientToken, IBinder deviceToken) {
         mDeviceInfo = deviceInfo;
         mDeviceServer = server;
-        mContext = context;
-        mServiceConnection = serviceConnection;
+        mMidiManager = midiManager;
+        mClientToken = clientToken;
+        mDeviceToken = deviceToken;
         mGuard.open("close");
     }
 
@@ -170,10 +165,10 @@
     public void close() throws IOException {
         synchronized (mGuard) {
             mGuard.close();
-            if (mContext != null && mServiceConnection != null) {
-                mContext.unbindService(mServiceConnection);
-                mContext = null;
-                mServiceConnection = null;
+            try {
+                mMidiManager.closeDevice(mClientToken, mDeviceToken);
+            } catch (RemoteException e) {
+                Log.e(TAG, "RemoteException in closeDevice");
             }
         }
     }
diff --git a/media/java/android/media/midi/MidiDeviceServer.java b/media/java/android/media/midi/MidiDeviceServer.java
index a316a44..1b56e1c 100644
--- a/media/java/android/media/midi/MidiDeviceServer.java
+++ b/media/java/android/media/midi/MidiDeviceServer.java
@@ -65,7 +65,6 @@
 
 
     // for reporting device status
-    private final IBinder mDeviceStatusToken = new Binder();
     private final boolean[] mInputPortOpen;
     private final int[] mOutputPortOpenCount;
 
@@ -81,6 +80,11 @@
          * @param status the {@link MidiDeviceStatus} for the device
          */
         public void onDeviceStatusChanged(MidiDeviceServer server, MidiDeviceStatus status);
+
+        /**
+         * Called to notify when the device is closed
+         */
+        public void onClose();
     }
 
     abstract private class PortClient implements IBinder.DeathRecipient {
@@ -242,6 +246,14 @@
         }
 
         @Override
+        public void closeDevice() {
+            if (mCallback != null) {
+                mCallback.onClose();
+            }
+            IoUtils.closeQuietly(MidiDeviceServer.this);
+        }
+
+        @Override
         public void connectPorts(IBinder token, ParcelFileDescriptor pfd,
                 int outputPortNumber) {
             MidiInputPort inputPort = new MidiInputPort(pfd, outputPortNumber);
@@ -305,7 +317,7 @@
             mCallback.onDeviceStatusChanged(this, status);
         }
         try {
-            mMidiManager.setDeviceStatus(mDeviceStatusToken, status);
+            mMidiManager.setDeviceStatus(mServer, status);
         } catch (RemoteException e) {
             Log.e(TAG, "RemoteException in updateDeviceStatus");
         } finally {
diff --git a/media/java/android/media/midi/MidiDeviceService.java b/media/java/android/media/midi/MidiDeviceService.java
index ce12a4f..d897ad2 100644
--- a/media/java/android/media/midi/MidiDeviceService.java
+++ b/media/java/android/media/midi/MidiDeviceService.java
@@ -59,6 +59,11 @@
         public void onDeviceStatusChanged(MidiDeviceServer server, MidiDeviceStatus status) {
             MidiDeviceService.this.onDeviceStatusChanged(status);
         }
+
+        @Override
+        public void onClose() {
+            MidiDeviceService.this.onClose();
+        }
     };
 
     @Override
@@ -125,6 +130,12 @@
     public void onDeviceStatusChanged(MidiDeviceStatus status) {
     }
 
+    /**
+     * Called to notify when our device has been closed by all its clients
+     */
+    public void onClose() {
+    }
+
     @Override
     public IBinder onBind(Intent intent) {
         if (SERVICE_INTERFACE.equals(intent.getAction()) && mServer != null) {
diff --git a/media/java/android/media/midi/MidiManager.java b/media/java/android/media/midi/MidiManager.java
index d19cf36..0beb9a4 100644
--- a/media/java/android/media/midi/MidiManager.java
+++ b/media/java/android/media/midi/MidiManager.java
@@ -17,11 +17,6 @@
 package android.media.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.ServiceInfo;
 import android.os.Binder;
 import android.os.IBinder;
 import android.os.Bundle;
@@ -52,16 +47,17 @@
 
     /**
      * BluetoothMidiService package name
+     * @hide
      */
-    private static final String BLUETOOTH_MIDI_SERVICE_PACKAGE = "com.android.bluetoothmidiservice";
+    public static final String BLUETOOTH_MIDI_SERVICE_PACKAGE = "com.android.bluetoothmidiservice";
 
     /**
      * BluetoothMidiService class name
+     * @hide
      */
-    private static final String BLUETOOTH_MIDI_SERVICE_CLASS =
+    public static final String BLUETOOTH_MIDI_SERVICE_CLASS =
                 "com.android.bluetoothmidiservice.BluetoothMidiService";
 
-    private final Context mContext;
     private final IMidiManager mService;
     private final IBinder mToken = new Binder();
 
@@ -166,8 +162,7 @@
     /**
      * @hide
      */
-    public MidiManager(Context context, IMidiManager service) {
-        mContext = context;
+    public MidiManager(IMidiManager service) {
         mService = service;
     }
 
@@ -237,7 +232,7 @@
      * Opens a MIDI device for reading and writing.
      *
      * @param deviceInfo a {@link android.media.midi.MidiDeviceInfo} to open
-     * @param listener a {@link MidiManager.OnDeviceOpenedListener} to be called 
+     * @param listener a {@link MidiManager.OnDeviceOpenedListener} to be called
      *                 to receive the result
      * @param handler the {@link android.os.Handler Handler} that will be used for delivering
      *                the result. If handler is null, then the thread used for the
@@ -245,52 +240,28 @@
      */
     public void openDevice(MidiDeviceInfo deviceInfo, OnDeviceOpenedListener listener,
             Handler handler) {
-        MidiDevice device = null;
-        try {
-            IMidiDeviceServer server = mService.openDevice(mToken, deviceInfo);
-            if (server == null) {
-                ServiceInfo serviceInfo = (ServiceInfo)deviceInfo.getProperties().getParcelable(
-                        MidiDeviceInfo.PROPERTY_SERVICE_INFO);
-                if (serviceInfo == null) {
-                    Log.e(TAG, "no ServiceInfo for " + deviceInfo);
-                } else {
-                    Intent intent = new Intent(MidiDeviceService.SERVICE_INTERFACE);
-                    intent.setComponent(new ComponentName(serviceInfo.packageName,
-                            serviceInfo.name));
-                    final MidiDeviceInfo deviceInfoF = deviceInfo;
-                    final OnDeviceOpenedListener listenerF = listener;
-                    final Handler handlerF = handler;
-                    if (mContext.bindService(intent,
-                        new ServiceConnection() {
-                            @Override
-                            public void onServiceConnected(ComponentName name, IBinder binder) {
-                                IMidiDeviceServer server =
-                                        IMidiDeviceServer.Stub.asInterface(binder);
-                                MidiDevice device = new MidiDevice(deviceInfoF, server, mContext,
-                                        this);
-                                sendOpenDeviceResponse(device, listenerF, handlerF);
-                            }
+        final MidiDeviceInfo deviceInfoF = deviceInfo;
+        final OnDeviceOpenedListener listenerF = listener;
+        final Handler handlerF = handler;
 
-                            @Override
-                            public void onServiceDisconnected(ComponentName name) {
-                                // FIXME - anything to do here?
-                            }
-                        },
-                        Context.BIND_AUTO_CREATE))
-                    {
-                        // return immediately to avoid calling sendOpenDeviceResponse below
-                        return;
-                    } else {
-                        Log.e(TAG, "Unable to bind service: " + intent);
-                    }
+        IMidiDeviceOpenCallback callback = new IMidiDeviceOpenCallback.Stub() {
+            @Override
+            public void onDeviceOpened(IMidiDeviceServer server, IBinder deviceToken) {
+                MidiDevice device;
+                if (server != null) {
+                    device = new MidiDevice(deviceInfoF, server, mService, mToken, deviceToken);
+                } else {
+                    device = null;
                 }
-            } else {
-                device = new MidiDevice(deviceInfo, server);
+                sendOpenDeviceResponse(device, listenerF, handlerF);
             }
+        };
+
+        try {
+            mService.openDevice(mToken, deviceInfo, callback);
         } catch (RemoteException e) {
             Log.e(TAG, "RemoteException in openDevice");
         }
-        sendOpenDeviceResponse(device, listener, handler);
     }
 
     /**
@@ -303,38 +274,33 @@
      *                the result. If handler is null, then the thread used for the
      *                listener is unspecified.
      */
-    public void openBluetoothDevice(final BluetoothDevice bluetoothDevice,
-            final OnDeviceOpenedListener listener, final Handler handler) {
-        Intent intent = new Intent(BLUETOOTH_MIDI_SERVICE_INTENT);
-        intent.setComponent(new ComponentName(BLUETOOTH_MIDI_SERVICE_PACKAGE,
-                BLUETOOTH_MIDI_SERVICE_CLASS));
-        intent.putExtra("device", bluetoothDevice);
-        if (!mContext.bindService(intent,
-            new ServiceConnection() {
-                @Override
-                public void onServiceConnected(ComponentName name, IBinder binder) {
-                    IMidiDeviceServer server =
-                            IMidiDeviceServer.Stub.asInterface(binder);
+    public void openBluetoothDevice(BluetoothDevice bluetoothDevice,
+            OnDeviceOpenedListener listener, Handler handler) {
+        final OnDeviceOpenedListener listenerF = listener;
+        final Handler handlerF = handler;
+
+        IMidiDeviceOpenCallback callback = new IMidiDeviceOpenCallback.Stub() {
+            @Override
+            public void onDeviceOpened(IMidiDeviceServer server, IBinder deviceToken) {
+                MidiDevice device = null;
+                if (server != null) {
                     try {
                         // fetch MidiDeviceInfo from the server
                         MidiDeviceInfo deviceInfo = server.getDeviceInfo();
-                        MidiDevice device = new MidiDevice(deviceInfo, server, mContext, this);
-                        sendOpenDeviceResponse(device, listener, handler);
+                        device = new MidiDevice(deviceInfo, server, mService, mToken, deviceToken);
+                        sendOpenDeviceResponse(device, listenerF, handlerF);
                     } catch (RemoteException e) {
-                        Log.e(TAG, "remote exception in onServiceConnected");
-                        sendOpenDeviceResponse(null, listener, handler);
+                        Log.e(TAG, "remote exception in getDeviceInfo()");
                     }
                 }
+                sendOpenDeviceResponse(device, listenerF, handlerF);
+            }
+        };
 
-                @Override
-                public void onServiceDisconnected(ComponentName name) {
-                    // FIXME - anything to do here?
-                }
-            },
-            Context.BIND_AUTO_CREATE))
-        {
-            Log.e(TAG, "Unable to bind service: " + intent);
-            sendOpenDeviceResponse(null, listener, handler);
+        try {
+            mService.openBluetoothDevice(mToken, bluetoothDevice, callback);
+        } catch (RemoteException e) {
+            Log.e(TAG, "RemoteException in openDevice");
         }
     }
 
diff --git a/media/java/android/media/midi/package.html b/media/java/android/media/midi/package.html
index bd589a9..8b2bd16 100644
--- a/media/java/android/media/midi/package.html
+++ b/media/java/android/media/midi/package.html
@@ -241,7 +241,8 @@
 
 
 <p>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. </p>
+an app can provide a custom synthesizer that other apps can send messages to.
+The service must be guarded with permission &quot;android.permission.BIND_MIDI_DEVICE_SERVICE&quot;.</p>
 
 <h2 id=manifest_files>Manifest Files</h2>
 
@@ -250,7 +251,8 @@
 AndroidManifest.xml file.</p>
 
 <pre class=prettyprint>
-&lt;service android:name="<strong>MySynthDeviceService</strong>">
+&lt;service android:name="<strong>MySynthDeviceService</strong>"
+  android:permission="android.permission.BIND_MIDI_DEVICE_SERVICE">
   &lt;intent-filter>
     &lt;action android:name="android.media.midi.MidiDeviceService" />
   &lt;/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 @@
 
     <application
         android:label="@string/app_name">
-        <service android:name="BluetoothMidiService">
+        <service android:name="BluetoothMidiService"
+            android:permission="android.permission.BIND_MIDI_DEVICE_SERVICE">
             <intent-filter>
                 <action android:name="android.media.midi.BluetoothMidiService" />
             </intent-filter>
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.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 @@
     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 @@
         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 @@
 
     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.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 @@
     private final HashMap<MidiDeviceInfo, Device> mDevicesByInfo
             = new HashMap<MidiDeviceInfo, Device>();
 
+    // list of all Bluetooth devices, keyed by BluetoothDevice
+     private final HashMap<BluetoothDevice, Device> mBluetoothDevices
+            = new HashMap<BluetoothDevice, Device>();
+
     // list of all devices, keyed by IMidiDeviceServer
     private final HashMap<IBinder, Device> mDevicesByServer = new HashMap<IBinder, Device>();
 
@@ -86,6 +95,9 @@
 
     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 @@
         // List of all receivers for this client
         private final ArrayList<IMidiDeviceListener> mListeners
                 = new ArrayList<IMidiDeviceListener>();
+        // List of all device connections for this client
+        private final HashMap<IBinder, DeviceConnection> mDeviceConnections
+                = new HashMap<IBinder, DeviceConnection>();
 
         public Client(IBinder token) {
             mToken = token;
@@ -132,8 +147,33 @@
 
         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 @@
             }
         }
 
+        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 @@
             sb.append(mPid);
             sb.append(" listener count: ");
             sb.append(mListeners.size());
+            sb.append(" Device Connections:");
+            for (DeviceConnection connection : mDeviceConnections.values()) {
+                sb.append(" <device ");
+                sb.append(connection.getDevice().getDeviceInfo().getId());
+                sb.append(">");
+            }
             return sb.toString();
         }
     }
@@ -211,57 +270,96 @@
         }
     }
 
-    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<DeviceConnection> mDeviceConnections
+                = new ArrayList<DeviceConnection>();
+
         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,14 +382,105 @@
             return (!mDeviceInfo.isPrivate() || mUid == uid);
         }
 
-        public void binderDied() {
-            synchronized (mDevicesByInfo) {
-                if (mDevicesByInfo.remove(mDeviceInfo) != null) {
-                    removeDeviceLocked(this);
+        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) {
+                closeLocked();
+            }
+        }
+
         @Override
         public String toString() {
             StringBuilder sb = new StringBuilder("Device Info: ");
@@ -300,10 +489,56 @@
             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 @@
                 }
             }
         }
+
+        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 @@
     }
 
     @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");
+            }
         }
 
-        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);
+            }
         }
 
-        return device.getDeviceServer();
+        // clear calling identity so bindService does not fail
+        long identity = Binder.clearCallingIdentity();
+        try {
+            client.addDeviceConnection(device, callback);
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    @Override
+    public void closeDevice(IBinder clientToken, IBinder deviceToken) {
+        Client client = getClient(clientToken);
+        if (client == null) return;
+        client.removeDeviceConnection(deviceToken);
     }
 
     @Override
@@ -376,6 +666,8 @@
         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 @@
         synchronized (mDevicesByInfo) {
             Device device = mDevicesByServer.get(server.asBinder());
             if (device != null) {
-                mDevicesByInfo.remove(device.getDeviceInfo());
-                removeDeviceLocked(device);
+                device.closeLocked();
             }
         }
     }
@@ -420,19 +711,16 @@
     }
 
     @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;
+    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);
         }
-        if (Binder.getCallingUid() != device.getUid()) {
-            throw new SecurityException("setDeviceStatus() caller UID " + Binder.getCallingUid()
-                    + " does not match device's UID " + device.getUid());
-        }
-        device.setDeviceStatus(token, status);
-        notifyDeviceStatusChanged(device, status);
     }
 
     private void notifyDeviceStatusChanged(Device device, MidiDeviceStatus status) {
@@ -452,18 +740,24 @@
         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 @@
     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 @@
                     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;