| /* | |
| * Copyright (C) 2014 The Android Open Source Project | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions an | |
| * limitations under the License. | |
| */ | |
| package com.android.server.usb; | |
| import android.content.Context; | |
| import android.media.midi.MidiDeviceInfo; | |
| import android.media.midi.MidiDeviceServer; | |
| import android.media.midi.MidiDeviceStatus; | |
| import android.media.midi.MidiManager; | |
| import android.media.midi.MidiReceiver; | |
| import android.media.midi.MidiSender; | |
| import android.os.Bundle; | |
| import android.system.ErrnoException; | |
| import android.system.Os; | |
| import android.system.OsConstants; | |
| import android.system.StructPollfd; | |
| import android.util.Log; | |
| import com.android.internal.midi.MidiEventScheduler; | |
| import com.android.internal.midi.MidiEventScheduler.MidiEvent; | |
| import libcore.io.IoUtils; | |
| import java.io.Closeable; | |
| import java.io.FileDescriptor; | |
| import java.io.FileInputStream; | |
| import java.io.FileOutputStream; | |
| import java.io.IOException; | |
| public final class UsbMidiDevice implements Closeable { | |
| private static final String TAG = "UsbMidiDevice"; | |
| private final int mAlsaCard; | |
| private final int mAlsaDevice; | |
| private final int mSubdeviceCount; | |
| private final InputReceiverProxy[] mInputPortReceivers; | |
| private MidiDeviceServer mServer; | |
| // event schedulers for each input port of the physical device | |
| private MidiEventScheduler[] mEventSchedulers; | |
| private static final int BUFFER_SIZE = 512; | |
| private FileDescriptor[] mFileDescriptors; | |
| // for polling multiple FileDescriptors for MIDI events | |
| private StructPollfd[] mPollFDs; | |
| // streams for reading from ALSA driver | |
| private FileInputStream[] mInputStreams; | |
| // streams for writing to ALSA driver | |
| private FileOutputStream[] mOutputStreams; | |
| private final Object mLock = new Object(); | |
| private boolean mIsOpen; | |
| // pipe file descriptor for signalling input thread to exit | |
| // only accessed from JNI code | |
| private int mPipeFD = -1; | |
| private final MidiDeviceServer.Callback mCallback = new MidiDeviceServer.Callback() { | |
| @Override | |
| public void onDeviceStatusChanged(MidiDeviceServer server, MidiDeviceStatus status) { | |
| MidiDeviceInfo deviceInfo = status.getDeviceInfo(); | |
| int inputPorts = deviceInfo.getInputPortCount(); | |
| int outputPorts = deviceInfo.getOutputPortCount(); | |
| boolean hasOpenPorts = false; | |
| for (int i = 0; i < inputPorts; i++) { | |
| if (status.isInputPortOpen(i)) { | |
| hasOpenPorts = true; | |
| break; | |
| } | |
| } | |
| if (!hasOpenPorts) { | |
| for (int i = 0; i < outputPorts; i++) { | |
| if (status.getOutputPortOpenCount(i) > 0) { | |
| hasOpenPorts = true; | |
| break; | |
| } | |
| } | |
| } | |
| synchronized (mLock) { | |
| if (hasOpenPorts && !mIsOpen) { | |
| openLocked(); | |
| } else if (!hasOpenPorts && mIsOpen) { | |
| closeLocked(); | |
| } | |
| } | |
| } | |
| @Override | |
| public void onClose() { | |
| } | |
| }; | |
| // This class acts as a proxy for our MidiEventScheduler receivers, which do not exist | |
| // until the device has active clients | |
| private final class InputReceiverProxy extends MidiReceiver { | |
| private MidiReceiver mReceiver; | |
| @Override | |
| public void onSend(byte[] msg, int offset, int count, long timestamp) throws IOException { | |
| MidiReceiver receiver = mReceiver; | |
| if (receiver != null) { | |
| receiver.send(msg, offset, count, timestamp); | |
| } | |
| } | |
| public void setReceiver(MidiReceiver receiver) { | |
| mReceiver = receiver; | |
| } | |
| @Override | |
| public void onFlush() throws IOException { | |
| MidiReceiver receiver = mReceiver; | |
| if (receiver != null) { | |
| receiver.flush(); | |
| } | |
| } | |
| } | |
| public static UsbMidiDevice create(Context context, Bundle properties, int card, int device) { | |
| // FIXME - support devices with different number of input and output ports | |
| int subDeviceCount = nativeGetSubdeviceCount(card, device); | |
| if (subDeviceCount <= 0) { | |
| Log.e(TAG, "nativeGetSubdeviceCount failed"); | |
| return null; | |
| } | |
| UsbMidiDevice midiDevice = new UsbMidiDevice(card, device, subDeviceCount); | |
| if (!midiDevice.register(context, properties)) { | |
| IoUtils.closeQuietly(midiDevice); | |
| Log.e(TAG, "createDeviceServer failed"); | |
| return null; | |
| } | |
| return midiDevice; | |
| } | |
| private UsbMidiDevice(int card, int device, int subdeviceCount) { | |
| mAlsaCard = card; | |
| mAlsaDevice = device; | |
| mSubdeviceCount = subdeviceCount; | |
| // FIXME - support devices with different number of input and output ports | |
| int inputPortCount = subdeviceCount; | |
| mInputPortReceivers = new InputReceiverProxy[inputPortCount]; | |
| for (int port = 0; port < inputPortCount; port++) { | |
| mInputPortReceivers[port] = new InputReceiverProxy(); | |
| } | |
| } | |
| private boolean openLocked() { | |
| // FIXME - support devices with different number of input and output ports | |
| FileDescriptor[] fileDescriptors = nativeOpen(mAlsaCard, mAlsaDevice, mSubdeviceCount); | |
| if (fileDescriptors == null) { | |
| Log.e(TAG, "nativeOpen failed"); | |
| return false; | |
| } | |
| mFileDescriptors = fileDescriptors; | |
| int inputStreamCount = fileDescriptors.length; | |
| // last file descriptor returned from nativeOpen() is only used for unblocking Os.poll() | |
| // in our input thread | |
| int outputStreamCount = fileDescriptors.length - 1; | |
| mPollFDs = new StructPollfd[inputStreamCount]; | |
| mInputStreams = new FileInputStream[inputStreamCount]; | |
| for (int i = 0; i < inputStreamCount; i++) { | |
| FileDescriptor fd = fileDescriptors[i]; | |
| StructPollfd pollfd = new StructPollfd(); | |
| pollfd.fd = fd; | |
| pollfd.events = (short)OsConstants.POLLIN; | |
| mPollFDs[i] = pollfd; | |
| mInputStreams[i] = new FileInputStream(fd); | |
| } | |
| mOutputStreams = new FileOutputStream[outputStreamCount]; | |
| mEventSchedulers = new MidiEventScheduler[outputStreamCount]; | |
| for (int i = 0; i < outputStreamCount; i++) { | |
| mOutputStreams[i] = new FileOutputStream(fileDescriptors[i]); | |
| MidiEventScheduler scheduler = new MidiEventScheduler(); | |
| mEventSchedulers[i] = scheduler; | |
| mInputPortReceivers[i].setReceiver(scheduler.getReceiver()); | |
| } | |
| final MidiReceiver[] outputReceivers = mServer.getOutputPortReceivers(); | |
| // Create input thread which will read from all output ports of the physical device | |
| new Thread("UsbMidiDevice input thread") { | |
| @Override | |
| public void run() { | |
| byte[] buffer = new byte[BUFFER_SIZE]; | |
| try { | |
| while (true) { | |
| // Record time of event immediately after waking. | |
| long timestamp = System.nanoTime(); | |
| synchronized (mLock) { | |
| if (!mIsOpen) break; | |
| // look for a readable FileDescriptor | |
| for (int index = 0; index < mPollFDs.length; index++) { | |
| StructPollfd pfd = mPollFDs[index]; | |
| if ((pfd.revents & (OsConstants.POLLERR | |
| | OsConstants.POLLHUP)) != 0) { | |
| break; | |
| } else if ((pfd.revents & OsConstants.POLLIN) != 0) { | |
| // clear readable flag | |
| pfd.revents = 0; | |
| if (index == mInputStreams.length - 1) { | |
| // last file descriptor is used only for unblocking Os.poll() | |
| break; | |
| } | |
| int count = mInputStreams[index].read(buffer); | |
| outputReceivers[index].send(buffer, 0, count, timestamp); | |
| } | |
| } | |
| } | |
| // wait until we have a readable port or we are signalled to close | |
| Os.poll(mPollFDs, -1 /* infinite timeout */); | |
| } | |
| } catch (IOException e) { | |
| Log.d(TAG, "reader thread exiting"); | |
| } catch (ErrnoException e) { | |
| Log.d(TAG, "reader thread exiting"); | |
| } | |
| Log.d(TAG, "input thread exit"); | |
| } | |
| }.start(); | |
| // Create output thread for each input port of the physical device | |
| for (int port = 0; port < outputStreamCount; port++) { | |
| final MidiEventScheduler eventSchedulerF = mEventSchedulers[port]; | |
| final FileOutputStream outputStreamF = mOutputStreams[port]; | |
| final int portF = port; | |
| new Thread("UsbMidiDevice output thread " + port) { | |
| @Override | |
| public void run() { | |
| while (true) { | |
| MidiEvent event; | |
| try { | |
| event = (MidiEvent)eventSchedulerF.waitNextEvent(); | |
| } catch (InterruptedException e) { | |
| // try again | |
| continue; | |
| } | |
| if (event == null) { | |
| break; | |
| } | |
| try { | |
| outputStreamF.write(event.data, 0, event.count); | |
| } catch (IOException e) { | |
| Log.e(TAG, "write failed for port " + portF); | |
| } | |
| eventSchedulerF.addEventToPool(event); | |
| } | |
| Log.d(TAG, "output thread exit"); | |
| } | |
| }.start(); | |
| } | |
| mIsOpen = true; | |
| return true; | |
| } | |
| private boolean register(Context context, Bundle properties) { | |
| MidiManager midiManager = (MidiManager)context.getSystemService(Context.MIDI_SERVICE); | |
| if (midiManager == null) { | |
| Log.e(TAG, "No MidiManager in UsbMidiDevice.create()"); | |
| return false; | |
| } | |
| mServer = midiManager.createDeviceServer(mInputPortReceivers, mSubdeviceCount, | |
| null, null, properties, MidiDeviceInfo.TYPE_USB, mCallback); | |
| if (mServer == null) { | |
| return false; | |
| } | |
| return true; | |
| } | |
| @Override | |
| public void close() throws IOException { | |
| synchronized (mLock) { | |
| if (mIsOpen) { | |
| closeLocked(); | |
| } | |
| } | |
| if (mServer != null) { | |
| IoUtils.closeQuietly(mServer); | |
| } | |
| } | |
| private void closeLocked() { | |
| for (int i = 0; i < mEventSchedulers.length; i++) { | |
| mInputPortReceivers[i].setReceiver(null); | |
| mEventSchedulers[i].close(); | |
| } | |
| mEventSchedulers = null; | |
| for (int i = 0; i < mInputStreams.length; i++) { | |
| IoUtils.closeQuietly(mInputStreams[i]); | |
| } | |
| mInputStreams = null; | |
| for (int i = 0; i < mOutputStreams.length; i++) { | |
| IoUtils.closeQuietly(mOutputStreams[i]); | |
| } | |
| mOutputStreams = null; | |
| // nativeClose will close the file descriptors and signal the input thread to exit | |
| nativeClose(mFileDescriptors); | |
| mFileDescriptors = null; | |
| mIsOpen = false; | |
| } | |
| private static native int nativeGetSubdeviceCount(int card, int device); | |
| private native FileDescriptor[] nativeOpen(int card, int device, int subdeviceCount); | |
| private native void nativeClose(FileDescriptor[] fileDescriptors); | |
| } |