blob: 46ce7a0a1aaff56d591000a1a91573c463337f8f [file] [log] [blame]
/*
* 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 output port
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 inputCount = subdeviceCount;
mInputPortReceivers = new InputReceiverProxy[inputCount];
for (int port = 0; port < inputCount; 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 inputCount = fileDescriptors.length;
// last file descriptor returned from nativeOpen() is only used for unblocking Os.poll()
// in our input thread
int outputCount = fileDescriptors.length - 1;
mPollFDs = new StructPollfd[inputCount];
mInputStreams = new FileInputStream[inputCount];
for (int i = 0; i < inputCount; 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[outputCount];
mEventSchedulers = new MidiEventScheduler[outputCount];
for (int i = 0; i < outputCount; 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 input ports
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 output port
for (int port = 0; port < outputCount; 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);
}