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.system.ErrnoException;
24import android.system.Os;
25import android.system.OsConstants;
26import android.util.Log;
27
28import com.android.internal.midi.MidiDispatcher;
29
30import dalvik.system.CloseGuard;
31
32import libcore.io.IoUtils;
33
34import java.io.Closeable;
35import java.io.FileDescriptor;
36import java.io.IOException;
37import java.util.HashMap;
38import java.util.concurrent.CopyOnWriteArrayList;
39
40/**
41 * Internal class used for providing an implementation for a MIDI device.
42 *
43 * @hide
44 */
45public final class MidiDeviceServer implements Closeable {
46    private static final String TAG = "MidiDeviceServer";
47
48    private final IMidiManager mMidiManager;
49
50    // MidiDeviceInfo for the device implemented by this server
51    private MidiDeviceInfo mDeviceInfo;
52    private final int mInputPortCount;
53    private final int mOutputPortCount;
54
55    // MidiReceivers for receiving data on our input ports
56    private final MidiReceiver[] mInputPortReceivers;
57
58    // MidiDispatchers for sending data on our output ports
59    private MidiDispatcher[] mOutputPortDispatchers;
60
61    // MidiOutputPorts for clients connected to our input ports
62    private final MidiOutputPort[] mInputPortOutputPorts;
63
64    // List of all MidiInputPorts we created
65    private final CopyOnWriteArrayList<MidiInputPort> mInputPorts
66            = new CopyOnWriteArrayList<MidiInputPort>();
67
68
69    // for reporting device status
70    private final boolean[] mInputPortOpen;
71    private final int[] mOutputPortOpenCount;
72
73    private final CloseGuard mGuard = CloseGuard.get();
74    private boolean mIsClosed;
75
76    private final Callback mCallback;
77
78    private final HashMap<IBinder, PortClient> mPortClients = new HashMap<IBinder, PortClient>();
79    private final HashMap<MidiInputPort, PortClient> mInputPortClients =
80            new HashMap<MidiInputPort, PortClient>();
81
82    public interface Callback {
83        /**
84         * Called to notify when an our device status has changed
85         * @param server the {@link MidiDeviceServer} that changed
86         * @param status the {@link MidiDeviceStatus} for the device
87         */
88        public void onDeviceStatusChanged(MidiDeviceServer server, MidiDeviceStatus status);
89
90        /**
91         * Called to notify when the device is closed
92         */
93        public void onClose();
94    }
95
96    abstract private class PortClient implements IBinder.DeathRecipient {
97        final IBinder mToken;
98
99        PortClient(IBinder token) {
100            mToken = token;
101
102            try {
103                token.linkToDeath(this, 0);
104            } catch (RemoteException e) {
105                close();
106            }
107        }
108
109        abstract void close();
110
111        MidiInputPort getInputPort() {
112            return null;
113        }
114
115        @Override
116        public void binderDied() {
117            close();
118        }
119    }
120
121    private class InputPortClient extends PortClient {
122        private final MidiOutputPort mOutputPort;
123
124        InputPortClient(IBinder token, MidiOutputPort outputPort) {
125            super(token);
126            mOutputPort = outputPort;
127        }
128
129        @Override
130        void close() {
131            mToken.unlinkToDeath(this, 0);
132            synchronized (mInputPortOutputPorts) {
133                int portNumber = mOutputPort.getPortNumber();
134                mInputPortOutputPorts[portNumber] = null;
135                mInputPortOpen[portNumber] = false;
136                updateDeviceStatus();
137            }
138            IoUtils.closeQuietly(mOutputPort);
139        }
140    }
141
142    private class OutputPortClient extends PortClient {
143        private final MidiInputPort mInputPort;
144
145        OutputPortClient(IBinder token, MidiInputPort inputPort) {
146            super(token);
147            mInputPort = inputPort;
148        }
149
150        @Override
151        void close() {
152            mToken.unlinkToDeath(this, 0);
153            int portNumber = mInputPort.getPortNumber();
154            MidiDispatcher dispatcher = mOutputPortDispatchers[portNumber];
155            synchronized (dispatcher) {
156                dispatcher.getSender().disconnect(mInputPort);
157                int openCount = dispatcher.getReceiverCount();
158                mOutputPortOpenCount[portNumber] = openCount;
159                updateDeviceStatus();
160           }
161
162            mInputPorts.remove(mInputPort);
163            IoUtils.closeQuietly(mInputPort);
164        }
165
166        @Override
167        MidiInputPort getInputPort() {
168            return mInputPort;
169        }
170    }
171
172    private static FileDescriptor[] createSeqPacketSocketPair() throws IOException {
173        try {
174            final FileDescriptor fd0 = new FileDescriptor();
175            final FileDescriptor fd1 = new FileDescriptor();
176            Os.socketpair(OsConstants.AF_UNIX, OsConstants.SOCK_SEQPACKET, 0, fd0, fd1);
177            return new FileDescriptor[] { fd0, fd1 };
178        } catch (ErrnoException e) {
179            throw e.rethrowAsIOException();
180        }
181    }
182
183    // Binder interface stub for receiving connection requests from clients
184    private final IMidiDeviceServer mServer = new IMidiDeviceServer.Stub() {
185
186        @Override
187        public FileDescriptor openInputPort(IBinder token, int portNumber) {
188            if (mDeviceInfo.isPrivate()) {
189                if (Binder.getCallingUid() != Process.myUid()) {
190                    throw new SecurityException("Can't access private device from different UID");
191                }
192            }
193
194            if (portNumber < 0 || portNumber >= mInputPortCount) {
195                Log.e(TAG, "portNumber out of range in openInputPort: " + portNumber);
196                return null;
197            }
198
199            synchronized (mInputPortOutputPorts) {
200                if (mInputPortOutputPorts[portNumber] != null) {
201                    Log.d(TAG, "port " + portNumber + " already open");
202                    return null;
203                }
204
205                try {
206                    FileDescriptor[] pair = createSeqPacketSocketPair();
207                    MidiOutputPort outputPort = new MidiOutputPort(pair[0], portNumber);
208                    mInputPortOutputPorts[portNumber] = outputPort;
209                    outputPort.connect(mInputPortReceivers[portNumber]);
210                    InputPortClient client = new InputPortClient(token, outputPort);
211                    synchronized (mPortClients) {
212                        mPortClients.put(token, client);
213                    }
214                    mInputPortOpen[portNumber] = true;
215                    updateDeviceStatus();
216                    return pair[1];
217                } catch (IOException e) {
218                    Log.e(TAG, "unable to create FileDescriptors in openInputPort");
219                    return null;
220                }
221            }
222        }
223
224        @Override
225        public FileDescriptor openOutputPort(IBinder token, int portNumber) {
226            if (mDeviceInfo.isPrivate()) {
227                if (Binder.getCallingUid() != Process.myUid()) {
228                    throw new SecurityException("Can't access private device from different UID");
229                }
230            }
231
232            if (portNumber < 0 || portNumber >= mOutputPortCount) {
233                Log.e(TAG, "portNumber out of range in openOutputPort: " + portNumber);
234                return null;
235            }
236
237            try {
238                FileDescriptor[] pair = createSeqPacketSocketPair();
239                MidiInputPort inputPort = new MidiInputPort(pair[0], portNumber);
240                // Undo the default blocking-mode of the server-side socket for
241                // physical devices to avoid stalling the Java device handler if
242                // client app code gets stuck inside 'onSend' handler.
243                if (mDeviceInfo.getType() != MidiDeviceInfo.TYPE_VIRTUAL) {
244                    IoUtils.setBlocking(pair[0], false);
245                }
246                MidiDispatcher dispatcher = mOutputPortDispatchers[portNumber];
247                synchronized (dispatcher) {
248                    dispatcher.getSender().connect(inputPort);
249                    int openCount = dispatcher.getReceiverCount();
250                    mOutputPortOpenCount[portNumber] = openCount;
251                    updateDeviceStatus();
252                }
253
254                mInputPorts.add(inputPort);
255                OutputPortClient client = new OutputPortClient(token, inputPort);
256                synchronized (mPortClients) {
257                    mPortClients.put(token, client);
258                }
259                synchronized (mInputPortClients) {
260                    mInputPortClients.put(inputPort, client);
261                }
262                return pair[1];
263            } catch (IOException e) {
264                Log.e(TAG, "unable to create FileDescriptors in openOutputPort");
265                return null;
266            }
267        }
268
269        @Override
270        public void closePort(IBinder token) {
271            MidiInputPort inputPort = null;
272            synchronized (mPortClients) {
273                PortClient client = mPortClients.remove(token);
274                if (client != null) {
275                    inputPort = client.getInputPort();
276                    client.close();
277                }
278            }
279            if (inputPort != null) {
280                synchronized (mInputPortClients) {
281                    mInputPortClients.remove(inputPort);
282                }
283            }
284        }
285
286        @Override
287        public void closeDevice() {
288            if (mCallback != null) {
289                mCallback.onClose();
290            }
291            IoUtils.closeQuietly(MidiDeviceServer.this);
292        }
293
294        @Override
295        public int connectPorts(IBinder token, FileDescriptor fd,
296                int outputPortNumber) {
297            MidiInputPort inputPort = new MidiInputPort(fd, outputPortNumber);
298            MidiDispatcher dispatcher = mOutputPortDispatchers[outputPortNumber];
299            synchronized (dispatcher) {
300                dispatcher.getSender().connect(inputPort);
301                int openCount = dispatcher.getReceiverCount();
302                mOutputPortOpenCount[outputPortNumber] = openCount;
303                updateDeviceStatus();
304            }
305
306            mInputPorts.add(inputPort);
307            OutputPortClient client = new OutputPortClient(token, inputPort);
308            synchronized (mPortClients) {
309                mPortClients.put(token, client);
310            }
311            synchronized (mInputPortClients) {
312                mInputPortClients.put(inputPort, client);
313            }
314            return Process.myPid(); // for caller to detect same process ID
315        }
316
317        @Override
318        public MidiDeviceInfo getDeviceInfo() {
319            return mDeviceInfo;
320        }
321
322        @Override
323        public void setDeviceInfo(MidiDeviceInfo deviceInfo) {
324            if (Binder.getCallingUid() != Process.SYSTEM_UID) {
325                throw new SecurityException("setDeviceInfo should only be called by MidiService");
326            }
327            if (mDeviceInfo != null) {
328                throw new IllegalStateException("setDeviceInfo should only be called once");
329            }
330            mDeviceInfo = deviceInfo;
331        }
332    };
333
334    // Constructor for MidiManager.createDeviceServer()
335    /* package */ MidiDeviceServer(IMidiManager midiManager, MidiReceiver[] inputPortReceivers,
336            int numOutputPorts, Callback callback) {
337        mMidiManager = midiManager;
338        mInputPortReceivers = inputPortReceivers;
339        mInputPortCount = inputPortReceivers.length;
340        mOutputPortCount = numOutputPorts;
341        mCallback = callback;
342
343        mInputPortOutputPorts = new MidiOutputPort[mInputPortCount];
344
345        mOutputPortDispatchers = new MidiDispatcher[numOutputPorts];
346        for (int i = 0; i < numOutputPorts; i++) {
347            mOutputPortDispatchers[i] = new MidiDispatcher(mInputPortFailureHandler);
348        }
349
350        mInputPortOpen = new boolean[mInputPortCount];
351        mOutputPortOpenCount = new int[numOutputPorts];
352
353        mGuard.open("close");
354    }
355
356    private final MidiDispatcher.MidiReceiverFailureHandler mInputPortFailureHandler =
357            new MidiDispatcher.MidiReceiverFailureHandler() {
358                public void onReceiverFailure(MidiReceiver receiver, IOException failure) {
359                    Log.e(TAG, "MidiInputPort failed to send data", failure);
360                    PortClient client = null;
361                    synchronized (mInputPortClients) {
362                        client = mInputPortClients.remove(receiver);
363                    }
364                    if (client != null) {
365                        client.close();
366                    }
367                }
368            };
369
370    // Constructor for MidiDeviceService.onCreate()
371    /* package */ MidiDeviceServer(IMidiManager midiManager, MidiReceiver[] inputPortReceivers,
372           MidiDeviceInfo deviceInfo, Callback callback) {
373        this(midiManager, inputPortReceivers, deviceInfo.getOutputPortCount(), callback);
374        mDeviceInfo = deviceInfo;
375    }
376
377    /* package */ IMidiDeviceServer getBinderInterface() {
378        return mServer;
379    }
380
381    public IBinder asBinder() {
382        return mServer.asBinder();
383    }
384
385    private void updateDeviceStatus() {
386        // clear calling identity, since we may be in a Binder call from one of our clients
387        long identityToken = Binder.clearCallingIdentity();
388
389        MidiDeviceStatus status = new MidiDeviceStatus(mDeviceInfo, mInputPortOpen,
390                mOutputPortOpenCount);
391        if (mCallback != null) {
392            mCallback.onDeviceStatusChanged(this, status);
393        }
394        try {
395            mMidiManager.setDeviceStatus(mServer, status);
396        } catch (RemoteException e) {
397            Log.e(TAG, "RemoteException in updateDeviceStatus");
398        } finally {
399            Binder.restoreCallingIdentity(identityToken);
400        }
401    }
402
403    @Override
404    public void close() throws IOException {
405        synchronized (mGuard) {
406            if (mIsClosed) return;
407            mGuard.close();
408
409            for (int i = 0; i < mInputPortCount; i++) {
410                MidiOutputPort outputPort = mInputPortOutputPorts[i];
411                if (outputPort != null) {
412                    IoUtils.closeQuietly(outputPort);
413                    mInputPortOutputPorts[i] = null;
414                }
415            }
416            for (MidiInputPort inputPort : mInputPorts) {
417                IoUtils.closeQuietly(inputPort);
418            }
419            mInputPorts.clear();
420            try {
421                mMidiManager.unregisterDeviceServer(mServer);
422            } catch (RemoteException e) {
423                Log.e(TAG, "RemoteException in unregisterDeviceServer");
424            }
425            mIsClosed = true;
426        }
427    }
428
429    @Override
430    protected void finalize() throws Throwable {
431        try {
432            if (mGuard != null) {
433                mGuard.warnIfOpen();
434            }
435
436            close();
437        } finally {
438            super.finalize();
439        }
440    }
441
442    /**
443     * Returns an array of {@link MidiReceiver} for the device's output ports.
444     * Clients can use these receivers to send data out the device's output ports.
445     * @return array of MidiReceivers
446     */
447    public MidiReceiver[] getOutputPortReceivers() {
448        MidiReceiver[] receivers = new MidiReceiver[mOutputPortCount];
449        System.arraycopy(mOutputPortDispatchers, 0, receivers, 0, mOutputPortCount);
450        return receivers;
451    }
452}
453