UsbMidiDevice.java revision 6d5a0f916499a69f28b860fd66d09b5025c30450
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 output port
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
132    public static UsbMidiDevice create(Context context, Bundle properties, int card, int device) {
133        // FIXME - support devices with different number of input and output ports
134        int subDeviceCount = nativeGetSubdeviceCount(card, device);
135        if (subDeviceCount <= 0) {
136            Log.e(TAG, "nativeGetSubdeviceCount failed");
137            return null;
138        }
139
140        UsbMidiDevice midiDevice = new UsbMidiDevice(card, device, subDeviceCount);
141        if (!midiDevice.register(context, properties)) {
142            IoUtils.closeQuietly(midiDevice);
143            Log.e(TAG, "createDeviceServer failed");
144            return null;
145        }
146        return midiDevice;
147    }
148
149    private UsbMidiDevice(int card, int device, int subdeviceCount) {
150        mAlsaCard = card;
151        mAlsaDevice = device;
152        mSubdeviceCount = subdeviceCount;
153
154        // FIXME - support devices with different number of input and output ports
155        int inputCount = subdeviceCount;
156        mInputPortReceivers = new InputReceiverProxy[inputCount];
157        for (int port = 0; port < inputCount; port++) {
158            mInputPortReceivers[port] = new InputReceiverProxy();
159        }
160    }
161
162    private boolean openLocked() {
163        // FIXME - support devices with different number of input and output ports
164        FileDescriptor[] fileDescriptors = nativeOpen(mAlsaCard, mAlsaDevice, mSubdeviceCount);
165        if (fileDescriptors == null) {
166            Log.e(TAG, "nativeOpen failed");
167            return false;
168        }
169
170        mFileDescriptors = fileDescriptors;
171        int inputCount = fileDescriptors.length;
172        // last file descriptor returned from nativeOpen() is only used for unblocking Os.poll()
173        // in our input thread
174        int outputCount = fileDescriptors.length - 1;
175
176        mPollFDs = new StructPollfd[inputCount];
177        mInputStreams = new FileInputStream[inputCount];
178        for (int i = 0; i < inputCount; i++) {
179            FileDescriptor fd = fileDescriptors[i];
180            StructPollfd pollfd = new StructPollfd();
181            pollfd.fd = fd;
182            pollfd.events = (short)OsConstants.POLLIN;
183            mPollFDs[i] = pollfd;
184            mInputStreams[i] = new FileInputStream(fd);
185        }
186
187        mOutputStreams = new FileOutputStream[outputCount];
188        mEventSchedulers = new MidiEventScheduler[outputCount];
189        for (int i = 0; i < outputCount; i++) {
190            mOutputStreams[i] = new FileOutputStream(fileDescriptors[i]);
191
192            MidiEventScheduler scheduler = new MidiEventScheduler();
193            mEventSchedulers[i] = scheduler;
194            mInputPortReceivers[i].setReceiver(scheduler.getReceiver());
195        }
196
197        final MidiReceiver[] outputReceivers = mServer.getOutputPortReceivers();
198
199        // Create input thread which will read from all input ports
200        new Thread("UsbMidiDevice input thread") {
201            @Override
202            public void run() {
203                byte[] buffer = new byte[BUFFER_SIZE];
204                try {
205                    while (true) {
206                        synchronized (mLock) {
207                            if (!mIsOpen) break;
208
209                            // look for a readable FileDescriptor
210                            for (int index = 0; index < mPollFDs.length; index++) {
211                                StructPollfd pfd = mPollFDs[index];
212                                if ((pfd.revents & (OsConstants.POLLERR
213                                                            | OsConstants.POLLHUP)) != 0) {
214                                    break;
215                                } else if ((pfd.revents & OsConstants.POLLIN) != 0) {
216                                    // clear readable flag
217                                    pfd.revents = 0;
218
219                                    if (index == mInputStreams.length - 1) {
220                                        // last file descriptor is used only for unblocking Os.poll()
221                                        break;
222                                    }
223
224                                    int count = mInputStreams[index].read(buffer);
225                                    outputReceivers[index].send(buffer, 0, count);
226                                }
227                            }
228                        }
229
230                        // wait until we have a readable port or we are signalled to close
231                        Os.poll(mPollFDs, -1 /* infinite timeout */);
232                     }
233                } catch (IOException e) {
234                    Log.d(TAG, "reader thread exiting");
235                } catch (ErrnoException e) {
236                    Log.d(TAG, "reader thread exiting");
237                }
238                Log.d(TAG, "input thread exit");
239            }
240        }.start();
241
242        // Create output thread for each output port
243        for (int port = 0; port < outputCount; port++) {
244            final MidiEventScheduler eventSchedulerF = mEventSchedulers[port];
245            final FileOutputStream outputStreamF = mOutputStreams[port];
246            final int portF = port;
247
248            new Thread("UsbMidiDevice output thread " + port) {
249                @Override
250                public void run() {
251                    while (true) {
252                        MidiEvent event;
253                        try {
254                            event = (MidiEvent)eventSchedulerF.waitNextEvent();
255                        } catch (InterruptedException e) {
256                            // try again
257                            continue;
258                        }
259                        if (event == null) {
260                            break;
261                        }
262                        try {
263                            outputStreamF.write(event.data, 0, event.count);
264                        } catch (IOException e) {
265                            Log.e(TAG, "write failed for port " + portF);
266                        }
267                        eventSchedulerF.addEventToPool(event);
268                    }
269                    Log.d(TAG, "output thread exit");
270                }
271            }.start();
272        }
273
274        mIsOpen = true;
275        return true;
276    }
277
278    private boolean register(Context context, Bundle properties) {
279        MidiManager midiManager = (MidiManager)context.getSystemService(Context.MIDI_SERVICE);
280        if (midiManager == null) {
281            Log.e(TAG, "No MidiManager in UsbMidiDevice.create()");
282            return false;
283        }
284
285        mServer = midiManager.createDeviceServer(mInputPortReceivers, mSubdeviceCount,
286                null, null, properties, MidiDeviceInfo.TYPE_USB, mCallback);
287        if (mServer == null) {
288            return false;
289        }
290
291        return true;
292    }
293
294    @Override
295    public void close() throws IOException {
296        synchronized (mLock) {
297            if (mIsOpen) {
298                closeLocked();
299            }
300        }
301
302        if (mServer != null) {
303            IoUtils.closeQuietly(mServer);
304        }
305    }
306
307    private void closeLocked() {
308        for (int i = 0; i < mEventSchedulers.length; i++) {
309            mInputPortReceivers[i].setReceiver(null);
310            mEventSchedulers[i].close();
311        }
312        mEventSchedulers = null;
313
314        for (int i = 0; i < mInputStreams.length; i++) {
315            IoUtils.closeQuietly(mInputStreams[i]);
316        }
317        mInputStreams = null;
318
319        for (int i = 0; i < mOutputStreams.length; i++) {
320            IoUtils.closeQuietly(mOutputStreams[i]);
321        }
322        mOutputStreams = null;
323
324        // nativeClose will close the file descriptors and signal the input thread to exit
325        nativeClose(mFileDescriptors);
326        mFileDescriptors = null;
327
328        mIsOpen = false;
329    }
330
331    private static native int nativeGetSubdeviceCount(int card, int device);
332    private native FileDescriptor[] nativeOpen(int card, int device, int subdeviceCount);
333    private native void nativeClose(FileDescriptor[] fileDescriptors);
334}
335