MidiManager: Virtual MIDI devices are now implemented as Services

To implement a virtual MIDI device, include a subclass of MidiDeviceService in
your application.  This service is identified by an intent filter and meta-data
in the application's manifest to allow the MIDI manager to register the virtual device
without actually running the application. Instead, the application's MidiDeviceService
subclass is started on demand when MIDI manager clients want to open the device.

Here is an example of how the MidiDeviceService might be described in the application manifest:

    <service android:name="VirtualDeviceService">
        <intent-filter>
            <action android:name="android.media.midi.MidiDeviceService" />
        </intent-filter>
        <meta-data android:name="android.media.midi.MidiDeviceService"
            android:resource="@xml/device_info" />
    </service>

and the device_info.xml meta-data:

<devices>
    <device manufacturer="Sample Manufacturer" model="Sample Model" private="false">
        <input-port name="my input port" />
        <output-port name="my output port" />
    </device>
</devices>

(note that the <input-port> and <output-port> names are not currently used, but support for these
will be added in a subsequent change)

Client's of the virtual device will bind directly to the hosting application's MidiDeviceService subclass.
To support this, MidiManager.openDevice() now returns the MidiDevice asynchronously via a callback.

This change also adds a utility class called MidiDispatcher, which is a MidiReceiver
that dispatches all data it receives to a list of other MidiReceivers.
We now use this internally in MidiInputPort and MidiDeviceServer, but developers
may use it for other purposes as well.

Change-Id: Ic3009f06d56f3d5edbd87de3f0c330b51a1c217d
diff --git a/Android.mk b/Android.mk
index 669efc7..19d3de9 100644
--- a/Android.mk
+++ b/Android.mk
@@ -333,8 +333,8 @@
 	media/java/android/media/IRingtonePlayer.aidl \
 	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/IMidiDeviceServer.aidl \
-	media/java/android/media/midi/IMidiListener.aidl \
 	media/java/android/media/midi/IMidiManager.aidl \
 	media/java/android/media/projection/IMediaProjection.aidl \
 	media/java/android/media/projection/IMediaProjectionCallback.aidl \
diff --git a/media/java/android/media/midi/IMidiListener.aidl b/media/java/android/media/midi/IMidiDeviceListener.aidl
similarity index 95%
rename from media/java/android/media/midi/IMidiListener.aidl
rename to media/java/android/media/midi/IMidiDeviceListener.aidl
index a4129e9..17d9bfd 100644
--- a/media/java/android/media/midi/IMidiListener.aidl
+++ b/media/java/android/media/midi/IMidiDeviceListener.aidl
@@ -19,7 +19,7 @@
 import android.media.midi.MidiDeviceInfo;
 
 /** @hide */
-oneway interface IMidiListener
+oneway interface IMidiDeviceListener
 {
     void onDeviceAdded(in MidiDeviceInfo device);
     void onDeviceRemoved(in MidiDeviceInfo device);
diff --git a/media/java/android/media/midi/IMidiManager.aidl b/media/java/android/media/midi/IMidiManager.aidl
index bba35f5..617b03e 100644
--- a/media/java/android/media/midi/IMidiManager.aidl
+++ b/media/java/android/media/midi/IMidiManager.aidl
@@ -16,8 +16,8 @@
 
 package android.media.midi;
 
+import android.media.midi.IMidiDeviceListener;
 import android.media.midi.IMidiDeviceServer;
-import android.media.midi.IMidiListener;
 import android.media.midi.MidiDeviceInfo;
 import android.os.Bundle;
 import android.os.IBinder;
@@ -28,14 +28,20 @@
     MidiDeviceInfo[] getDeviceList();
 
     // for device creation & removal notifications
-    void registerListener(IBinder token, in IMidiListener listener);
-    void unregisterListener(IBinder token, in IMidiListener listener);
+    void registerListener(IBinder token, in IMidiDeviceListener listener);
+    void unregisterListener(IBinder token, in IMidiDeviceListener listener);
 
-    // for communicating with MIDI devices
+    // for opening built-in MIDI devices
     IMidiDeviceServer openDevice(IBinder token, in MidiDeviceInfo device);
 
-    // for implementing virtual MIDI devices
+    // for registering built-in MIDI devices
     MidiDeviceInfo registerDeviceServer(in IMidiDeviceServer server, int numInputPorts,
-            int numOutputPorts, in Bundle properties, boolean isPrivate, int type);
+            int numOutputPorts, in Bundle properties, int type);
+
+    // for unregistering built-in MIDI devices
     void unregisterDeviceServer(in IMidiDeviceServer server);
+
+    // used by MidiDeviceService to access the MidiDeviceInfo that was created based on its
+    // manifest's meta-data
+    MidiDeviceInfo getServiceDeviceInfo(String packageName, String className);
 }
diff --git a/media/java/android/media/midi/MidiDeviceInfo.java b/media/java/android/media/midi/MidiDeviceInfo.java
index fd35052..b7756fd 100644
--- a/media/java/android/media/midi/MidiDeviceInfo.java
+++ b/media/java/android/media/midi/MidiDeviceInfo.java
@@ -50,6 +50,7 @@
     private final int mInputPortCount;
     private final int mOutputPortCount;
     private final Bundle mProperties;
+    private final boolean mIsPrivate;
 
     /**
      * Bundle key for the device's manufacturer name property.
@@ -83,6 +84,8 @@
      * Bundle key for the device's ALSA card number.
      * Only set for USB MIDI devices.
      * Used with the {@link android.os.Bundle} returned by {@link #getProperties}
+     *
+     * @hide
      */
     public static final String PROPERTY_ALSA_CARD = "alsa_card";
 
@@ -90,20 +93,32 @@
      * Bundle key for the device's ALSA device number.
      * Only set for USB MIDI devices.
      * Used with the {@link android.os.Bundle} returned by {@link #getProperties}
+     *
+     * @hide
      */
     public static final String PROPERTY_ALSA_DEVICE = "alsa_device";
 
     /**
+     * {@link android.content.pm.ServiceInfo} for the service hosting the device implementation.
+     * Only set for Virtual MIDI devices.
+     * Used with the {@link android.os.Bundle} returned by {@link #getProperties}
+     *
+     * @hide
+     */
+    public static final String PROPERTY_SERVICE_INFO = "service_info";
+
+    /**
      * MidiDeviceInfo should only be instantiated by MidiService implementation
      * @hide
      */
     public MidiDeviceInfo(int type, int id, int numInputPorts, int numOutputPorts,
-            Bundle properties) {
+            Bundle properties, boolean isPrivate) {
         mType = type;
         mId = id;
         mInputPortCount = numInputPorts;
         mOutputPortCount = numOutputPorts;
         mProperties = properties;
+        mIsPrivate = isPrivate;
     }
 
     /**
@@ -152,6 +167,16 @@
         return mProperties;
     }
 
+    /**
+     * Returns true if the device is private.  Private devices are only visible and accessible
+     * to clients with the same UID as the application that is hosting the device.
+     *
+     * @return true if the device is private
+     */
+    public boolean isPrivate() {
+        return mIsPrivate;
+    }
+
     @Override
     public boolean equals(Object o) {
         if (o instanceof MidiDeviceInfo) {
@@ -171,7 +196,8 @@
         return ("MidiDeviceInfo[mType=" + mType +
                 ",mInputPortCount=" + mInputPortCount +
                 ",mOutputPortCount=" + mOutputPortCount +
-                ",mProperties=" + mProperties);
+                ",mProperties=" + mProperties +
+                ",mIsPrivate=" + mIsPrivate);
     }
 
     public static final Parcelable.Creator<MidiDeviceInfo> CREATOR =
@@ -182,7 +208,8 @@
             int inputPorts = in.readInt();
             int outputPorts = in.readInt();
             Bundle properties = in.readBundle();
-            return new MidiDeviceInfo(type, id, inputPorts, outputPorts, properties);
+            boolean isPrivate = (in.readInt() == 1);
+            return new MidiDeviceInfo(type, id, inputPorts, outputPorts, properties, isPrivate);
         }
 
         public MidiDeviceInfo[] newArray(int size) {
@@ -200,5 +227,6 @@
         parcel.writeInt(mInputPortCount);
         parcel.writeInt(mOutputPortCount);
         parcel.writeBundle(mProperties);
+        parcel.writeInt(mIsPrivate ? 1 : 0);
    }
 }
diff --git a/media/java/android/media/midi/MidiDeviceServer.java b/media/java/android/media/midi/MidiDeviceServer.java
index 3317baa..24ef528 100644
--- a/media/java/android/media/midi/MidiDeviceServer.java
+++ b/media/java/android/media/midi/MidiDeviceServer.java
@@ -16,21 +16,22 @@
 
 package android.media.midi;
 
+import android.os.IBinder;
+import android.os.Binder;
 import android.os.ParcelFileDescriptor;
+import android.os.Process;
 import android.os.RemoteException;
 import android.system.OsConstants;
 import android.util.Log;
 
+import libcore.io.IoUtils;
+
 import java.io.Closeable;
 import java.io.IOException;
-import java.util.ArrayList;
 
 /**
- * This class is used to provide the implemention of MIDI device.
- * Applications may call {@link MidiManager#createDeviceServer}
- * to create an instance of this class to implement a virtual MIDI device.
+ * Internal class used for providing an implementation for a MIDI device.
  *
- * CANDIDATE FOR PUBLIC API
  * @hide
  */
 public final class MidiDeviceServer implements Closeable {
@@ -40,64 +41,36 @@
 
     // MidiDeviceInfo for the device implemented by this server
     private MidiDeviceInfo mDeviceInfo;
-    private int mInputPortCount;
-    private int mOutputPortCount;
+    private final int mInputPortCount;
+    private final int mOutputPortCount;
 
-    // output ports for receiving messages from our clients
-    // we can have only one per port number
-    private MidiOutputPort[] mInputPortSenders;
+    // MidiReceivers for receiving data on our input ports
+    private final MidiReceiver[] mInputPortReceivers;
 
-    // receivers attached to our input ports
-    private ArrayList<MidiReceiver>[] mInputPortReceivers;
+    // MidiDispatchers for sending data on our output ports
+    private MidiDispatcher[] mOutputPortDispatchers;
 
-    // input ports for sending messages to our clients
-    // we can have multiple outputs per port number
-    private ArrayList<MidiInputPort>[] mOutputPortReceivers;
-
-    // subclass of MidiInputPort for passing to clients
-    // that notifies us when the connection has failed
-    private class ServerInputPort extends MidiInputPort {
-        ServerInputPort(ParcelFileDescriptor pfd, int portNumber) {
-            super(pfd, portNumber);
-        }
-
-        @Override
-        public void onIOException() {
-            synchronized (mOutputPortReceivers) {
-                mOutputPortReceivers[getPortNumber()].clear();
-            }
-        }
-    }
-
-    // subclass of MidiOutputPort for passing to clients
-    // that notifies us when the connection has failed
-    private class ServerOutputPort extends MidiOutputPort {
-        ServerOutputPort(ParcelFileDescriptor pfd, int portNumber) {
-            super(pfd, portNumber);
-        }
-
-        @Override
-        public void onIOException() {
-            synchronized (mInputPortSenders) {
-                mInputPortSenders[getPortNumber()] = null;
-            }
-        }
-    }
+    // MidiOutputPorts for clients connected to our input ports
+    private final MidiOutputPort[] mInputPortOutputPorts;
 
     // Binder interface stub for receiving connection requests from clients
     private final IMidiDeviceServer mServer = new IMidiDeviceServer.Stub() {
 
         @Override
         public ParcelFileDescriptor openInputPort(int portNumber) {
+            if (mDeviceInfo.isPrivate()) {
+                if (Binder.getCallingUid() != Process.myUid()) {
+                    throw new SecurityException("Can't access private device from different UID");
+                }
+            }
+
             if (portNumber < 0 || portNumber >= mInputPortCount) {
                 Log.e(TAG, "portNumber out of range in openInputPort: " + portNumber);
                 return null;
             }
 
-            ParcelFileDescriptor result = null;
-
-            synchronized (mInputPortSenders) {
-                if (mInputPortSenders[portNumber] != null) {
+            synchronized (mInputPortOutputPorts) {
+                if (mInputPortOutputPorts[portNumber] != null) {
                     Log.d(TAG, "port " + portNumber + " already open");
                     return null;
                 }
@@ -105,47 +78,86 @@
                 try {
                     ParcelFileDescriptor[] pair = ParcelFileDescriptor.createSocketPair(
                                                         OsConstants.SOCK_SEQPACKET);
-                    MidiOutputPort newOutputPort = new ServerOutputPort(pair[0], portNumber);
-                    mInputPortSenders[portNumber] = newOutputPort;
-                    result =  pair[1];
+                    final MidiOutputPort outputPort = new MidiOutputPort(pair[0], portNumber);
+                    mInputPortOutputPorts[portNumber] = outputPort;
+                    final int portNumberF = portNumber;
+                    final MidiReceiver inputPortReceviver = mInputPortReceivers[portNumber];
 
-                    ArrayList<MidiReceiver> receivers = mInputPortReceivers[portNumber];
-                    synchronized (receivers) {
-                        for (int i = 0; i < receivers.size(); i++) {
-                            newOutputPort.connect(receivers.get(i));
+                    outputPort.connect(new MidiReceiver() {
+                        @Override
+                        public void post(byte[] msg, int offset, int count, long timestamp)
+                                throws IOException {
+                            try {
+                                inputPortReceviver.post(msg, offset, count, timestamp);
+                            } catch (IOException e) {
+                                IoUtils.closeQuietly(mInputPortOutputPorts[portNumberF]);
+                                mInputPortOutputPorts[portNumberF] = null;
+                                // FIXME also flush the receiver
+                            }
                         }
-                    }
+                    });
+
+                    return pair[1];
                 } catch (IOException e) {
                     Log.e(TAG, "unable to create ParcelFileDescriptors in openInputPort");
                     return null;
                 }
             }
-
-            return result;
         }
 
         @Override
         public ParcelFileDescriptor openOutputPort(int portNumber) {
+            if (mDeviceInfo.isPrivate()) {
+                if (Binder.getCallingUid() != Process.myUid()) {
+                    throw new SecurityException("Can't access private device from different UID");
+                }
+            }
+
             if (portNumber < 0 || portNumber >= mOutputPortCount) {
                 Log.e(TAG, "portNumber out of range in openOutputPort: " + portNumber);
                 return null;
             }
-            synchronized (mOutputPortReceivers) {
-                try {
-                    ParcelFileDescriptor[] pair = ParcelFileDescriptor.createSocketPair(
-                                                        OsConstants.SOCK_SEQPACKET);
-                    mOutputPortReceivers[portNumber].add(new ServerInputPort(pair[0], portNumber));
-                    return pair[1];
-                } catch (IOException e) {
-                    Log.e(TAG, "unable to create ParcelFileDescriptors in openOutputPort");
-                    return null;
-                }
+
+            try {
+                ParcelFileDescriptor[] pair = ParcelFileDescriptor.createSocketPair(
+                                                    OsConstants.SOCK_SEQPACKET);
+                final MidiInputPort inputPort = new MidiInputPort(pair[0], portNumber);
+                final MidiSender sender = mOutputPortDispatchers[portNumber].getSender();
+                sender.connect(new MidiReceiver() {
+                        @Override
+                        public void post(byte[] msg, int offset, int count, long timestamp)
+                                throws IOException {
+                            try {
+                                inputPort.post(msg, offset, count, timestamp);
+                            } catch (IOException e) {
+                                IoUtils.closeQuietly(inputPort);
+                                sender.disconnect(this);
+                                // FIXME also flush the receiver?
+                            }
+                        }
+                    });
+
+                return pair[1];
+            } catch (IOException e) {
+                Log.e(TAG, "unable to create ParcelFileDescriptors in openOutputPort");
+                return null;
             }
         }
     };
 
-    /* package */ MidiDeviceServer(IMidiManager midiManager) {
+    /* package */ MidiDeviceServer(IMidiManager midiManager, MidiReceiver[] inputPortReceivers,
+            int numOutputPorts) {
         mMidiManager = midiManager;
+        mInputPortReceivers = inputPortReceivers;
+        mInputPortCount = inputPortReceivers.length;
+        mOutputPortCount = numOutputPorts;
+
+        mInputPortOutputPorts = new MidiOutputPort[mInputPortCount];
+
+        mOutputPortDispatchers = new MidiDispatcher[numOutputPorts];
+        for (int i = 0; i < numOutputPorts; i++) {
+            mOutputPortDispatchers[i] = new MidiDispatcher();
+        }
     }
 
     /* package */ IMidiDeviceServer getBinderInterface() {
@@ -157,19 +169,6 @@
             throw new IllegalStateException("setDeviceInfo should only be called once");
         }
         mDeviceInfo = deviceInfo;
-        mInputPortCount = deviceInfo.getInputPortCount();
-        mOutputPortCount = deviceInfo.getOutputPortCount();
-        mInputPortSenders = new MidiOutputPort[mInputPortCount];
-
-        mInputPortReceivers = new ArrayList[mInputPortCount];
-        for (int i = 0; i < mInputPortCount; i++) {
-            mInputPortReceivers[i] = new ArrayList<MidiReceiver>();
-        }
-
-        mOutputPortReceivers = new ArrayList[mOutputPortCount];
-        for (int i = 0; i < mOutputPortCount; i++) {
-            mOutputPortReceivers[i] = new ArrayList<MidiInputPort>();
-        }
     }
 
     @Override
@@ -183,86 +182,13 @@
     }
 
     /**
-     * Returns a {@link MidiDeviceInfo} object, which describes this device.
-     *
-     * @return the {@link MidiDeviceInfo} object
+     * Returns an array of {@link MidiReceiver} for the device's output ports.
+     * Clients can use these receivers to send data out the device's output ports.
+     * @return array of MidiReceivers
      */
-    public MidiDeviceInfo getInfo() {
-        return mDeviceInfo;
-    }
-
-    /**
-     * Called to open a {@link MidiSender} to allow receiving MIDI messages
-     * on the device's input port for the specified port number.
-     *
-     * @param portNumber the number of the input port
-     * @return the {@link MidiSender}
-     */
-    public MidiSender openInputPortSender(int portNumber) {
-        if (portNumber < 0 || portNumber >= mDeviceInfo.getInputPortCount()) {
-            throw new IllegalArgumentException("portNumber " + portNumber + " out of range");
-        }
-        final int portNumberF = portNumber;
-        return new MidiSender() {
-
-            @Override
-            public void connect(MidiReceiver receiver) {
-                // We always synchronize on mInputPortSenders before receivers if we need to
-                // synchronize on both.
-                synchronized (mInputPortSenders) {
-                    ArrayList<MidiReceiver> receivers = mInputPortReceivers[portNumberF];
-                    synchronized (receivers) {
-                        receivers.add(receiver);
-                        MidiOutputPort outputPort = mInputPortSenders[portNumberF];
-                        if (outputPort != null) {
-                            outputPort.connect(receiver);
-                        }
-                    }
-                }
-            }
-
-            @Override
-            public void disconnect(MidiReceiver receiver) {
-                // We always synchronize on mInputPortSenders before receivers if we need to
-                // synchronize on both.
-                synchronized (mInputPortSenders) {
-                    ArrayList<MidiReceiver> receivers = mInputPortReceivers[portNumberF];
-                    synchronized (receivers) {
-                        receivers.remove(receiver);
-                        MidiOutputPort outputPort = mInputPortSenders[portNumberF];
-                        if (outputPort != null) {
-                            outputPort.disconnect(receiver);
-                        }
-                    }
-                }
-            }
-        };
-    }
-
-    /**
-     * Called to open a {@link MidiReceiver} to allow sending MIDI messages
-     * on the virtual device's output port for the specified port number.
-     *
-     * @param portNumber the number of the output port
-     * @return the {@link MidiReceiver}
-     */
-    public MidiReceiver openOutputPortReceiver(int portNumber) {
-        if (portNumber < 0 || portNumber >= mDeviceInfo.getOutputPortCount()) {
-            throw new IllegalArgumentException("portNumber " + portNumber + " out of range");
-        }
-        final int portNumberF = portNumber;
-        return new MidiReceiver() {
-
-            @Override
-            public void post(byte[] msg, int offset, int count, long timestamp) throws IOException {
-                ArrayList<MidiInputPort> receivers = mOutputPortReceivers[portNumberF];
-                synchronized (receivers) {
-                    for (int i = 0; i < receivers.size(); i++) {
-                        // FIXME catch errors and remove dead ones
-                        receivers.get(i).post(msg, offset, count, timestamp);
-                    }
-                }
-            }
-        };
+    public MidiReceiver[] getOutputPortReceivers() {
+        MidiReceiver[] receivers = new MidiReceiver[mOutputPortCount];
+        System.arraycopy(mOutputPortDispatchers, 0, receivers, 0, mOutputPortCount);
+        return receivers;
     }
 }
diff --git a/media/java/android/media/midi/MidiDeviceService.java b/media/java/android/media/midi/MidiDeviceService.java
new file mode 100644
index 0000000..1d91be2
--- /dev/null
+++ b/media/java/android/media/midi/MidiDeviceService.java
@@ -0,0 +1,111 @@
+/*
+ * 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.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Log;
+
+/**
+ * A service that implements a virtual MIDI device.
+ * Subclasses must implement the {@link #getInputPortReceivers} method to provide a
+ * list of {@link MidiReceiver}s to receive data sent to the device's input ports.
+ * Similarly, subclasses can call {@link #getOutputPortReceivers} to fetch a list
+ * of {@link MidiReceiver}s for sending data out the output ports.
+ *
+ * <p>To extend this class, you must declare the service in your manifest file with
+ * an intent filter with the {@link #SERVICE_INTERFACE} action
+ * and meta-data to describe the virtual device.
+ For example:</p>
+ * <pre>
+ * &lt;service android:name=".VirtualDeviceService"
+ *          android:label="&#64;string/service_name">
+ *     &lt;intent-filter>
+ *         &lt;action android:name="android.media.midi.MidiDeviceService" />
+ *     &lt;/intent-filter>
+ *           &lt;meta-data android:name="android.media.midi.MidiDeviceService"
+                android:resource="@xml/device_info" />
+ * &lt;/service></pre>
+ *
+ * CANDIDATE FOR PUBLIC API
+ * @hide
+ */
+abstract public class MidiDeviceService extends Service {
+    private static final String TAG = "MidiDeviceService";
+
+    public static final String SERVICE_INTERFACE = "android.media.midi.MidiDeviceService";
+
+    private IMidiManager mMidiManager;
+    private MidiDeviceServer mServer;
+
+    @Override
+    public void onCreate() {
+        mMidiManager = IMidiManager.Stub.asInterface(
+                    ServiceManager.getService(Context.MIDI_SERVICE));
+        MidiDeviceServer server;
+        try {
+            MidiDeviceInfo deviceInfo = mMidiManager.getServiceDeviceInfo(getPackageName(),
+                    this.getClass().getName());
+            MidiReceiver[] inputPortReceivers = getInputPortReceivers();
+            if (inputPortReceivers == null) {
+                inputPortReceivers = new MidiReceiver[0];
+            }
+            server = new MidiDeviceServer(mMidiManager, inputPortReceivers,
+                    deviceInfo.getOutputPortCount());
+            server.setDeviceInfo(deviceInfo);
+        } catch (RemoteException e) {
+            Log.e(TAG, "RemoteException in IMidiManager.getServiceDeviceInfo");
+            server = null;
+        }
+        mServer = server;
+   }
+
+    /**
+     * Returns an array of {@link MidiReceiver} for the device's input ports.
+     * Subclasses must override this to provide the receivers which will receive
+     * data sent to the device's input ports. An empty array or null should be returned if
+     * the device has no input ports.
+     * @return array of MidiReceivers
+     */
+    abstract public MidiReceiver[] getInputPortReceivers();
+
+    /**
+     * Returns an array of {@link MidiReceiver} for the device's output ports.
+     * These can be used to send data out the device's output ports.
+     * @return array of MidiReceivers
+     */
+    public MidiReceiver[] getOutputPortReceivers() {
+        if (mServer == null) {
+            return null;
+        } else {
+            return mServer.getOutputPortReceivers();
+        }
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        if (SERVICE_INTERFACE.equals(intent.getAction()) && mServer != null) {
+             return mServer.getBinderInterface().asBinder();
+        } else {
+             return null;
+       }
+    }
+}
diff --git a/media/java/android/media/midi/MidiDispatcher.java b/media/java/android/media/midi/MidiDispatcher.java
new file mode 100644
index 0000000..165061f
--- /dev/null
+++ b/media/java/android/media/midi/MidiDispatcher.java
@@ -0,0 +1,89 @@
+/*
+ * 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 java.io.IOException;
+import java.util.ArrayList;
+
+/**
+ * Utility class for dispatching MIDI data to a list of {@link MidiReceiver}s.
+ * This class subclasses {@link MidiReceiver} and dispatches any data it receives
+ * to its receiver list. Any receivers that throw an exception upon receiving data will
+ * be automatically removed from the receiver list, but no IOException will be returned
+ * from the dispatcher's {@link #post} in that case.
+ *
+ * CANDIDATE FOR PUBLIC API
+ * @hide
+ */
+public class MidiDispatcher implements MidiReceiver {
+
+    private final ArrayList<MidiReceiver> mReceivers = new ArrayList<MidiReceiver>();
+
+    private final MidiSender mSender = new MidiSender() {
+        /**
+         * Called to connect a {@link MidiReceiver} to the sender
+         *
+         * @param receiver the receiver to connect
+         */
+        public void connect(MidiReceiver receiver) {
+            mReceivers.add(receiver);
+        }
+
+        /**
+         * Called to disconnect a {@link MidiReceiver} from the sender
+         *
+         * @param receiver the receiver to disconnect
+         */
+        public void disconnect(MidiReceiver receiver) {
+            mReceivers.remove(receiver);
+        }
+    };
+
+    /**
+     * Returns whether this dispatcher contains any receivers.
+     * @return true if the receiver list is not empty
+     */
+    public boolean hasReceivers() {
+        return mReceivers.size() > 0;
+    }
+
+    /**
+     * Returns a {@link MidiSender} which is used to add and remove {@link MidiReceiver}s
+     * to the dispatcher's receiver list.
+     * @return the dispatcher's MidiSender
+     */
+    public MidiSender getSender() {
+        return mSender;
+    }
+
+    @Override
+    public void post(byte[] msg, int offset, int count, long timestamp) throws IOException {
+        synchronized (mReceivers) {
+           for (int i = 0; i < mReceivers.size(); ) {
+                MidiReceiver receiver = mReceivers.get(i);
+                try {
+                    receiver.post(msg, offset, count, timestamp);
+                    i++;    // increment only on success. on failure we remove the receiver
+                            // so i should not be incremented
+                } catch (IOException e) {
+                    // if the receiver fails we remove the receiver but do not propogate the exception
+                    mSender.disconnect(receiver);
+                }
+            }
+        }
+    }
+}
diff --git a/media/java/android/media/midi/MidiManager.java b/media/java/android/media/midi/MidiManager.java
index 410120d..7022543 100644
--- a/media/java/android/media/midi/MidiManager.java
+++ b/media/java/android/media/midi/MidiManager.java
@@ -16,7 +16,11 @@
 
 package android.media.midi;
 
+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;
@@ -50,7 +54,7 @@
         new HashMap<DeviceCallback,DeviceListener>();
 
     // Binder stub for receiving device notifications from MidiService
-    private class DeviceListener extends IMidiListener.Stub {
+    private class DeviceListener extends IMidiDeviceListener.Stub {
         private final DeviceCallback mCallback;
         private final Handler mHandler;
 
@@ -89,22 +93,33 @@
     /**
      * Callback class used for clients to receive MIDI device added and removed notifications
      */
-    public static class DeviceCallback {
+    abstract public static class DeviceCallback {
         /**
          * Called to notify when a new MIDI device has been added
          *
          * @param device a {@link MidiDeviceInfo} for the newly added device
          */
-        public void onDeviceAdded(MidiDeviceInfo device) {
-        }
+        abstract public void onDeviceAdded(MidiDeviceInfo device);
 
         /**
          * Called to notify when a MIDI device has been removed
          *
          * @param device a {@link MidiDeviceInfo} for the removed device
          */
-        public void onDeviceRemoved(MidiDeviceInfo device) {
-        }
+        abstract public void onDeviceRemoved(MidiDeviceInfo device);
+    }
+
+    /**
+     * Callback class used for receiving the results of {@link #openDevice}
+     */
+    abstract public static class DeviceOpenCallback {
+        /**
+         * Called to respond to a {@link #openDevice} request
+         *
+         * @param deviceInfo the {@link MidiDeviceInfo} for the device to open
+         * @param device a {@link MidiDevice} for opened device, or null if opening failed
+         */
+        abstract public void onDeviceOpened(MidiDeviceInfo deviceInfo, MidiDevice device);
     }
 
     /**
@@ -164,33 +179,85 @@
         }
     }
 
+    private void sendOpenDeviceResponse(final MidiDeviceInfo deviceInfo, final MidiDevice device,
+            final DeviceOpenCallback callback, Handler handler) {
+        if (handler != null) {
+            handler.post(new Runnable() {
+                    @Override public void run() {
+                        callback.onDeviceOpened(deviceInfo, device);
+                    }
+                });
+        } else {
+            callback.onDeviceOpened(deviceInfo, device);
+        }
+    }
+
     /**
      * Opens a MIDI device for reading and writing.
      *
      * @param deviceInfo a {@link android.media.midi.MidiDeviceInfo} to open
-     * @return a {@link MidiDevice} object for the device
+     * @param callback a {@link #DeviceOpenCallback} 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
+     *                callback is unspecified.
      */
-    public MidiDevice openDevice(MidiDeviceInfo deviceInfo) {
+    public void openDevice(MidiDeviceInfo deviceInfo, DeviceOpenCallback callback,
+            Handler handler) {
+        MidiDevice device = null;
         try {
             IMidiDeviceServer server = mService.openDevice(mToken, deviceInfo);
             if (server == null) {
-                Log.e(TAG, "could not open device " + deviceInfo);
-                return 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 DeviceOpenCallback callbackF = callback;
+                    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);
+                                sendOpenDeviceResponse(deviceInfoF, device, callbackF, handlerF);
+                            }
+
+                            @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);
+                    }
+                }
+            } else {
+                device = new MidiDevice(deviceInfo, server);
             }
-            return new MidiDevice(deviceInfo, server);
         } catch (RemoteException e) {
             Log.e(TAG, "RemoteException in openDevice");
         }
-        return null;
+        sendOpenDeviceResponse(deviceInfo, device, callback, handler);
     }
 
     /** @hide */
-    public MidiDeviceServer createDeviceServer(int numInputPorts, int numOutputPorts,
-            Bundle properties, boolean isPrivate, int type) {
+    public MidiDeviceServer createDeviceServer(MidiReceiver[] inputPortReceivers,
+            int numOutputPorts, Bundle properties, int type) {
         try {
-            MidiDeviceServer server = new MidiDeviceServer(mService);
+            MidiDeviceServer server = new MidiDeviceServer(mService, inputPortReceivers,
+                    numOutputPorts);
             MidiDeviceInfo deviceInfo = mService.registerDeviceServer(server.getBinderInterface(),
-                    numInputPorts, numOutputPorts, properties, isPrivate, type);
+                    inputPortReceivers.length, numOutputPorts, properties, type);
             if (deviceInfo == null) {
                 Log.e(TAG, "registerVirtualDevice failed");
                 return null;
@@ -202,21 +269,4 @@
             return null;
         }
     }
-
-    /**
-     * Creates a new MIDI virtual device.
-     *
-     * @param numInputPorts number of input ports for the virtual device
-     * @param numOutputPorts number of output ports for the virtual device
-     * @param properties a {@link android.os.Bundle} containing properties describing the device
-     * @param isPrivate true if this device should only be visible and accessible to apps
-     *                  with the same UID as the caller
-     * @return a {@link MidiDeviceServer} object to locally represent the device
-     */
-    public MidiDeviceServer createDeviceServer(int numInputPorts, int numOutputPorts,
-            Bundle properties, boolean isPrivate) {
-        return createDeviceServer(numInputPorts, numOutputPorts, properties,
-                isPrivate, MidiDeviceInfo.TYPE_VIRTUAL);
-    }
-
 }
diff --git a/media/java/android/media/midi/MidiOutputPort.java b/media/java/android/media/midi/MidiOutputPort.java
index 83ddeeb..c195603 100644
--- a/media/java/android/media/midi/MidiOutputPort.java
+++ b/media/java/android/media/midi/MidiOutputPort.java
@@ -23,7 +23,6 @@
 
 import java.io.FileInputStream;
 import java.io.IOException;
-import java.util.ArrayList;
 
 /**
  * This class is used for receiving data from a port on a MIDI device
@@ -35,11 +34,7 @@
     private static final String TAG = "MidiOutputPort";
 
     private final FileInputStream mInputStream;
-
-    // array of receiver lists, indexed by port number
-    private final ArrayList<MidiReceiver> mReceivers = new ArrayList<MidiReceiver>();
-
-    private int mReceiverCount; // total number of receivers for all ports
+    private final MidiDispatcher mDispatcher = new MidiDispatcher();
 
     // This thread reads MIDI events from a socket and distributes them to the list of
     // MidiReceivers attached to this device.
@@ -47,7 +42,6 @@
         @Override
         public void run() {
             byte[] buffer = new byte[MAX_PACKET_SIZE];
-            ArrayList<MidiReceiver> deadReceivers = new ArrayList<MidiReceiver>();
 
             try {
                 while (true) {
@@ -55,77 +49,39 @@
                     int count = mInputStream.read(buffer);
                     if (count < 0) {
                         break;
+                        // FIXME - inform receivers here?
                     }
 
                     int offset = getMessageOffset(buffer, count);
                     int size = getMessageSize(buffer, count);
                     long timestamp = getMessageTimeStamp(buffer, count);
 
-                    synchronized (mReceivers) {
-                        for (int i = 0; i < mReceivers.size(); i++) {
-                            MidiReceiver receiver = mReceivers.get(i);
-                            try {
-                                receiver.post(buffer, offset, size, timestamp);
-                            } catch (IOException e) {
-                                Log.e(TAG, "post failed");
-                                deadReceivers.add(receiver);
-                            }
-                        }
-                        // remove any receivers that failed
-                        if (deadReceivers.size() > 0) {
-                            for (MidiReceiver receiver: deadReceivers) {
-                                mReceivers.remove(receiver);
-                                mReceiverCount--;
-                            }
-                            deadReceivers.clear();
-                        }
-                        // exit if we have no receivers left
-                        if (mReceiverCount == 0) {
-                            break;
-                        }
-                    }
+                    // dispatch to all our receivers
+                    mDispatcher.post(buffer, offset, size, timestamp);
                 }
             } catch (IOException e) {
-                // report I/O failure
+                // FIXME report I/O failure?
                 Log.e(TAG, "read failed");
             } finally {
                 IoUtils.closeQuietly(mInputStream);
-                onIOException();
             }
         }
     };
 
-  /* package */ MidiOutputPort(ParcelFileDescriptor pfd, int portNumber) {
+   /* package */ MidiOutputPort(ParcelFileDescriptor pfd, int portNumber) {
         super(portNumber);
         mInputStream = new ParcelFileDescriptor.AutoCloseInputStream(pfd);
+        mThread.start();
     }
 
-    /**
-     * Connects a {@link MidiReceiver} to the output port to allow receiving
-     * MIDI messages from the port.
-     *
-     * @param receiver the receiver to connect
-     */
+    @Override
     public void connect(MidiReceiver receiver) {
-        synchronized (mReceivers) {
-            mReceivers.add(receiver);
-            if (mReceiverCount++ == 0) {
-                mThread.start();
-            }
-        }
+        mDispatcher.getSender().connect(receiver);
     }
 
-    /**
-     * Disconnects a {@link MidiReceiver} from the output port.
-     *
-     * @param receiver the receiver to connect
-     */
+    @Override
     public void disconnect(MidiReceiver receiver) {
-        synchronized (mReceivers) {
-            if (mReceivers.remove(receiver)) {
-                mReceiverCount--;
-            }
-        }
+        mDispatcher.getSender().disconnect(receiver);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/MidiService.java b/services/core/java/com/android/server/MidiService.java
index 04911fa..725e925 100644
--- a/services/core/java/com/android/server/MidiService.java
+++ b/services/core/java/com/android/server/MidiService.java
@@ -17,10 +17,18 @@
 package com.android.server;
 
 import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.content.res.XmlResourceParser;
+import android.media.midi.IMidiDeviceListener;
 import android.media.midi.IMidiDeviceServer;
-import android.media.midi.IMidiListener;
 import android.media.midi.IMidiManager;
 import android.media.midi.MidiDeviceInfo;
+import android.media.midi.MidiDeviceService;
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.IBinder;
@@ -29,12 +37,17 @@
 import android.os.RemoteException;
 import android.util.Log;
 
+import com.android.internal.content.PackageMonitor;
 import com.android.internal.util.IndentingPrintWriter;
+import com.android.internal.util.XmlUtils;
+
+import org.xmlpull.v1.XmlPullParser;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 
 public class MidiService extends IMidiManager.Stub {
     private static final String TAG = "MidiService";
@@ -54,6 +67,27 @@
     // used for assigning IDs to MIDI devices
     private int mNextDeviceId = 1;
 
+    private final PackageManager mPackageManager;
+
+    // PackageMonitor for listening to package changes
+    private final PackageMonitor mPackageMonitor = new PackageMonitor() {
+        @Override
+        public void onPackageAdded(String packageName, int uid) {
+            addPackageDeviceServers(packageName);
+        }
+
+        @Override
+        public void onPackageModified(String packageName) {
+            removePackageDeviceServers(packageName);
+            addPackageDeviceServers(packageName);
+        }
+
+        @Override
+        public void onPackageRemoved(String packageName, int uid) {
+            removePackageDeviceServers(packageName);
+        }
+    };
+
     private final class Client implements IBinder.DeathRecipient {
         // Binder token for this client
         private final IBinder mToken;
@@ -62,7 +96,8 @@
         // This client's PID
         private final int mPid;
         // List of all receivers for this client
-        private final ArrayList<IMidiListener> mListeners = new ArrayList<IMidiListener>();
+        private final ArrayList<IMidiDeviceListener> mListeners
+                = new ArrayList<IMidiDeviceListener>();
 
         public Client(IBinder token) {
             mToken = token;
@@ -74,11 +109,11 @@
             return mUid;
         }
 
-        public void addListener(IMidiListener listener) {
+        public void addListener(IMidiDeviceListener listener) {
             mListeners.add(listener);
         }
 
-        public void removeListener(IMidiListener listener) {
+        public void removeListener(IMidiDeviceListener listener) {
             mListeners.remove(listener);
             if (mListeners.size() == 0) {
                 removeClient(mToken);
@@ -91,7 +126,7 @@
 
             MidiDeviceInfo deviceInfo = device.getDeviceInfo();
             try {
-                for (IMidiListener listener : mListeners) {
+                for (IMidiDeviceListener listener : mListeners) {
                     listener.onDeviceAdded(deviceInfo);
                 }
             } catch (RemoteException e) {
@@ -105,7 +140,7 @@
 
             MidiDeviceInfo deviceInfo = device.getDeviceInfo();
             try {
-                for (IMidiListener listener : mListeners) {
+                for (IMidiDeviceListener listener : mListeners) {
                     listener.onDeviceRemoved(deviceInfo);
                 }
             } catch (RemoteException e) {
@@ -153,16 +188,17 @@
     private final class Device implements IBinder.DeathRecipient {
         private final IMidiDeviceServer mServer;
         private final MidiDeviceInfo mDeviceInfo;
-        // UID of device creator
+        // ServiceInfo for the device's MidiDeviceServer implementation (virtual devices only)
+        private final ServiceInfo mServiceInfo;
+        // UID of device implementation
         private final int mUid;
-        // PID of device creator
-        private final int mPid;
 
-        public Device(IMidiDeviceServer server, MidiDeviceInfo deviceInfo) {
+        public Device(IMidiDeviceServer server, MidiDeviceInfo deviceInfo,
+                ServiceInfo serviceInfo, int uid) {
             mServer = server;
             mDeviceInfo = deviceInfo;
-            mUid = Binder.getCallingUid();
-            mPid = Binder.getCallingPid();
+            mServiceInfo = serviceInfo;
+            mUid = uid;
         }
 
         public MidiDeviceInfo getDeviceInfo() {
@@ -173,13 +209,20 @@
             return mServer;
         }
 
+        public ServiceInfo getServiceInfo() {
+            return mServiceInfo;
+        }
+
+        public String getPackageName() {
+            return (mServiceInfo == null ? null : mServiceInfo.packageName);
+        }
+
         public boolean isUidAllowed(int uid) {
-            // FIXME
-            return true;
+            return (!mDeviceInfo.isPrivate() || mUid == uid);
         }
 
         public void binderDied() {
-            synchronized (mDevicesByServer) {
+            synchronized (mDevicesByInfo) {
                 removeDeviceLocked(this);
             }
         }
@@ -190,32 +233,59 @@
             sb.append(mDeviceInfo);
             sb.append(" UID: ");
             sb.append(mUid);
-            sb.append(" PID: ");
-            sb.append(mPid);
             return sb.toString();
         }
     }
 
     public MidiService(Context context) {
         mContext = context;
-    }
+        mPackageManager = context.getPackageManager();
+        mPackageMonitor.register(context, null, true);
 
-    public void registerListener(IBinder token, IMidiListener listener) {
+        Intent intent = new Intent(MidiDeviceService.SERVICE_INTERFACE);
+        List<ResolveInfo> resolveInfos = mPackageManager.queryIntentServices(intent,
+                PackageManager.GET_META_DATA);
+        if (resolveInfos != null) {
+            int count = resolveInfos.size();
+            for (int i = 0; i < count; i++) {
+                ServiceInfo serviceInfo = resolveInfos.get(i).serviceInfo;
+                if (serviceInfo != null) {
+                    addPackageDeviceServer(serviceInfo);
+                }
+            }
+        }
+   }
+
+    @Override
+    public void registerListener(IBinder token, IMidiDeviceListener listener) {
         Client client = getClient(token);
         if (client == null) return;
         client.addListener(listener);
     }
 
-    public void unregisterListener(IBinder token, IMidiListener listener) {
+    @Override
+    public void unregisterListener(IBinder token, IMidiDeviceListener listener) {
         Client client = getClient(token);
         if (client == null) return;
         client.removeListener(listener);
     }
 
     public MidiDeviceInfo[] getDeviceList() {
-        return mDevicesByInfo.keySet().toArray(new MidiDeviceInfo[0]);
+        ArrayList<MidiDeviceInfo> deviceInfos = new ArrayList<MidiDeviceInfo>();
+        int uid = Binder.getCallingUid();
+
+        synchronized (mDevicesByInfo) {
+            for (Device device : mDevicesByInfo.values()) {
+                if (device.isUidAllowed(uid)) {
+                    deviceInfos.add(device.getDeviceInfo());
+                }
+            }
+        }
+
+        return deviceInfos.toArray(new MidiDeviceInfo[0]);
     }
 
+    @Override
     public IMidiDeviceServer openDevice(IBinder token, MidiDeviceInfo deviceInfo) {
         Device device = mDevicesByInfo.get(deviceInfo);
         if (device == null) {
@@ -230,28 +300,64 @@
         return device.getDeviceServer();
     }
 
+    @Override
     public MidiDeviceInfo registerDeviceServer(IMidiDeviceServer server, int numInputPorts,
-            int numOutputPorts, Bundle properties, boolean isPrivate, int type) {
+            int numOutputPorts, Bundle properties, int type) {
         if (type != MidiDeviceInfo.TYPE_VIRTUAL && Binder.getCallingUid() != Process.SYSTEM_UID) {
             throw new SecurityException("only system can create non-virtual devices");
         }
 
-        MidiDeviceInfo deviceInfo;
-        Device device;
+        synchronized (mDevicesByInfo) {
+            return addDeviceLocked(type, numInputPorts, numOutputPorts, properties,
+            server, null, false, -1);
+        }
+    }
 
-        synchronized (mDevicesByServer) {
-            int id = mNextDeviceId++;
-            deviceInfo = new MidiDeviceInfo(type, id, numInputPorts, numOutputPorts, properties);
+    @Override
+    public void unregisterDeviceServer(IMidiDeviceServer server) {
+        synchronized (mDevicesByInfo) {
+            Device device = mDevicesByServer.get(server.asBinder());
+            if (device != null) {
+                removeDeviceLocked(device);
+            }
+        }
+    }
+
+    @Override
+    public MidiDeviceInfo getServiceDeviceInfo(String packageName, String className) {
+        synchronized (mDevicesByInfo) {
+            for (Device device : mDevicesByInfo.values()) {
+                 ServiceInfo serviceInfo = device.getServiceInfo();
+                 if (serviceInfo != null &&
+                        packageName.equals(serviceInfo.packageName) &&
+                        className.equals(serviceInfo.name)) {
+                    return device.getDeviceInfo();
+                }
+            }
+            return null;
+        }
+    }
+
+    // synchronize on mDevicesByInfo
+    private MidiDeviceInfo addDeviceLocked(int type, int numInputPorts, int numOutputPorts,
+            Bundle properties, IMidiDeviceServer server, ServiceInfo serviceInfo,
+            boolean isPrivate, int uid) {
+
+        int id = mNextDeviceId++;
+        MidiDeviceInfo deviceInfo = new MidiDeviceInfo(type, id, numInputPorts, numOutputPorts,
+                properties, isPrivate);
+        Device device = new Device(server, deviceInfo, serviceInfo, uid);
+
+        if (server != null) {
             IBinder binder = server.asBinder();
-            device = new Device(server, deviceInfo);
             try {
                 binder.linkToDeath(device, 0);
             } catch (RemoteException e) {
                 return null;
             }
-            mDevicesByInfo.put(deviceInfo, device);
-            mDevicesByServer.put(server.asBinder(), device);
+            mDevicesByServer.put(binder, device);
         }
+        mDevicesByInfo.put(deviceInfo, device);
 
         synchronized (mClients) {
             for (Client c : mClients.values()) {
@@ -262,16 +368,13 @@
         return deviceInfo;
     }
 
-    public void unregisterDeviceServer(IMidiDeviceServer server) {
-        synchronized (mDevicesByServer) {
-            removeDeviceLocked(mDevicesByServer.get(server.asBinder()));
-        }
-    }
-
-    // synchronize on mDevicesByServer
+    // synchronize on mDevicesByInfo
     private void removeDeviceLocked(Device device) {
-        if (mDevicesByServer.remove(device.getDeviceServer().asBinder()) != null) {
-            mDevicesByInfo.remove(device.getDeviceInfo());
+        if (mDevicesByInfo.remove(device.getDeviceInfo()) != null) {
+            IMidiDeviceServer server = device.getDeviceServer();
+            if (server != null) {
+                mDevicesByServer.remove(server);
+            }
 
             synchronized (mClients) {
                 for (Client c : mClients.values()) {
@@ -281,6 +384,134 @@
         }
     }
 
+    private void addPackageDeviceServers(String packageName) {
+        PackageInfo info;
+
+        try {
+            info = mPackageManager.getPackageInfo(packageName,
+                    PackageManager.GET_SERVICES | PackageManager.GET_META_DATA);
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.e(TAG, "handlePackageUpdate could not find package " + packageName, e);
+            return;
+        }
+
+        ServiceInfo[] services = info.services;
+        if (services == null) return;
+        for (int i = 0; i < services.length; i++) {
+            addPackageDeviceServer(services[i]);
+        }
+    }
+
+    private void addPackageDeviceServer(ServiceInfo serviceInfo) {
+        XmlResourceParser parser = null;
+
+        try {
+            parser = serviceInfo.loadXmlMetaData(mPackageManager,
+                    MidiDeviceService.SERVICE_INTERFACE);
+            if (parser == null) return;
+
+            Bundle properties = null;
+            int numInputPorts = 0;
+            int numOutputPorts = 0;
+            boolean isPrivate = false;
+
+            while (true) {
+                int eventType = parser.next();
+                if (eventType == XmlPullParser.END_DOCUMENT) {
+                    break;
+                } else if (eventType == XmlPullParser.START_TAG) {
+                    String tagName = parser.getName();
+                    if ("device".equals(tagName)) {
+                        if (properties != null) {
+                            Log.w(TAG, "nested <device> elements in metadata for "
+                                + serviceInfo.packageName);
+                            continue;
+                        }
+                        properties = new Bundle();
+                        properties.putParcelable(MidiDeviceInfo.PROPERTY_SERVICE_INFO, serviceInfo);
+                        numInputPorts = 0;
+                        numOutputPorts = 0;
+                        isPrivate = false;
+
+                        int count = parser.getAttributeCount();
+                        for (int i = 0; i < count; i++) {
+                            String name = parser.getAttributeName(i);
+                            String value = parser.getAttributeValue(i);
+                            if ("private".equals(name)) {
+                                isPrivate = "true".equals(value);
+                            } else {
+                                properties.putString(name, value);
+                            }
+                        }
+                    } else if ("input-port".equals(tagName)) {
+                        if (properties == null) {
+                            Log.w(TAG, "<input-port> outside of <device> in metadata for "
+                                + serviceInfo.packageName);
+                            continue;
+                        }
+                        numInputPorts++;
+                        // TODO - add support for port properties
+                    } else if ("output-port".equals(tagName)) {
+                        if (properties == null) {
+                            Log.w(TAG, "<output-port> outside of <device> in metadata for "
+                                + serviceInfo.packageName);
+                            continue;
+                        }
+                        numOutputPorts++;
+                        // TODO - add support for port properties
+                    }
+                } else if (eventType == XmlPullParser.END_TAG) {
+                    String tagName = parser.getName();
+                    if ("device".equals(tagName)) {
+                        if (properties != null) {
+                            if (numInputPorts == 0 && numOutputPorts == 0) {
+                                Log.w(TAG, "<device> with no ports in metadata for "
+                                    + serviceInfo.packageName);
+                                continue;
+                            }
+
+                            int uid = -1;
+                            if (isPrivate) {
+                                try {
+                                    ApplicationInfo appInfo = mPackageManager.getApplicationInfo(
+                                            serviceInfo.packageName, 0);
+                                    uid = appInfo.uid;
+                                } catch (PackageManager.NameNotFoundException e) {
+                                    Log.e(TAG, "could not fetch ApplicationInfo for "
+                                            + serviceInfo.packageName);
+                                    continue;
+                                }
+                            }
+
+                            synchronized (mDevicesByInfo) {
+                                addDeviceLocked(MidiDeviceInfo.TYPE_VIRTUAL,
+                                    numInputPorts, numOutputPorts, properties,
+                                    null, serviceInfo, isPrivate, uid);
+                            }
+                            // setting properties to null signals that we are no longer
+                            // processing a <device>
+                            properties = null;
+                        }
+                    }
+                }
+            }
+        } catch (Exception e) {
+            Log.w(TAG, "Unable to load component info " + serviceInfo.toString(), e);
+        } finally {
+            if (parser != null) parser.close();
+        }
+    }
+
+    private void removePackageDeviceServers(String packageName) {
+        synchronized (mDevicesByInfo) {
+            for (Device device : mDevicesByInfo.values()) {
+                if (packageName.equals(device.getPackageName())) {
+                    removeDeviceLocked(device);
+                }
+            }
+        }
+    }
+
     @Override
     public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
         mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, TAG);
diff --git a/services/usb/java/com/android/server/usb/UsbMidiDevice.java b/services/usb/java/com/android/server/usb/UsbMidiDevice.java
index e17abc0..a6e9677 100644
--- a/services/usb/java/com/android/server/usb/UsbMidiDevice.java
+++ b/services/usb/java/com/android/server/usb/UsbMidiDevice.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.media.midi.MidiDeviceInfo;
 import android.media.midi.MidiDeviceServer;
+import android.media.midi.MidiDispatcher;
 import android.media.midi.MidiManager;
 import android.media.midi.MidiPort;
 import android.media.midi.MidiReceiver;
@@ -30,6 +31,8 @@
 import android.system.StructPollfd;
 import android.util.Log;
 
+import libcore.io.IoUtils;
+
 import java.io.Closeable;
 import java.io.FileDescriptor;
 import java.io.FileInputStream;
@@ -39,8 +42,9 @@
 public final class UsbMidiDevice implements Closeable {
     private static final String TAG = "UsbMidiDevice";
 
-    private final MidiDeviceServer mServer;
-    private final MidiReceiver[] mOutputPortReceivers;
+    private MidiDeviceServer mServer;
+
+    private final MidiReceiver[] mInputPortReceivers;
 
     // for polling multiple FileDescriptors for MIDI events
     private final StructPollfd[] mPollFDs;
@@ -50,12 +54,6 @@
     private final FileOutputStream[] mOutputStreams;
 
     public static UsbMidiDevice create(Context context, Bundle properties, int card, int device) {
-        MidiManager midiManager = (MidiManager)context.getSystemService(Context.MIDI_SERVICE);
-        if (midiManager == null) {
-            Log.e(TAG, "No MidiManager in UsbMidiDevice.create()");
-            return null;
-        }
-
         // FIXME - support devices with different number of input and output ports
         int subDevices = nativeGetSubdeviceCount(card, device);
         if (subDevices <= 0) {
@@ -70,19 +68,16 @@
             return null;
         }
 
-        MidiDeviceServer server = midiManager.createDeviceServer(subDevices, subDevices, properties,
-                false, MidiDeviceInfo.TYPE_USB);
-        if (server == null) {
+        UsbMidiDevice midiDevice = new UsbMidiDevice(fileDescriptors, fileDescriptors);
+        if (!midiDevice.register(context, properties)) {
+            IoUtils.closeQuietly(midiDevice);
             Log.e(TAG, "createDeviceServer failed");
             return null;
         }
-
-        return new UsbMidiDevice(server, fileDescriptors, fileDescriptors);
+        return midiDevice;
     }
 
-    private UsbMidiDevice(MidiDeviceServer server, FileDescriptor[] inputFiles,
-            FileDescriptor[] outputFiles) {
-        mServer = server;
+    private UsbMidiDevice(FileDescriptor[] inputFiles, FileDescriptor[] outputFiles) {
         int inputCount = inputFiles.length;
         int outputCount = outputFiles.length;
 
@@ -102,26 +97,36 @@
             mOutputStreams[i] = new FileOutputStream(outputFiles[i]);
         }
 
-        mOutputPortReceivers = new MidiReceiver[outputCount];
-        for (int port = 0; port < outputCount; port++) {
-            mOutputPortReceivers[port] = server.openOutputPortReceiver(port);
-        }
-
+        mInputPortReceivers = new MidiReceiver[inputCount];
         for (int port = 0; port < inputCount; port++) {
-            final int portNumberF = port;
-            MidiReceiver receiver = new MidiReceiver() {
-
+            final int portF = port;
+            mInputPortReceivers[port] = new MidiReceiver() {
                 @Override
                 public void post(byte[] data, int offset, int count, long timestamp)
                         throws IOException {
                     // FIXME - timestamps are ignored, future posting not supported yet.
-                    mOutputStreams[portNumberF].write(data, offset, count);
+                    mOutputStreams[portF].write(data, offset, count);
                 }
             };
-            MidiSender sender = server.openInputPortSender(port);
-            sender.connect(receiver);
+        }
+    }
+
+    private boolean register(Context context, Bundle properties) {
+        MidiManager midiManager = (MidiManager)context.getSystemService(Context.MIDI_SERVICE);
+        if (midiManager == null) {
+            Log.e(TAG, "No MidiManager in UsbMidiDevice.create()");
+            return false;
         }
 
+        int outputCount = mOutputStreams.length;
+        mServer = midiManager.createDeviceServer(mInputPortReceivers, outputCount,
+                properties, MidiDeviceInfo.TYPE_USB);
+        if (mServer == null) {
+            return false;
+        }
+        final MidiReceiver[] outputReceivers = mServer.getOutputPortReceivers();
+
+        // FIXME can we only run this when we have a dispatcher that has listeners?
         new Thread() {
             @Override
             public void run() {
@@ -137,8 +142,8 @@
                                 pfd.revents = 0;
 
                                 int count = mInputStreams[index].read(buffer);
-                                mOutputPortReceivers[index].post(buffer, 0, count,
-                                        System.nanoTime());
+                                long timestamp = System.nanoTime();
+                                outputReceivers[index].post(buffer, 0, count, timestamp);
                             } else if ((pfd.revents & (OsConstants.POLLERR
                                                         | OsConstants.POLLHUP)) != 0) {
                                 done = true;
@@ -155,11 +160,15 @@
                 }
             }
         }.start();
+
+        return true;
     }
 
     @Override
     public void close() throws IOException {
-        mServer.close();
+        if (mServer != null) {
+            mServer.close();
+        }
 
         for (int i = 0; i < mInputStreams.length; i++) {
             mInputStreams[i].close();