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