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 an
14 * limitations under the License.
15 */
16
17package com.android.server.usb;
18
19import android.content.Context;
20import android.media.midi.MidiDeviceInfo;
21import android.media.midi.MidiDeviceServer;
22import android.media.midi.MidiDeviceStatus;
23import android.media.midi.MidiManager;
24import android.media.midi.MidiReceiver;
25import android.media.midi.MidiSender;
26import android.os.Bundle;
27import android.system.ErrnoException;
28import android.system.Os;
29import android.system.OsConstants;
30import android.system.StructPollfd;
31import android.util.Log;
32
33import com.android.internal.midi.MidiEventScheduler;
34import com.android.internal.midi.MidiEventScheduler.MidiEvent;
35
36import libcore.io.IoUtils;
37
38import java.io.Closeable;
39import java.io.FileDescriptor;
40import java.io.FileInputStream;
41import java.io.FileOutputStream;
42import java.io.IOException;
43
44public final class UsbMidiDevice implements Closeable {
45    private static final String TAG = "UsbMidiDevice";
46
47    private final int mAlsaCard;
48    private final int mAlsaDevice;
49    private final int mSubdeviceCount;
50    private final InputReceiverProxy[] mInputPortReceivers;
51
52    private MidiDeviceServer mServer;
53
54    // event schedulers for each input port of the physical device
55    private MidiEventScheduler[] mEventSchedulers;
56
57    private static final int BUFFER_SIZE = 512;
58
59    private FileDescriptor[] mFileDescriptors;
60
61    // for polling multiple FileDescriptors for MIDI events
62    private StructPollfd[] mPollFDs;
63    // streams for reading from ALSA driver
64    private FileInputStream[] mInputStreams;
65    // streams for writing to ALSA driver
66    private FileOutputStream[] mOutputStreams;
67
68    private final Object mLock = new Object();
69    private boolean mIsOpen;
70
71    // pipe file descriptor for signalling input thread to exit
72    // only accessed from JNI code
73    private int mPipeFD = -1;
74
75    private final MidiDeviceServer.Callback mCallback = new MidiDeviceServer.Callback() {
76
77        @Override
78        public void onDeviceStatusChanged(MidiDeviceServer server, MidiDeviceStatus status) {
79            MidiDeviceInfo deviceInfo = status.getDeviceInfo();
80            int inputPorts = deviceInfo.getInputPortCount();
81            int outputPorts = deviceInfo.getOutputPortCount();
82            boolean hasOpenPorts = false;
83
84            for (int i = 0; i < inputPorts; i++) {
85                if (status.isInputPortOpen(i)) {
86                    hasOpenPorts = true;
87                    break;
88                }
89            }
90
91            if (!hasOpenPorts) {
92                for (int i = 0; i < outputPorts; i++) {
93                    if (status.getOutputPortOpenCount(i) > 0) {
94                        hasOpenPorts = true;
95                        break;
96                    }
97                }
98            }
99
100            synchronized (mLock) {
101                if (hasOpenPorts && !mIsOpen) {
102                    openLocked();
103                } else if (!hasOpenPorts && mIsOpen) {
104                    closeLocked();
105                }
106            }
107        }
108
109        @Override
110        public void onClose() {
111        }
112    };
113
114    // This class acts as a proxy for our MidiEventScheduler receivers, which do not exist
115    // until the device has active clients
116    private final class InputReceiverProxy extends MidiReceiver {
117        private MidiReceiver mReceiver;
118
119        @Override
120        public void onSend(byte[] msg, int offset, int count, long timestamp) throws IOException {
121            MidiReceiver receiver = mReceiver;
122            if (receiver != null) {
123                receiver.send(msg, offset, count, timestamp);
124            }
125        }
126
127        public void setReceiver(MidiReceiver receiver) {
128            mReceiver = receiver;
129        }
130
131        @Override
132        public void onFlush() throws IOException {
133            MidiReceiver receiver = mReceiver;
134            if (receiver != null) {
135                receiver.flush();
136            }
137        }
138    }
139
140    public static UsbMidiDevice create(Context context, Bundle properties, int card, int device) {
141        // FIXME - support devices with different number of input and output ports
142        int subDeviceCount = nativeGetSubdeviceCount(card, device);
143        if (subDeviceCount <= 0) {
144            Log.e(TAG, "nativeGetSubdeviceCount failed");
145            return null;
146        }
147
148        UsbMidiDevice midiDevice = new UsbMidiDevice(card, device, subDeviceCount);
149        if (!midiDevice.register(context, properties)) {
150            IoUtils.closeQuietly(midiDevice);
151            Log.e(TAG, "createDeviceServer failed");
152            return null;
153        }
154        return midiDevice;
155    }
156
157    private UsbMidiDevice(int card, int device, int subdeviceCount) {
158        mAlsaCard = card;
159        mAlsaDevice = device;
160        mSubdeviceCount = subdeviceCount;
161
162        // FIXME - support devices with different number of input and output ports
163        int inputPortCount = subdeviceCount;
164        mInputPortReceivers = new InputReceiverProxy[inputPortCount];
165        for (int port = 0; port < inputPortCount; port++) {
166            mInputPortReceivers[port] = new InputReceiverProxy();
167        }
168    }
169
170    private boolean openLocked() {
171        // FIXME - support devices with different number of input and output ports
172        FileDescriptor[] fileDescriptors = nativeOpen(mAlsaCard, mAlsaDevice, mSubdeviceCount);
173        if (fileDescriptors == null) {
174            Log.e(TAG, "nativeOpen failed");
175            return false;
176        }
177
178        mFileDescriptors = fileDescriptors;
179        int inputStreamCount = fileDescriptors.length;
180        // last file descriptor returned from nativeOpen() is only used for unblocking Os.poll()
181        // in our input thread
182        int outputStreamCount = fileDescriptors.length - 1;
183
184        mPollFDs = new StructPollfd[inputStreamCount];
185        mInputStreams = new FileInputStream[inputStreamCount];
186        for (int i = 0; i < inputStreamCount; i++) {
187            FileDescriptor fd = fileDescriptors[i];
188            StructPollfd pollfd = new StructPollfd();
189            pollfd.fd = fd;
190            pollfd.events = (short)OsConstants.POLLIN;
191            mPollFDs[i] = pollfd;
192            mInputStreams[i] = new FileInputStream(fd);
193        }
194
195        mOutputStreams = new FileOutputStream[outputStreamCount];
196        mEventSchedulers = new MidiEventScheduler[outputStreamCount];
197        for (int i = 0; i < outputStreamCount; i++) {
198            mOutputStreams[i] = new FileOutputStream(fileDescriptors[i]);
199
200            MidiEventScheduler scheduler = new MidiEventScheduler();
201            mEventSchedulers[i] = scheduler;
202            mInputPortReceivers[i].setReceiver(scheduler.getReceiver());
203        }
204
205        final MidiReceiver[] outputReceivers = mServer.getOutputPortReceivers();
206
207        // Create input thread which will read from all output ports of the physical device
208        new Thread("UsbMidiDevice input thread") {
209            @Override
210            public void run() {
211                byte[] buffer = new byte[BUFFER_SIZE];
212                try {
213                    while (true) {
214                        // Record time of event immediately after waking.
215                        long timestamp = System.nanoTime();
216                        synchronized (mLock) {
217                            if (!mIsOpen) break;
218
219                            // look for a readable FileDescriptor
220                            for (int index = 0; index < mPollFDs.length; index++) {
221                                StructPollfd pfd = mPollFDs[index];
222                                if ((pfd.revents & (OsConstants.POLLERR
223                                                            | OsConstants.POLLHUP)) != 0) {
224                                    break;
225                                } else if ((pfd.revents & OsConstants.POLLIN) != 0) {
226                                    // clear readable flag
227                                    pfd.revents = 0;
228
229                                    if (index == mInputStreams.length - 1) {
230                                        // last file descriptor is used only for unblocking Os.poll()
231                                        break;
232                                    }
233
234                                    int count = mInputStreams[index].read(buffer);
235                                    outputReceivers[index].send(buffer, 0, count, timestamp);
236                                }
237                            }
238                        }
239
240                        // wait until we have a readable port or we are signalled to close
241                        Os.poll(mPollFDs, -1 /* infinite timeout */);
242                     }
243                } catch (IOException e) {
244                    Log.d(TAG, "reader thread exiting");
245                } catch (ErrnoException e) {
246                    Log.d(TAG, "reader thread exiting");
247                }
248                Log.d(TAG, "input thread exit");
249            }
250        }.start();
251
252        // Create output thread for each input port of the physical device
253        for (int port = 0; port < outputStreamCount; port++) {
254            final MidiEventScheduler eventSchedulerF = mEventSchedulers[port];
255            final FileOutputStream outputStreamF = mOutputStreams[port];
256            final int portF = port;
257
258            new Thread("UsbMidiDevice output thread " + port) {
259                @Override
260                public void run() {
261                    while (true) {
262                        MidiEvent event;
263                        try {
264                            event = (MidiEvent)eventSchedulerF.waitNextEvent();
265                        } catch (InterruptedException e) {
266                            // try again
267                            continue;
268                        }
269                        if (event == null) {
270                            break;
271                        }
272                        try {
273                            outputStreamF.write(event.data, 0, event.count);
274                        } catch (IOException e) {
275                            Log.e(TAG, "write failed for port " + portF);
276                        }
277                        eventSchedulerF.addEventToPool(event);
278                    }
279                    Log.d(TAG, "output thread exit");
280                }
281            }.start();
282        }
283
284        mIsOpen = true;
285        return true;
286    }
287
288    private boolean register(Context context, Bundle properties) {
289        MidiManager midiManager = (MidiManager)context.getSystemService(Context.MIDI_SERVICE);
290        if (midiManager == null) {
291            Log.e(TAG, "No MidiManager in UsbMidiDevice.create()");
292            return false;
293        }
294
295        mServer = midiManager.createDeviceServer(mInputPortReceivers, mSubdeviceCount,
296                null, null, properties, MidiDeviceInfo.TYPE_USB, mCallback);
297        if (mServer == null) {
298            return false;
299        }
300
301        return true;
302    }
303
304    @Override
305    public void close() throws IOException {
306        synchronized (mLock) {
307            if (mIsOpen) {
308                closeLocked();
309            }
310        }
311
312        if (mServer != null) {
313            IoUtils.closeQuietly(mServer);
314        }
315    }
316
317    private void closeLocked() {
318        for (int i = 0; i < mEventSchedulers.length; i++) {
319            mInputPortReceivers[i].setReceiver(null);
320            mEventSchedulers[i].close();
321        }
322        mEventSchedulers = null;
323
324        for (int i = 0; i < mInputStreams.length; i++) {
325            IoUtils.closeQuietly(mInputStreams[i]);
326        }
327        mInputStreams = null;
328
329        for (int i = 0; i < mOutputStreams.length; i++) {
330            IoUtils.closeQuietly(mOutputStreams[i]);
331        }
332        mOutputStreams = null;
333
334        // nativeClose will close the file descriptors and signal the input thread to exit
335        nativeClose(mFileDescriptors);
336        mFileDescriptors = null;
337
338        mIsOpen = false;
339    }
340
341    private static native int nativeGetSubdeviceCount(int card, int device);
342    private native FileDescriptor[] nativeOpen(int card, int device, int subdeviceCount);
343    private native void nativeClose(FileDescriptor[] fileDescriptors);
344}
345