blob: e92c0346ce748fd06be5e38f806e2beb38b7e09d [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 and
* limitations under the License.
*/
package com.android.server.usb;
import android.annotation.NonNull;
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.os.Bundle;
import android.service.usb.UsbAlsaMidiDeviceProto;
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 com.android.internal.util.dump.DualDumpOutputStream;
import libcore.io.IoUtils;
import java.io.Closeable;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* Opens device connections to MIDI 1.0 endpoints.
* These endpoints will use ALSA.
*/
public final class UsbAlsaMidiDevice implements Closeable {
private static final String TAG = "UsbAlsaMidiDevice";
private final int mAlsaCard;
private final int mAlsaDevice;
// USB outputs are MIDI inputs
private final InputReceiverProxy[] mMidiInputPortReceivers;
private final int mNumInputs;
private final int mNumOutputs;
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;
private boolean mServerAvailable;
// pipe file descriptor for signalling input thread to exit
// only accessed from JNI code
private int mPipeFD = -1;
private PowerBoostSetter mPowerBoostSetter = null;
private final MidiDeviceServer.Callback mCallback = new MidiDeviceServer.Callback() {
@Override
public void onDeviceStatusChanged(MidiDeviceServer server, MidiDeviceStatus status) {
MidiDeviceInfo deviceInfo = status.getDeviceInfo();
int numInputPorts = deviceInfo.getInputPortCount();
int numOutputPorts = deviceInfo.getOutputPortCount();
int numOpenPorts = 0;
for (int i = 0; i < numInputPorts; i++) {
if (status.isInputPortOpen(i)) {
numOpenPorts++;
}
}
for (int i = 0; i < numOutputPorts; i++) {
if (status.getOutputPortOpenCount(i) > 0) {
numOpenPorts += status.getOutputPortOpenCount(i);
}
}
synchronized (mLock) {
Log.d(TAG, "numOpenPorts: " + numOpenPorts + " isOpen: " + mIsOpen
+ " mServerAvailable: " + mServerAvailable);
if ((numOpenPorts > 0) && !mIsOpen && mServerAvailable) {
openLocked();
} else if ((numOpenPorts == 0) && 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();
}
}
}
/**
* Creates an UsbAlsaMidiDevice based on the input parameters. Read/Write streams
* will be created individually as some devices don't have the same number of
* inputs and outputs.
*/
public static UsbAlsaMidiDevice create(Context context, Bundle properties, int card,
int device, int numInputs, int numOutputs) {
UsbAlsaMidiDevice midiDevice = new UsbAlsaMidiDevice(card, device, numInputs, numOutputs);
if (!midiDevice.register(context, properties)) {
IoUtils.closeQuietly(midiDevice);
Log.e(TAG, "createDeviceServer failed");
return null;
}
return midiDevice;
}
private UsbAlsaMidiDevice(int card, int device, int numInputs, int numOutputs) {
mAlsaCard = card;
mAlsaDevice = device;
mNumInputs = numInputs;
mNumOutputs = numOutputs;
// Create MIDI port receivers based on the number of output ports. The
// output of USB is the input of MIDI.
mMidiInputPortReceivers = new InputReceiverProxy[numOutputs];
for (int port = 0; port < numOutputs; port++) {
mMidiInputPortReceivers[port] = new InputReceiverProxy();
}
mPowerBoostSetter = new PowerBoostSetter();
}
private boolean openLocked() {
int inputStreamCount = mNumInputs;
// Create an extra stream for unblocking Os.poll()
if (inputStreamCount > 0) {
inputStreamCount++;
}
int outputStreamCount = mNumOutputs;
// The resulting file descriptors will be O_RDONLY following by O_WRONLY
FileDescriptor[] fileDescriptors = nativeOpen(mAlsaCard, mAlsaDevice,
inputStreamCount, outputStreamCount);
if (fileDescriptors == null) {
Log.e(TAG, "nativeOpen failed");
return false;
}
mFileDescriptors = fileDescriptors;
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];
int curOutputStream = 0;
for (int i = 0; i < outputStreamCount; i++) {
mOutputStreams[i] = new FileOutputStream(fileDescriptors[inputStreamCount + i]);
MidiEventScheduler scheduler = new MidiEventScheduler();
mEventSchedulers[i] = scheduler;
mMidiInputPortReceivers[i].setReceiver(scheduler.getReceiver());
}
final MidiReceiver[] outputReceivers = mServer.getOutputPortReceivers();
if (inputStreamCount > 0) {
// Create input thread which will read from all output ports of the physical device
new Thread("UsbAlsaMidiDevice 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 fd is used only for unblocking Os.poll()
break;
}
int count = mInputStreams[index].read(buffer);
outputReceivers[index].send(buffer, 0, count, timestamp);
// If messages are more than size 1, boost power.
if (mPowerBoostSetter != null && count > 1) {
mPowerBoostSetter.boostPower();
}
}
}
}
// 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("UsbAlsaMidiDevice 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 = context.getSystemService(MidiManager.class);
if (midiManager == null) {
Log.e(TAG, "No MidiManager in UsbAlsaMidiDevice.register()");
return false;
}
mServerAvailable = true;
mServer = midiManager.createDeviceServer(mMidiInputPortReceivers, mNumInputs,
null, null, properties, MidiDeviceInfo.TYPE_USB,
MidiDeviceInfo.PROTOCOL_UNKNOWN, mCallback);
if (mServer == null) {
return false;
}
return true;
}
@Override
public void close() throws IOException {
synchronized (mLock) {
if (mIsOpen) {
closeLocked();
}
mServerAvailable = false;
}
if (mServer != null) {
IoUtils.closeQuietly(mServer);
}
}
private void closeLocked() {
for (int i = 0; i < mEventSchedulers.length; i++) {
mMidiInputPortReceivers[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;
}
/**
* Write a description of the device to a dump stream.
*/
public void dump(String deviceAddr, @NonNull DualDumpOutputStream dump, @NonNull String idName,
long id) {
long token = dump.start(idName, id);
dump.write("device_address", UsbAlsaMidiDeviceProto.DEVICE_ADDRESS, deviceAddr);
dump.write("card", UsbAlsaMidiDeviceProto.CARD, mAlsaCard);
dump.write("device", UsbAlsaMidiDeviceProto.DEVICE, mAlsaDevice);
dump.end(token);
}
private native FileDescriptor[] nativeOpen(int card, int device, int numInputs,
int numOutputs);
private native void nativeClose(FileDescriptor[] fileDescriptors);
}