1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.media.midi;
18 
19 import android.os.Binder;
20 import android.os.IBinder;
21 import android.os.Process;
22 import android.os.RemoteException;
23 import android.util.Log;
24 
25 import dalvik.system.CloseGuard;
26 
27 import libcore.io.IoUtils;
28 
29 import java.io.Closeable;
30 import java.io.FileDescriptor;
31 import java.io.IOException;
32 
33 import java.util.HashSet;
34 
35 /**
36  * This class is used for sending and receiving data to and from a MIDI device
37  * Instances of this class are created by {@link MidiManager#openDevice}.
38  */
39 public final class MidiDevice implements Closeable {
40     private static final String TAG = "MidiDevice";
41 
42     private final MidiDeviceInfo mDeviceInfo;    // accessed from native code
43     private final IMidiDeviceServer mDeviceServer;
44     private final IBinder mDeviceServerBinder;    // accessed from native code
45     private final IMidiManager mMidiManager;
46     private final IBinder mClientToken;
47     private final IBinder mDeviceToken;
48     private boolean mIsDeviceClosed;    // accessed from native code
49 
50     private long mNativeHandle;    // accessed from native code
51 
52     private final CloseGuard mGuard = CloseGuard.get();
53 
54     /**
55      * This class represents a connection between the output port of one device
56      * and the input port of another. Created by {@link #connectPorts}.
57      * Close this object to terminate the connection.
58      */
59     public class MidiConnection implements Closeable {
60         private final IMidiDeviceServer mInputPortDeviceServer;
61         private final IBinder mInputPortToken;
62         private final IBinder mOutputPortToken;
63         private final CloseGuard mGuard = CloseGuard.get();
64         private boolean mIsClosed;
65 
MidiConnection(IBinder outputPortToken, MidiInputPort inputPort)66         MidiConnection(IBinder outputPortToken, MidiInputPort inputPort) {
67             mInputPortDeviceServer = inputPort.getDeviceServer();
68             mInputPortToken = inputPort.getToken();
69             mOutputPortToken = outputPortToken;
70             mGuard.open("close");
71         }
72 
73         @Override
close()74         public void close() throws IOException {
75             synchronized (mGuard) {
76                 if (mIsClosed) return;
77                 mGuard.close();
78                 try {
79                     // close input port
80                     mInputPortDeviceServer.closePort(mInputPortToken);
81                     // close output port
82                     mDeviceServer.closePort(mOutputPortToken);
83                 } catch (RemoteException e) {
84                     Log.e(TAG, "RemoteException in MidiConnection.close");
85                 }
86                 mIsClosed = true;
87             }
88         }
89 
90         @Override
finalize()91         protected void finalize() throws Throwable {
92             try {
93                 if (mGuard != null) {
94                     mGuard.warnIfOpen();
95                 }
96 
97                 close();
98             } finally {
99                 super.finalize();
100             }
101         }
102     }
103 
MidiDevice(MidiDeviceInfo deviceInfo, IMidiDeviceServer server, IMidiManager midiManager, IBinder clientToken, IBinder deviceToken)104     /* package */ MidiDevice(MidiDeviceInfo deviceInfo, IMidiDeviceServer server,
105             IMidiManager midiManager, IBinder clientToken, IBinder deviceToken) {
106         mDeviceInfo = deviceInfo;
107         mDeviceServer = server;
108         mDeviceServerBinder = mDeviceServer.asBinder();
109         mMidiManager = midiManager;
110         mClientToken = clientToken;
111         mDeviceToken = deviceToken;
112         mGuard.open("close");
113     }
114 
115     /**
116      * Returns a {@link MidiDeviceInfo} object, which describes this device.
117      *
118      * @return the {@link MidiDeviceInfo} object
119      */
getInfo()120     public MidiDeviceInfo getInfo() {
121         return mDeviceInfo;
122     }
123 
124     /**
125      * Called to open a {@link MidiInputPort} for the specified port number.
126      *
127      * An input port can only be used by one sender at a time.
128      * Opening an input port will fail if another application has already opened it for use.
129      * A {@link MidiDeviceStatus} can be used to determine if an input port is already open.
130      *
131      * @param portNumber the number of the input port to open
132      * @return the {@link MidiInputPort} if the open is successful,
133      *         or null in case of failure.
134      */
openInputPort(int portNumber)135     public MidiInputPort openInputPort(int portNumber) {
136         if (mIsDeviceClosed) {
137             return null;
138         }
139         try {
140             IBinder token = new Binder();
141             FileDescriptor fd = mDeviceServer.openInputPort(token, portNumber);
142             if (fd == null) {
143                 return null;
144             }
145             return new MidiInputPort(mDeviceServer, token, fd, portNumber);
146         } catch (RemoteException e) {
147             Log.e(TAG, "RemoteException in openInputPort");
148             return null;
149         }
150     }
151 
152     /**
153      * Called to open a {@link MidiOutputPort} for the specified port number.
154      *
155      * An output port may be opened by multiple applications.
156      *
157      * @param portNumber the number of the output port to open
158      * @return the {@link MidiOutputPort} if the open is successful,
159      *         or null in case of failure.
160      */
openOutputPort(int portNumber)161     public MidiOutputPort openOutputPort(int portNumber) {
162         if (mIsDeviceClosed) {
163             return null;
164         }
165         try {
166             IBinder token = new Binder();
167             FileDescriptor fd = mDeviceServer.openOutputPort(token, portNumber);
168             if (fd == null) {
169                 return null;
170             }
171             return new MidiOutputPort(mDeviceServer, token, fd, portNumber);
172         } catch (RemoteException e) {
173             Log.e(TAG, "RemoteException in openOutputPort");
174             return null;
175         }
176     }
177 
178     /**
179      * Connects the supplied {@link MidiInputPort} to the output port of this device
180      * with the specified port number. Once the connection is made, the MidiInput port instance
181      * can no longer receive data via its {@link MidiReceiver#onSend} method.
182      * This method returns a {@link MidiDevice.MidiConnection} object, which can be used
183      * to close the connection.
184      *
185      * @param inputPort the inputPort to connect
186      * @param outputPortNumber the port number of the output port to connect inputPort to.
187      * @return {@link MidiDevice.MidiConnection} object if the connection is successful,
188      *         or null in case of failure.
189      */
connectPorts(MidiInputPort inputPort, int outputPortNumber)190     public MidiConnection connectPorts(MidiInputPort inputPort, int outputPortNumber) {
191         if (outputPortNumber < 0 || outputPortNumber >= mDeviceInfo.getOutputPortCount()) {
192             throw new IllegalArgumentException("outputPortNumber out of range");
193         }
194         if (mIsDeviceClosed) {
195             return null;
196         }
197 
198         FileDescriptor fd = inputPort.claimFileDescriptor();
199         if (fd == null) {
200             return null;
201         }
202         try {
203             IBinder token = new Binder();
204             int calleePid = mDeviceServer.connectPorts(token, fd, outputPortNumber);
205             // If the service is a different Process then it will duplicate the fd
206             // and we can safely close this one.
207             // But if the service is in the same Process then closing the fd will
208             // kill the connection. So don't do that.
209             if (calleePid != Process.myPid()) {
210                 // close our copy of the file descriptor
211                 IoUtils.closeQuietly(fd);
212             }
213 
214             return new MidiConnection(token, inputPort);
215         } catch (RemoteException e) {
216             Log.e(TAG, "RemoteException in connectPorts");
217             return null;
218         }
219     }
220 
221     @Override
close()222     public void close() throws IOException {
223         synchronized (mGuard) {
224             // What if there is a native reference to this?
225             if (mNativeHandle != 0) {
226                 Log.w(TAG, "MidiDevice#close() called while there is an outstanding native client 0x"
227                            + Long.toHexString(mNativeHandle));
228             }
229             if (!mIsDeviceClosed && mNativeHandle == 0) {
230                 mGuard.close();
231                 mIsDeviceClosed = true;
232                 try {
233                     mMidiManager.closeDevice(mClientToken, mDeviceToken);
234                 } catch (RemoteException e) {
235                     Log.e(TAG, "RemoteException in closeDevice");
236                 }
237             }
238         }
239     }
240 
241     @Override
finalize()242     protected void finalize() throws Throwable {
243         try {
244             if (mGuard != null) {
245                 mGuard.warnIfOpen();
246             }
247 
248             close();
249         } finally {
250             super.finalize();
251         }
252     }
253 
254     @Override
toString()255     public String toString() {
256         return ("MidiDevice: " + mDeviceInfo.toString());
257     }
258 }
259