MIDI: API changes to support multiple ports per device

Add MidiInputPort and MidiOutputPort classes (with MidiPort base class)
A MidiDevice can now have multiple input and output ports.
Multiple ports are currently supported only for virtual devices, USB support coming later.

Change-Id: Ib55076aa1374aa46ae4ae76ad93bd717df6d7e21
diff --git a/core/java/android/midi/IMidiManager.aidl b/core/java/android/midi/IMidiManager.aidl
index 06b675b..7a9f887 100644
--- a/core/java/android/midi/IMidiManager.aidl
+++ b/core/java/android/midi/IMidiManager.aidl
@@ -37,7 +37,8 @@
     ParcelFileDescriptor openDevice(IBinder token, in MidiDeviceInfo device);
 
     // for implementing virtual MIDI devices
-    MidiDevice registerVirtualDevice(IBinder token, in Bundle properties);
+    MidiDevice registerVirtualDevice(IBinder token, int numInputPorts, int numOutputPorts,
+            in Bundle properties);
     void unregisterVirtualDevice(IBinder token, in MidiDeviceInfo device);
 
     // for use by UsbAudioManager
diff --git a/core/java/android/midi/MidiDevice.java b/core/java/android/midi/MidiDevice.java
index 8954d2b..e704ea0 100644
--- a/core/java/android/midi/MidiDevice.java
+++ b/core/java/android/midi/MidiDevice.java
@@ -28,7 +28,7 @@
 import java.util.ArrayList;
 
 /**
- * This class is used for sending and receiving data to and from an midi device
+ * This class is used for sending and receiving data to and from an MIDI device
  * Instances of this class are created by {@link MidiManager#openDevice}.
  * This class can also be used to provide the implementation for a virtual device.
  *
@@ -44,21 +44,29 @@
     private ParcelFileDescriptor mParcelFileDescriptor;
     private FileInputStream mInputStream;
     private FileOutputStream mOutputStream;
-    private final ArrayList<MidiReceiver> mReceivers = new ArrayList<MidiReceiver>();
+
+    // lazily populated lists of ports
+    private final MidiInputPort[] mInputPorts;
+    private final MidiOutputPort[] mOutputPorts;
+
+    // array of receiver lists, indexed by port number
+    private final ArrayList<MidiReceiver>[] mReceivers;
+
+    private int mReceiverCount; // total number of receivers for all ports
 
     /**
      * Minimum size of packed message as sent through our ParcelFileDescriptor
-     * 8 bytes for timestamp and 1 to 3 bytes for message
+     * 8 bytes for timestamp, 1 byte for port number and 1 to 3 bytes for message
      * @hide
      */
-    public static final int MIN_PACKED_MESSAGE_SIZE = 9;
+    public static final int MIN_PACKED_MESSAGE_SIZE = 10;
 
     /**
      * Maximum size of packed message as sent through our ParcelFileDescriptor
-     * 8 bytes for timestamp and 1 to 3 bytes for message
+     * 8 bytes for timestamp, 1 byte for port number and 1 to 3 bytes for message
      * @hide
      */
-    public static final int MAX_PACKED_MESSAGE_SIZE = 11;
+    public static final int MAX_PACKED_MESSAGE_SIZE = 12;
 
     // This thread reads MIDI events from a socket and distributes them to the list of
     // MidiReceivers attached to this device.
@@ -80,27 +88,35 @@
                     int offset = getMessageOffset(buffer, count);
                     int size = getMessageSize(buffer, count);
                     long timestamp = getMessageTimeStamp(buffer, count);
+                    int port = getMessagePortNumber(buffer, count);
 
                     synchronized (mReceivers) {
-                        for (int i = 0; i < mReceivers.size(); i++) {
-                            MidiReceiver receiver = mReceivers.get(i);
-                            try {
-                                mReceivers.get(i).onPost(buffer, offset, size, timestamp);
-                            } catch (IOException e) {
-                                Log.e(TAG, "post failed");
-                                deadReceivers.add(receiver);
+                        ArrayList<MidiReceiver> receivers = mReceivers[port];
+                        if (receivers != null) {
+                            for (int i = 0; i < receivers.size(); i++) {
+                                MidiReceiver receiver = receivers.get(i);
+                                try {
+                                    receivers.get(i).onPost(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);
+                            // remove any receivers that failed
+                            if (deadReceivers.size() > 0) {
+                                for (MidiReceiver receiver: deadReceivers) {
+                                    receivers.remove(receiver);
+                                    mReceiverCount--;
+                                }
+                                deadReceivers.clear();
                             }
-                            deadReceivers.clear();
-                        }
-                        // exit if we have no receivers left
-                        if (mReceivers.size() == 0) {
-                            break;
+                            if (receivers.size() == 0) {
+                                mReceivers[port] = null;
+                            }
+                            // exit if we have no receivers left
+                            if (mReceiverCount == 0) {
+                                break;
+                            }
                         }
                     }
                 }
@@ -110,38 +126,6 @@
         }
     };
 
-    // This is the receiver that clients use for sending events to this device.
-    private final MidiReceiver mReceiver = new MidiReceiver() {
-        private final byte[] mBuffer = new byte[MAX_PACKED_MESSAGE_SIZE];
-        public void onPost(byte[] msg, int offset, int count, long timestamp) throws IOException {
-            synchronized (mBuffer) {
-                int length = packMessage(msg, offset, count, timestamp, mBuffer);
-                mOutputStream.write(mBuffer, 0, length);
-            }
-        }
-    };
-
-    // Our MidiSender object, to which clients can attach MidiReceivers.
-    private final MidiSender mSender = new MidiSender() {
-        public void connect(MidiReceiver receiver) {
-            synchronized (mReceivers) {
-                if (mReceivers.size() == 0) {
-                    mThread.start();
-                }
-                mReceivers.add(receiver);
-            }
-        }
-
-        public void disconnect(MidiReceiver receiver) {
-            synchronized (mReceivers) {
-                mReceivers.remove(receiver);
-                if (mReceivers.size() == 0) {
-                    // ???
-                }
-            }
-        }
-    };
-
    /**
      * MidiDevice should only be instantiated by MidiManager or MidiService
      * @hide
@@ -149,9 +133,59 @@
     public MidiDevice(MidiDeviceInfo deviceInfo, ParcelFileDescriptor pfd) {
         mDeviceInfo = deviceInfo;
         mParcelFileDescriptor = pfd;
+        int inputPorts = deviceInfo.getInputPortCount();
+        int outputPorts = deviceInfo.getOutputPortCount();
+        mInputPorts = new MidiInputPort[inputPorts];
+        mOutputPorts = new MidiOutputPort[outputPorts];
+        mReceivers = new ArrayList[outputPorts];
     }
 
-    public boolean open() {
+    public MidiInputPort openInputPort(int portNumber) {
+        if (portNumber < 0 || portNumber >= mDeviceInfo.getInputPortCount()) {
+            throw new IllegalArgumentException("input port number out of range");
+        }
+        synchronized (mInputPorts) {
+            if (mInputPorts[portNumber] == null) {
+                mInputPorts[portNumber] = new MidiInputPort(mOutputStream, portNumber);
+            }
+            return mInputPorts[portNumber];
+        }
+    }
+
+    public MidiOutputPort openOutputPort(int portNumber) {
+        if (portNumber < 0 || portNumber >= mDeviceInfo.getOutputPortCount()) {
+            throw new IllegalArgumentException("output port number out of range");
+        }
+        synchronized (mOutputPorts) {
+            if (mOutputPorts[portNumber] == null) {
+                mOutputPorts[portNumber] = new MidiOutputPort(this, portNumber);
+            }
+            return mOutputPorts[portNumber];
+        }
+    }
+
+    /* package */ void connect(MidiReceiver receiver, int portNumber) {
+        synchronized (mReceivers) {
+            if (mReceivers[portNumber] == null) {
+                mReceivers[portNumber] = new  ArrayList<MidiReceiver>();
+            }
+            mReceivers[portNumber].add(receiver);
+            if (mReceiverCount++ == 0) {
+                mThread.start();
+            }
+        }
+    }
+
+    /* package */ void disconnect(MidiReceiver receiver, int portNumber) {
+        synchronized (mReceivers) {
+            ArrayList<MidiReceiver> receivers = mReceivers[portNumber];
+            if (receivers != null && receivers.remove(receiver)) {
+                mReceiverCount--;
+            }
+        }
+    }
+
+    /* package */ boolean open() {
         FileDescriptor fd = mParcelFileDescriptor.getFileDescriptor();
         try {
             mInputStream = new FileInputStream(fd);
@@ -187,16 +221,6 @@
         return mDeviceInfo;
     }
 
-    // returns our MidiReceiver, which clients can use for sending events to this device.
-    public MidiReceiver getReceiver() {
-        return mReceiver;
-    }
-
-    // Returns our MidiSender object, to which clients can attach MidiReceivers.
-    public MidiSender getSender() {
-        return mSender;
-    }
-
     @Override
     public String toString() {
         return ("MidiDevice: " + mDeviceInfo.toString() + " fd: " + mParcelFileDescriptor);
@@ -236,7 +260,7 @@
      * @hide
      */
     public static int packMessage(byte[] message, int offset, int size, long timestamp,
-            byte[] dest) {
+            int portNumber, byte[] dest) {
         // pack variable length message first
         System.arraycopy(message, offset, dest, 0, size);
         int destOffset = size;
@@ -245,6 +269,9 @@
             dest[destOffset++] = (byte)timestamp;
             timestamp >>= 8;
         }
+        // portNumber is last
+        dest[destOffset++] = (byte)portNumber;
+
         return destOffset;
     }
 
@@ -266,8 +293,8 @@
      * @hide
      */
     public static int getMessageSize(byte[] buffer, int bufferLength) {
-        // message length is total buffer length minus size of the timestamp
-        return bufferLength - 8;
+        // message length is total buffer length minus size of the timestamp and port number
+        return bufferLength - 9 /* (sizeof(timestamp) + sizeof(portNumber)) */;
     }
 
     /**
@@ -289,4 +316,16 @@
         }
         return timestamp;
      }
+
+    /**
+     * Utility function for unpacking a MIDI message to be sent through our ParcelFileDescriptor
+     * unpacks port number from packed buffer
+     *
+     * @hide
+     */
+    public static int getMessagePortNumber(byte[] buffer, int bufferLength) {
+        // timestamp follows variable length message data and timestamp
+        int dataLength = getMessageSize(buffer, bufferLength);
+        return buffer[dataLength + 8 /* sizeof(timestamp) */];
+     }
 }
diff --git a/core/java/android/midi/MidiDeviceInfo.java b/core/java/android/midi/MidiDeviceInfo.java
index def6878c..239481b 100644
--- a/core/java/android/midi/MidiDeviceInfo.java
+++ b/core/java/android/midi/MidiDeviceInfo.java
@@ -39,6 +39,8 @@
 
     private final int mType;    // USB or virtual
     private final int mId;  // unique ID generated by MidiService
+    private final int mInputPortCount;
+    private final int mOutputPortCount;
     private final Bundle mProperties;
 
     // used for USB devices only
@@ -77,9 +79,12 @@
      * MidiDeviceInfo should only be instantiated by MidiService implementation
      * @hide
      */
-    public MidiDeviceInfo(int type, int id, Bundle properties) {
+    public MidiDeviceInfo(int type, int id, int numInputPorts, int numOutputPorts,
+            Bundle properties) {
         mType = type;
         mId = id;
+        mInputPortCount = numInputPorts;
+        mOutputPortCount = numOutputPorts;
         mProperties = properties;
         mAlsaCard = -1;
         mAlsaDevice = -1;
@@ -89,10 +94,12 @@
      * MidiDeviceInfo should only be instantiated by MidiService implementation
      * @hide
      */
-    public MidiDeviceInfo(int type, int id, Bundle properties,
-            int alsaCard, int alsaDevice) {
+    public MidiDeviceInfo(int type, int id, int numInputPorts, int numOutputPorts,
+            Bundle properties, int alsaCard, int alsaDevice) {
         mType = type;
         mId = id;
+        mInputPortCount = numInputPorts;
+        mOutputPortCount = numOutputPorts;
         mProperties = properties;
         mAlsaCard = alsaCard;
         mAlsaDevice = alsaDevice;
@@ -118,6 +125,24 @@
     }
 
     /**
+     * Returns the device's number of input ports.
+     *
+     * @return the number of input ports
+     */
+    public int getInputPortCount() {
+        return mInputPortCount;
+    }
+
+    /**
+     * Returns the device's number of output ports.
+     *
+     * @return the number of output ports
+     */
+    public int getOutputPortCount() {
+        return mOutputPortCount;
+    }
+
+    /**
      * Returns the {@link android.os.Bundle} containing the device's properties.
      *
      * @return the device's properties
@@ -157,7 +182,8 @@
     @Override
     public String toString() {
         return ("MidiDeviceInfo[mType=" + mType +
-                ",mId=" + mId +
+                ",mInputPortCount=" + mInputPortCount +
+                ",mOutputPortCount=" + mOutputPortCount +
                 ",mProperties=" + mProperties +
                 ",mAlsaCard=" + mAlsaCard +
                 ",mAlsaDevice=" + mAlsaDevice);
@@ -168,10 +194,12 @@
         public MidiDeviceInfo createFromParcel(Parcel in) {
             int type = in.readInt();
             int id = in.readInt();
+            int inputPorts = in.readInt();
+            int outputPorts = in.readInt();
             Bundle properties = in.readBundle();
             int card = in.readInt();
             int device = in.readInt();
-            return new MidiDeviceInfo(type, id, properties, card, device);
+            return new MidiDeviceInfo(type, id, inputPorts, outputPorts, properties, card, device);
         }
 
         public MidiDeviceInfo[] newArray(int size) {
@@ -186,6 +214,8 @@
     public void writeToParcel(Parcel parcel, int flags) {
         parcel.writeInt(mType);
         parcel.writeInt(mId);
+        parcel.writeInt(mInputPortCount);
+        parcel.writeInt(mOutputPortCount);
         parcel.writeBundle(mProperties);
         parcel.writeInt(mAlsaCard);
         parcel.writeInt(mAlsaDevice);
diff --git a/core/java/android/midi/MidiInputPort.java b/core/java/android/midi/MidiInputPort.java
new file mode 100644
index 0000000..583c367
--- /dev/null
+++ b/core/java/android/midi/MidiInputPort.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2014 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.midi;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+/**
+ * This class is used for sending data to a port on a MIDI device
+ *
+ * @hide
+ */
+public final class MidiInputPort extends MidiPort implements MidiReceiver {
+
+    private final FileOutputStream mOutputStream;
+    // buffer to use for sending messages out our output stream
+    private final byte[] mBuffer = new byte[MidiDevice.MAX_PACKED_MESSAGE_SIZE];
+
+  /* package */ MidiInputPort(FileOutputStream outputStream, int portNumber) {
+        super(portNumber);
+        mOutputStream = outputStream;
+    }
+
+    /**
+     * Writes a MIDI message to the input port
+     *
+     * @param msg message bytes
+     * @param offset offset of first byte of the message in msg array
+     * @param count size of the message in bytes
+     * @param timestamp future time to post the message
+     */
+    public void onPost(byte[] msg, int offset, int count, long timestamp) throws IOException {
+        synchronized (mBuffer) {
+            int length = MidiDevice.packMessage(msg, offset, count, timestamp, mPortNumber,
+                    mBuffer);
+            mOutputStream.write(mBuffer, 0, length);
+        }
+    }
+}
diff --git a/core/java/android/midi/MidiManager.java b/core/java/android/midi/MidiManager.java
index ec869b7..f4d1918 100644
--- a/core/java/android/midi/MidiManager.java
+++ b/core/java/android/midi/MidiManager.java
@@ -132,9 +132,11 @@
 
     // Use this if you want to register and implement a virtual device.
     // The MidiDevice returned by this method is the proxy you use to implement the device.
-    public MidiDevice createVirtualDevice(Bundle properties) {
+    public MidiDevice createVirtualDevice(int numInputPorts, int numOutputPorts,
+            Bundle properties) {
         try {
-            MidiDevice device = mService.registerVirtualDevice(mToken, properties);
+            MidiDevice device = mService.registerVirtualDevice(mToken,
+                    numInputPorts, numOutputPorts, properties);
             if (device != null && !device.open()) {
                 device = null;
             }
diff --git a/core/java/android/midi/MidiOutputPort.java b/core/java/android/midi/MidiOutputPort.java
new file mode 100644
index 0000000..69a33cb
--- /dev/null
+++ b/core/java/android/midi/MidiOutputPort.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2014 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.midi;
+
+import java.io.FileInputStream;
+
+/**
+ * This class is used for receiving data to a port on a MIDI device
+ *
+ * @hide
+ */
+public final class MidiOutputPort extends MidiPort implements MidiSender {
+
+    private final MidiDevice mDevice;
+
+  /* package */ MidiOutputPort(MidiDevice device, int portNumber) {
+        super(portNumber);
+        mDevice = device;
+    }
+
+    public void connect(MidiReceiver receiver) {
+        mDevice.connect(receiver, mPortNumber);
+    }
+
+    public void disconnect(MidiReceiver receiver) {
+        mDevice.disconnect(receiver, mPortNumber);
+    }
+}
diff --git a/core/java/android/midi/MidiPort.java b/core/java/android/midi/MidiPort.java
new file mode 100644
index 0000000..e94f62d
--- /dev/null
+++ b/core/java/android/midi/MidiPort.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2014 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.midi;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+/**
+ * This class represents a MIDI input or output port
+ *
+ * @hide
+ */
+public class MidiPort {
+
+    protected final int mPortNumber;
+
+  /* package */ MidiPort(int portNumber) {
+        mPortNumber = portNumber;
+    }
+
+    /**
+     * Returns the port number of this port
+     *
+     * @return the port's port number
+     */
+    public int getPortNumber() {
+        return mPortNumber;
+    }
+}
diff --git a/services/core/java/com/android/server/midi/MidiService.java b/services/core/java/com/android/server/midi/MidiService.java
index ff8dda0..29a8339 100644
--- a/services/core/java/com/android/server/midi/MidiService.java
+++ b/services/core/java/com/android/server/midi/MidiService.java
@@ -172,7 +172,8 @@
         return device.getFileDescriptor();
     }
 
-    public MidiDevice registerVirtualDevice(IBinder token, Bundle properties) {
+    public MidiDevice registerVirtualDevice(IBinder token, int numInputPorts, int numOutputPorts,
+            Bundle properties) {
         VirtualMidiDevice device;
         Client client = getClient(token);
         if (client == null) return null;
@@ -180,7 +181,7 @@
         synchronized (mDevices) {
             int id = mNextDeviceId++;
             MidiDeviceInfo deviceInfo = new MidiDeviceInfo(MidiDeviceInfo.TYPE_VIRTUAL, id,
-                    properties);
+                    numInputPorts, numOutputPorts, properties);
 
             device = new VirtualMidiDevice(deviceInfo);
             if (!device.open()) {
@@ -238,7 +239,12 @@
                     usbDevice.getSerialNumber());
             properties.putParcelable(MidiDeviceInfo.PROPERTY_USB_DEVICE, usbDevice);
 
-            deviceInfo = new MidiDeviceInfo(MidiDeviceInfo.TYPE_USB, id, properties, card, device);
+            // FIXME - multiple ports not supported yet
+            int inputPorts = 1;
+            int outputPorts = 1;
+
+            deviceInfo = new MidiDeviceInfo(MidiDeviceInfo.TYPE_USB, id, inputPorts, outputPorts,
+                    properties, card, device);
             UsbMidiDevice midiDevice = new UsbMidiDevice(deviceInfo);
             mDevices.put(id, midiDevice);
             mUsbDevices.put(usbDevice, midiDevice);
diff --git a/services/core/java/com/android/server/midi/UsbMidiDevice.java b/services/core/java/com/android/server/midi/UsbMidiDevice.java
index 1bc91f05..3d42c67 100644
--- a/services/core/java/com/android/server/midi/UsbMidiDevice.java
+++ b/services/core/java/com/android/server/midi/UsbMidiDevice.java
@@ -100,13 +100,16 @@
                 return -1;
             }
         }
-        return MidiDevice.packMessage(mBuffer, 0, dataSize + 1, System.nanoTime(), buffer);
+        return MidiDevice.packMessage(mBuffer, 0, dataSize + 1, System.nanoTime(),
+            0, // FIXME - multiple ports not supported yet
+            buffer);
     }
 
     // writes a message to the ALSA driver
     void writeMessage(byte[] buffer, int count) throws IOException {
         int offset = MidiDevice.getMessageOffset(buffer, count);
         int size = MidiDevice.getMessageSize(buffer, count);
+        // FIXME - multiple ports not supported yet
         mOutputStream.write(buffer, offset, count);
     }
 }