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
17package android.media.midi;
18
19import android.os.Binder;
20import android.os.IBinder;
21import android.os.Process;
22import android.os.RemoteException;
23import android.util.Log;
24
25import dalvik.system.CloseGuard;
26
27import libcore.io.IoUtils;
28
29import java.io.Closeable;
30import java.io.FileDescriptor;
31import java.io.IOException;
32
33import 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 */
39public final class MidiDevice implements Closeable {
40    static {
41        System.loadLibrary("media_jni");
42    }
43
44    private static final String TAG = "MidiDevice";
45
46    private final MidiDeviceInfo mDeviceInfo;
47    private final IMidiDeviceServer mDeviceServer;
48    private final IMidiManager mMidiManager;
49    private final IBinder mClientToken;
50    private final IBinder mDeviceToken;
51    private boolean mIsDeviceClosed;
52
53    // Native API Helpers
54    /**
55     * Keep a static list of MidiDevice objects that are mirrorToNative()'d so they
56     * don't get inadvertantly garbage collected.
57     */
58    private static HashSet<MidiDevice> mMirroredDevices = new HashSet<MidiDevice>();
59
60    /**
61     * If this device is mirrorToNatived(), this is the native device handler.
62     */
63    private long mNativeHandle;
64
65    private final CloseGuard mGuard = CloseGuard.get();
66
67    /**
68     * This class represents a connection between the output port of one device
69     * and the input port of another. Created by {@link #connectPorts}.
70     * Close this object to terminate the connection.
71     */
72    public class MidiConnection implements Closeable {
73        private final IMidiDeviceServer mInputPortDeviceServer;
74        private final IBinder mInputPortToken;
75        private final IBinder mOutputPortToken;
76        private final CloseGuard mGuard = CloseGuard.get();
77        private boolean mIsClosed;
78
79        MidiConnection(IBinder outputPortToken, MidiInputPort inputPort) {
80            mInputPortDeviceServer = inputPort.getDeviceServer();
81            mInputPortToken = inputPort.getToken();
82            mOutputPortToken = outputPortToken;
83            mGuard.open("close");
84        }
85
86        @Override
87        public void close() throws IOException {
88            synchronized (mGuard) {
89                if (mIsClosed) return;
90                mGuard.close();
91                try {
92                    // close input port
93                    mInputPortDeviceServer.closePort(mInputPortToken);
94                    // close output port
95                    mDeviceServer.closePort(mOutputPortToken);
96                } catch (RemoteException e) {
97                    Log.e(TAG, "RemoteException in MidiConnection.close");
98                }
99                mIsClosed = true;
100            }
101        }
102
103        @Override
104        protected void finalize() throws Throwable {
105            try {
106                if (mGuard != null) {
107                    mGuard.warnIfOpen();
108                }
109
110                close();
111            } finally {
112                super.finalize();
113            }
114        }
115    }
116
117    /* package */ MidiDevice(MidiDeviceInfo deviceInfo, IMidiDeviceServer server,
118            IMidiManager midiManager, IBinder clientToken, IBinder deviceToken) {
119        mDeviceInfo = deviceInfo;
120        mDeviceServer = server;
121        mMidiManager = midiManager;
122        mClientToken = clientToken;
123        mDeviceToken = deviceToken;
124        mGuard.open("close");
125    }
126
127    /**
128     * Returns a {@link MidiDeviceInfo} object, which describes this device.
129     *
130     * @return the {@link MidiDeviceInfo} object
131     */
132    public MidiDeviceInfo getInfo() {
133        return mDeviceInfo;
134    }
135
136    /**
137     * Called to open a {@link MidiInputPort} for the specified port number.
138     *
139     * An input port can only be used by one sender at a time.
140     * Opening an input port will fail if another application has already opened it for use.
141     * A {@link MidiDeviceStatus} can be used to determine if an input port is already open.
142     *
143     * @param portNumber the number of the input port to open
144     * @return the {@link MidiInputPort} if the open is successful,
145     *         or null in case of failure.
146     */
147    public MidiInputPort openInputPort(int portNumber) {
148        if (mIsDeviceClosed) {
149            return null;
150        }
151        try {
152            IBinder token = new Binder();
153            FileDescriptor fd = mDeviceServer.openInputPort(token, portNumber);
154            if (fd == null) {
155                return null;
156            }
157            return new MidiInputPort(mDeviceServer, token, fd, portNumber);
158        } catch (RemoteException e) {
159            Log.e(TAG, "RemoteException in openInputPort");
160            return null;
161        }
162    }
163
164    /**
165     * Called to open a {@link MidiOutputPort} for the specified port number.
166     *
167     * An output port may be opened by multiple applications.
168     *
169     * @param portNumber the number of the output port to open
170     * @return the {@link MidiOutputPort} if the open is successful,
171     *         or null in case of failure.
172     */
173    public MidiOutputPort openOutputPort(int portNumber) {
174        if (mIsDeviceClosed) {
175            return null;
176        }
177        try {
178            IBinder token = new Binder();
179            FileDescriptor fd = mDeviceServer.openOutputPort(token, portNumber);
180            if (fd == null) {
181                return null;
182            }
183            return new MidiOutputPort(mDeviceServer, token, fd, portNumber);
184        } catch (RemoteException e) {
185            Log.e(TAG, "RemoteException in openOutputPort");
186            return null;
187        }
188    }
189
190    /**
191     * Connects the supplied {@link MidiInputPort} to the output port of this device
192     * with the specified port number. Once the connection is made, the MidiInput port instance
193     * can no longer receive data via its {@link MidiReceiver#onSend} method.
194     * This method returns a {@link MidiDevice.MidiConnection} object, which can be used
195     * to close the connection.
196     *
197     * @param inputPort the inputPort to connect
198     * @param outputPortNumber the port number of the output port to connect inputPort to.
199     * @return {@link MidiDevice.MidiConnection} object if the connection is successful,
200     *         or null in case of failure.
201     */
202    public MidiConnection connectPorts(MidiInputPort inputPort, int outputPortNumber) {
203        if (outputPortNumber < 0 || outputPortNumber >= mDeviceInfo.getOutputPortCount()) {
204            throw new IllegalArgumentException("outputPortNumber out of range");
205        }
206        if (mIsDeviceClosed) {
207            return null;
208        }
209
210        FileDescriptor fd = inputPort.claimFileDescriptor();
211        if (fd == null) {
212            return null;
213        }
214        try {
215            IBinder token = new Binder();
216            int calleePid = mDeviceServer.connectPorts(token, fd, outputPortNumber);
217            // If the service is a different Process then it will duplicate the fd
218            // and we can safely close this one.
219            // But if the service is in the same Process then closing the fd will
220            // kill the connection. So don't do that.
221            if (calleePid != Process.myPid()) {
222                // close our copy of the file descriptor
223                IoUtils.closeQuietly(fd);
224            }
225
226            return new MidiConnection(token, inputPort);
227        } catch (RemoteException e) {
228            Log.e(TAG, "RemoteException in connectPorts");
229            return null;
230        }
231    }
232
233    /**
234     * Makes Midi Device available to the Native API
235     * @hide
236     */
237    public long mirrorToNative() throws IOException {
238        if (mIsDeviceClosed || mNativeHandle != 0) {
239            return 0;
240        }
241
242        mNativeHandle = native_mirrorToNative(mDeviceServer.asBinder(), mDeviceInfo.getId());
243        if (mNativeHandle == 0) {
244            throw new IOException("Failed mirroring to native");
245        }
246
247        synchronized (mMirroredDevices) {
248            mMirroredDevices.add(this);
249        }
250        return mNativeHandle;
251    }
252
253    /**
254     * Makes Midi Device no longer available to the Native API
255     * @hide
256     */
257    public void removeFromNative() {
258        if (mNativeHandle == 0) {
259            return;
260        }
261
262        synchronized (mGuard) {
263            native_removeFromNative(mNativeHandle);
264            mNativeHandle = 0;
265        }
266
267        synchronized (mMirroredDevices) {
268            mMirroredDevices.remove(this);
269        }
270    }
271
272    @Override
273    public void close() throws IOException {
274        synchronized (mGuard) {
275            if (!mIsDeviceClosed) {
276                removeFromNative();
277                mGuard.close();
278                mIsDeviceClosed = true;
279                try {
280                    mMidiManager.closeDevice(mClientToken, mDeviceToken);
281                } catch (RemoteException e) {
282                    Log.e(TAG, "RemoteException in closeDevice");
283                }
284            }
285        }
286    }
287
288    @Override
289    protected void finalize() throws Throwable {
290        try {
291            if (mGuard != null) {
292                mGuard.warnIfOpen();
293            }
294
295            close();
296        } finally {
297            super.finalize();
298        }
299    }
300
301    @Override
302    public String toString() {
303        return ("MidiDevice: " + mDeviceInfo.toString());
304    }
305
306    private native long native_mirrorToNative(IBinder deviceServerBinder, int id);
307    private native void native_removeFromNative(long deviceHandle);
308}
309