MidiDevice: Add support for making direct connections between ports

The output port of one device can be connected to the input port of another
device using the new MidiDevice.connectPorts() method.
This allows an application to direct the output of one device directly
to the input port of another without having to copy data from one to another.

Change-Id: I4d361c4e0950b9b9516b0c2f0c158677b1aca208
diff --git a/media/java/android/media/midi/IMidiDeviceServer.aidl b/media/java/android/media/midi/IMidiDeviceServer.aidl
index 3331aae..642078a 100644
--- a/media/java/android/media/midi/IMidiDeviceServer.aidl
+++ b/media/java/android/media/midi/IMidiDeviceServer.aidl
@@ -24,4 +24,7 @@
     ParcelFileDescriptor openInputPort(IBinder token, int portNumber);
     ParcelFileDescriptor openOutputPort(IBinder token, int portNumber);
     void closePort(IBinder token);
+
+    // 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/MidiDevice.java b/media/java/android/media/midi/MidiDevice.java
index af0737d..569f7c6 100644
--- a/media/java/android/media/midi/MidiDevice.java
+++ b/media/java/android/media/midi/MidiDevice.java
@@ -26,6 +26,8 @@
 
 import dalvik.system.CloseGuard;
 
+import libcore.io.IoUtils;
+
 import java.io.Closeable;
 import java.io.IOException;
 
@@ -44,8 +46,29 @@
     private Context mContext;
     private ServiceConnection mServiceConnection;
 
+
     private final CloseGuard mGuard = CloseGuard.get();
 
+    public class MidiConnection implements Closeable {
+        private final IBinder mToken;
+        private final MidiInputPort mInputPort;
+
+        MidiConnection(IBinder token, MidiInputPort inputPort) {
+            mToken = token;
+            mInputPort = inputPort;
+        }
+
+        @Override
+        public void close() throws IOException {
+            try {
+                mDeviceServer.closePort(mToken);
+                IoUtils.closeQuietly(mInputPort);
+            } catch (RemoteException e) {
+                Log.e(TAG, "RemoteException in MidiConnection.close");
+            }
+        }
+    }
+
     /* package */ MidiDevice(MidiDeviceInfo deviceInfo, IMidiDeviceServer server) {
         this(deviceInfo, server, null, null);
     }
@@ -108,6 +131,36 @@
         }
     }
 
+    /**
+     * Connects the supplied {@link MidiInputPort} to the output port of this device
+     * with the specified port number. Once the connection is made, the MidiInput port instance
+     * can no longer receive data via its {@link MidiReciever.receive} method.
+     * This method returns a {@link #MidiConnection} object, which can be used to close the connection
+     * @param inputPort the inputPort to connect
+     * @param outputPortNumber the port number of the output port to connect inputPort to.
+     * @return {@link #MidiConnection} object if the connection is successful, or null in case of failure
+     */
+    public MidiConnection connectPorts(MidiInputPort inputPort, int outputPortNumber) {
+        if (outputPortNumber < 0 || outputPortNumber >= mDeviceInfo.getOutputPortCount()) {
+            throw new IllegalArgumentException("outputPortNumber out of range");
+        }
+
+        ParcelFileDescriptor pfd = inputPort.claimFileDescriptor();
+        if (pfd == null) {
+            return null;
+        }
+         try {
+            IBinder token = new Binder();
+            mDeviceServer.connectPorts(token, pfd, outputPortNumber);
+            // close our copy of the file descriptor
+            IoUtils.closeQuietly(pfd);
+            return new MidiConnection(token, inputPort);
+        } catch (RemoteException e) {
+            Log.e(TAG, "RemoteException in connectPorts");
+            return null;
+        }
+    }
+
     @Override
     public void close() throws IOException {
         synchronized (mGuard) {
diff --git a/media/java/android/media/midi/MidiDeviceServer.java b/media/java/android/media/midi/MidiDeviceServer.java
index 3b4b6f0..6531052 100644
--- a/media/java/android/media/midi/MidiDeviceServer.java
+++ b/media/java/android/media/midi/MidiDeviceServer.java
@@ -200,6 +200,18 @@
                 }
             }
         }
+
+        @Override
+        public void connectPorts(IBinder token, ParcelFileDescriptor pfd,
+                int outputPortNumber) {
+            MidiInputPort inputPort = new MidiInputPort(pfd, outputPortNumber);
+            mOutputPortDispatchers[outputPortNumber].getSender().connect(inputPort);
+            mInputPorts.add(inputPort);
+            OutputPortClient client = new OutputPortClient(token, inputPort);
+            synchronized (mPortClients) {
+                mPortClients.put(token, client);
+            }
+        }
     };
 
     /* package */ MidiDeviceServer(IMidiManager midiManager, MidiReceiver[] inputPortReceivers,
diff --git a/media/java/android/media/midi/MidiInputPort.java b/media/java/android/media/midi/MidiInputPort.java
index 74e1fa4..752075e1 100644
--- a/media/java/android/media/midi/MidiInputPort.java
+++ b/media/java/android/media/midi/MidiInputPort.java
@@ -41,7 +41,8 @@
     private IMidiDeviceServer mDeviceServer;
     private final IBinder mToken;
     private final int mPortNumber;
-    private final FileOutputStream mOutputStream;
+    private ParcelFileDescriptor mParcelFileDescriptor;
+    private FileOutputStream mOutputStream;
 
     private final CloseGuard mGuard = CloseGuard.get();
     private boolean mIsClosed;
@@ -53,8 +54,9 @@
             ParcelFileDescriptor pfd, int portNumber) {
         mDeviceServer = server;
         mToken = token;
+        mParcelFileDescriptor = pfd;
         mPortNumber = portNumber;
-        mOutputStream = new ParcelFileDescriptor.AutoCloseOutputStream(pfd);
+        mOutputStream = new FileOutputStream(pfd.getFileDescriptor());
         mGuard.open("close");
     }
 
@@ -89,11 +91,27 @@
         }
 
         synchronized (mBuffer) {
+            if (mOutputStream == null) {
+                throw new IOException("MidiInputPort is closed");
+            }
             int length = MidiPortImpl.packMessage(msg, offset, count, timestamp, mBuffer);
             mOutputStream.write(mBuffer, 0, length);
         }
     }
 
+    // used by MidiDevice.connectInputPort() to connect our socket directly to another device
+    /* package */ ParcelFileDescriptor claimFileDescriptor() {
+        synchronized (mBuffer) {
+            ParcelFileDescriptor pfd = mParcelFileDescriptor;
+            if (pfd != null) {
+                IoUtils.closeQuietly(mOutputStream);
+                mParcelFileDescriptor = null;
+                mOutputStream = null;
+            }
+            return pfd;
+        }
+    }
+
     @Override
     public int getMaxMessageSize() {
         return MidiPortImpl.MAX_PACKET_DATA_SIZE;
@@ -104,7 +122,16 @@
         synchronized (mGuard) {
             if (mIsClosed) return;
             mGuard.close();
-            mOutputStream.close();
+            synchronized (mBuffer) {
+                if (mParcelFileDescriptor != null) {
+                    mParcelFileDescriptor.close();
+                    mParcelFileDescriptor = null;
+                }
+                if (mOutputStream != null) {
+                    mOutputStream.close();
+                    mOutputStream = null;
+                }
+            }
             if (mDeviceServer != null) {
                 try {
                     mDeviceServer.closePort(mToken);