Merge "MidiManager: proxy all requests to open devices through MidiService" into mnc-dev
diff --git a/Android.mk b/Android.mk
index 6bfed8e..68f471a 100644
--- a/Android.mk
+++ b/Android.mk
@@ -343,6 +343,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 49e2268..c3761ad 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -28,6 +28,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";
@@ -17360,6 +17361,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 3e8b065..43f7f95 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -40,6 +40,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";
@@ -18672,6 +18673,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 f32918d..709de9e 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -1827,6 +1827,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;