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