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