Commit 10024b3d authored by Mike Lockwood's avatar Mike Lockwood
Browse files

MidiManager updates:

MIDI ports are now implemented as file descriptors directly between the sender
and receiver, so the MidiService is no longer in the message path.

To facilitate the above, each port has its own file descriptor, rather than multiplexing
all ports on a device through a single socket.

Added a new class MidiDeviceServer, which is used by implementors of MIDI devices.
This replaces the MidiVirtualDevice class (which only was included in changes that were reviewed but never submitted).

The USB MIDI implementation has moved from the MIDI service to the USB service.
The USB MIDI implementation uses MidiDeviceServer as its interface, so we now have a common
interface for all MIDI device implementations.

Change-Id: I8effd1583f344beb6c940c3a24dbf20b477a6436
parent 34b064a1
......@@ -174,6 +174,7 @@ LOCAL_SRC_FILES += \
core/java/android/hardware/location/IGeofenceHardwareMonitorCallback.aidl \
core/java/android/hardware/soundtrigger/IRecognitionStatusCallback.aidl \
core/java/android/hardware/usb/IUsbManager.aidl \
core/java/android/midi/IMidiDeviceServer.aidl \
core/java/android/midi/IMidiListener.aidl \
core/java/android/midi/IMidiManager.aidl \
core/java/android/net/IConnectivityManager.aidl \
......
/*
* Copyright (C) 2014, The Android Open Source Project
* 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
* 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,
......@@ -16,4 +16,11 @@
package android.midi;
parcelable MidiDevice;
import android.os.ParcelFileDescriptor;
/** @hide */
interface IMidiDeviceServer
{
ParcelFileDescriptor openInputPort(int portNumber);
ParcelFileDescriptor openOutputPort(int portNumber);
}
......@@ -16,13 +16,11 @@
package android.midi;
import android.hardware.usb.UsbDevice;
import android.midi.IMidiDeviceServer;
import android.midi.IMidiListener;
import android.midi.MidiDevice;
import android.midi.MidiDeviceInfo;
import android.os.Bundle;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
/** @hide */
interface IMidiManager
......@@ -34,14 +32,10 @@ interface IMidiManager
void unregisterListener(IBinder token, in IMidiListener listener);
// for communicating with MIDI devices
ParcelFileDescriptor openDevice(IBinder token, in MidiDeviceInfo device);
IMidiDeviceServer openDevice(IBinder token, in MidiDeviceInfo device);
// for implementing virtual MIDI devices
MidiDevice registerVirtualDevice(IBinder token, int numInputPorts, int numOutputPorts,
in Bundle properties);
void unregisterVirtualDevice(IBinder token, in MidiDeviceInfo device);
// for use by UsbAudioManager
void alsaDeviceAdded(int card, int device, in UsbDevice usbDevice);
void alsaDeviceRemoved(in UsbDevice usbDevice);
MidiDeviceInfo registerDeviceServer(in IMidiDeviceServer server, int numInputPorts,
int numOutputPorts, in Bundle properties, boolean isPrivate, int type);
void unregisterDeviceServer(in IMidiDeviceServer server);
}
......@@ -16,9 +16,8 @@
package android.midi;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.util.Log;
import java.io.FileDescriptor;
......@@ -30,114 +29,31 @@ import java.util.ArrayList;
/**
* This class is used for sending and receiving data to and from an MIDI device
* Instances of this class are created by {@link MidiManager#openDevice}.
* This class can also be used to provide the implementation for a virtual device.
*
* This class implements Parcelable so it can be returned from MidiService when creating
* virtual MIDI devices.
*
* @hide
*/
public final class MidiDevice implements Parcelable {
public final class MidiDevice {
private static final String TAG = "MidiDevice";
private final MidiDeviceInfo mDeviceInfo;
private ParcelFileDescriptor mParcelFileDescriptor;
private FileInputStream mInputStream;
private FileOutputStream mOutputStream;
// lazily populated lists of ports
private final MidiInputPort[] mInputPorts;
private final MidiOutputPort[] mOutputPorts;
// array of receiver lists, indexed by port number
private final ArrayList<MidiReceiver>[] mReceivers;
private int mReceiverCount; // total number of receivers for all ports
private final IMidiDeviceServer mServer;
/**
* Minimum size of packed message as sent through our ParcelFileDescriptor
* 8 bytes for timestamp, 1 byte for port number and 1 to 3 bytes for message
/**
* MidiDevice should only be instantiated by MidiManager
* @hide
*/
public static final int MIN_PACKED_MESSAGE_SIZE = 10;
public MidiDevice(MidiDeviceInfo deviceInfo, IMidiDeviceServer server) {
mDeviceInfo = deviceInfo;
mServer = server;
}
/**
* Maximum size of packed message as sent through our ParcelFileDescriptor
* 8 bytes for timestamp, 1 byte for port number and 1 to 3 bytes for message
* @hide
*/
public static final int MAX_PACKED_MESSAGE_SIZE = 12;
// This thread reads MIDI events from a socket and distributes them to the list of
// MidiReceivers attached to this device.
private final Thread mThread = new Thread() {
@Override
public void run() {
byte[] buffer = new byte[MAX_PACKED_MESSAGE_SIZE];
ArrayList<MidiReceiver> deadReceivers = new ArrayList<MidiReceiver>();
try {
while (true) {
// read next event
int count = mInputStream.read(buffer);
if (count < MIN_PACKED_MESSAGE_SIZE || count > MAX_PACKED_MESSAGE_SIZE) {
Log.e(TAG, "Number of bytes read out of range: " + count);
break;
}
int offset = getMessageOffset(buffer, count);
int size = getMessageSize(buffer, count);
long timestamp = getMessageTimeStamp(buffer, count);
int port = getMessagePortNumber(buffer, count);
synchronized (mReceivers) {
ArrayList<MidiReceiver> receivers = mReceivers[port];
if (receivers != null) {
for (int i = 0; i < receivers.size(); i++) {
MidiReceiver receiver = receivers.get(i);
try {
receivers.get(i).onPost(buffer, offset, size, timestamp);
} catch (IOException e) {
Log.e(TAG, "post failed");
deadReceivers.add(receiver);
}
}
// remove any receivers that failed
if (deadReceivers.size() > 0) {
for (MidiReceiver receiver: deadReceivers) {
receivers.remove(receiver);
mReceiverCount--;
}
deadReceivers.clear();
}
if (receivers.size() == 0) {
mReceivers[port] = null;
}
// exit if we have no receivers left
if (mReceiverCount == 0) {
break;
}
}
}
}
} catch (IOException e) {
Log.e(TAG, "read failed");
}
}
};
/**
* MidiDevice should only be instantiated by MidiManager or MidiService
* @hide
* Returns a {@link MidiDeviceInfo} object, which describes this device.
*
* @return the {@link MidiDeviceInfo} object
*/
public MidiDevice(MidiDeviceInfo deviceInfo, ParcelFileDescriptor pfd) {
mDeviceInfo = deviceInfo;
mParcelFileDescriptor = pfd;
int inputPorts = deviceInfo.getInputPortCount();
int outputPorts = deviceInfo.getOutputPortCount();
mInputPorts = new MidiInputPort[inputPorts];
mOutputPorts = new MidiOutputPort[outputPorts];
mReceivers = new ArrayList[outputPorts];
public MidiDeviceInfo getInfo() {
return mDeviceInfo;
}
/**
......@@ -147,14 +63,15 @@ public final class MidiDevice implements Parcelable {
* @return the {@link MidiInputPort}
*/
public MidiInputPort openInputPort(int portNumber) {
if (portNumber < 0 || portNumber >= mDeviceInfo.getInputPortCount()) {
throw new IllegalArgumentException("input port number out of range");
}
synchronized (mInputPorts) {
if (mInputPorts[portNumber] == null) {
mInputPorts[portNumber] = new MidiInputPort(mOutputStream, portNumber);
try {
ParcelFileDescriptor pfd = mServer.openInputPort(portNumber);
if (pfd == null) {
return null;
}
return mInputPorts[portNumber];
return new MidiInputPort(pfd, portNumber);
} catch (RemoteException e) {
Log.e(TAG, "RemoteException in openInputPort");
return null;
}
}
......@@ -165,185 +82,20 @@ public final class MidiDevice implements Parcelable {
* @return the {@link MidiOutputPort}
*/
public MidiOutputPort openOutputPort(int portNumber) {
if (portNumber < 0 || portNumber >= mDeviceInfo.getOutputPortCount()) {
throw new IllegalArgumentException("output port number out of range");
}
synchronized (mOutputPorts) {
if (mOutputPorts[portNumber] == null) {
mOutputPorts[portNumber] = new MidiOutputPort(this, portNumber);
}
return mOutputPorts[portNumber];
}
}
/* package */ void connect(MidiReceiver receiver, int portNumber) {
synchronized (mReceivers) {
if (mReceivers[portNumber] == null) {
mReceivers[portNumber] = new ArrayList<MidiReceiver>();
}
mReceivers[portNumber].add(receiver);
if (mReceiverCount++ == 0) {
mThread.start();
}
}
}
/* package */ void disconnect(MidiReceiver receiver, int portNumber) {
synchronized (mReceivers) {
ArrayList<MidiReceiver> receivers = mReceivers[portNumber];
if (receivers != null && receivers.remove(receiver)) {
mReceiverCount--;
}
}
}
/* package */ boolean open() {
FileDescriptor fd = mParcelFileDescriptor.getFileDescriptor();
try {
mInputStream = new FileInputStream(fd);
} catch (Exception e) {
Log.e(TAG, "could not create mInputStream", e);
return false;
}
try {
mOutputStream = new FileOutputStream(fd);
} catch (Exception e) {
Log.e(TAG, "could not create mOutputStream", e);
return false;
}
return true;
}
/* package */ void close() {
try {
if (mInputStream != null) {
mInputStream.close();
}
if (mOutputStream != null) {
mOutputStream.close();
ParcelFileDescriptor pfd = mServer.openOutputPort(portNumber);
if (pfd == null) {
return null;
}
mParcelFileDescriptor.close();
} catch (IOException e) {
return new MidiOutputPort(pfd, portNumber);
} catch (RemoteException e) {
Log.e(TAG, "RemoteException in openOutputPort");
return null;
}
}
/**
* Returns a {@link MidiDeviceInfo} object, which describes this device.
*
* @return the {@link MidiDeviceInfo} object
*/
public MidiDeviceInfo getInfo() {
return mDeviceInfo;
}
@Override
public String toString() {
return ("MidiDevice: " + mDeviceInfo.toString() + " fd: " + mParcelFileDescriptor);
}
public static final Parcelable.Creator<MidiDevice> CREATOR =
new Parcelable.Creator<MidiDevice>() {
public MidiDevice createFromParcel(Parcel in) {
MidiDeviceInfo deviceInfo = (MidiDeviceInfo)in.readParcelable(null);
ParcelFileDescriptor pfd = (ParcelFileDescriptor)in.readParcelable(null);
return new MidiDevice(deviceInfo, pfd);
}
public MidiDevice[] newArray(int size) {
return new MidiDevice[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeParcelable(mDeviceInfo, flags);
parcel.writeParcelable(mParcelFileDescriptor, flags);
}
/**
* Utility function for packing a MIDI message to be sent through our ParcelFileDescriptor
*
* message byte array contains variable length MIDI message.
* messageSize is size of variable length MIDI message
* timestamp is message timestamp to pack
* dest is buffer to pack into
* returns size of packed message
*
* @hide
*/
public static int packMessage(byte[] message, int offset, int size, long timestamp,
int portNumber, byte[] dest) {
// pack variable length message first
System.arraycopy(message, offset, dest, 0, size);
int destOffset = size;
// timestamp takes 8 bytes
for (int i = 0; i < 8; i++) {
dest[destOffset++] = (byte)timestamp;
timestamp >>= 8;
}
// portNumber is last
dest[destOffset++] = (byte)portNumber;
return destOffset;
}
/**
* Utility function for unpacking a MIDI message to be sent through our ParcelFileDescriptor
* returns the offet of of MIDI message in packed buffer
*
* @hide
*/
public static int getMessageOffset(byte[] buffer, int bufferLength) {
// message is at start of buffer
return 0;
}
/**
* Utility function for unpacking a MIDI message to be sent through our ParcelFileDescriptor
* returns size of MIDI message in packed buffer
*
* @hide
*/
public static int getMessageSize(byte[] buffer, int bufferLength) {
// message length is total buffer length minus size of the timestamp and port number
return bufferLength - 9 /* (sizeof(timestamp) + sizeof(portNumber)) */;
return ("MidiDevice: " + mDeviceInfo.toString());
}
/**
* Utility function for unpacking a MIDI message to be sent through our ParcelFileDescriptor
* unpacks timestamp from packed buffer
*
* @hide
*/
public static long getMessageTimeStamp(byte[] buffer, int bufferLength) {
long timestamp = 0;
// timestamp follows variable length message data
int dataLength = getMessageSize(buffer, bufferLength);
for (int i = dataLength + 7; i >= dataLength; i--) {
// why can't Java deal with unsigned ints?
int b = buffer[i];
if (b < 0) b += 256;
timestamp = (timestamp << 8) | b;
}
return timestamp;
}
/**
* Utility function for unpacking a MIDI message to be sent through our ParcelFileDescriptor
* unpacks port number from packed buffer
*
* @hide
*/
public static int getMessagePortNumber(byte[] buffer, int bufferLength) {
// timestamp follows variable length message data and timestamp
int dataLength = getMessageSize(buffer, bufferLength);
return buffer[dataLength + 8 /* sizeof(timestamp) */];
}
}
......@@ -50,10 +50,6 @@ public class MidiDeviceInfo implements Parcelable {
private final int mOutputPortCount;
private final Bundle mProperties;
// used for USB devices only
private final int mAlsaCard;
private final int mAlsaDevice;
/**
* Bundle key for the device's manufacturer name property.
* Used with the {@link android.os.Bundle} returned by {@link #getProperties}.
......@@ -83,33 +79,30 @@ public class MidiDeviceInfo implements Parcelable {
public static final String PROPERTY_USB_DEVICE = "usb_device";
/**
* MidiDeviceInfo should only be instantiated by MidiService implementation
* @hide
* Bundle key for the device's ALSA card number.
* Only set for USB MIDI devices.
* Used with the {@link android.os.Bundle} returned by {@link #getProperties}
*/
public MidiDeviceInfo(int type, int id, int numInputPorts, int numOutputPorts,
Bundle properties) {
mType = type;
mId = id;
mInputPortCount = numInputPorts;
mOutputPortCount = numOutputPorts;
mProperties = properties;
mAlsaCard = -1;
mAlsaDevice = -1;
}
public static final String PROPERTY_ALSA_CARD = "alsa_card";
/**
* Bundle key for the device's ALSA device number.
* Only set for USB MIDI devices.
* Used with the {@link android.os.Bundle} returned by {@link #getProperties}
*/
public static final String PROPERTY_ALSA_DEVICE = "alsa_device";
/**
* MidiDeviceInfo should only be instantiated by MidiService implementation
* @hide
*/
public MidiDeviceInfo(int type, int id, int numInputPorts, int numOutputPorts,
Bundle properties, int alsaCard, int alsaDevice) {
Bundle properties) {
mType = type;
mId = id;
mInputPortCount = numInputPorts;
mOutputPortCount = numOutputPorts;
mProperties = properties;
mAlsaCard = alsaCard;
mAlsaDevice = alsaDevice;
}
/**
......@@ -158,20 +151,6 @@ public class MidiDeviceInfo implements Parcelable {
return mProperties;
}
/**
* @hide
*/
public int getAlsaCard() {
return mAlsaCard;
}
/**
* @hide
*/
public int getAlsaDevice() {
return mAlsaDevice;
}
@Override
public boolean equals(Object o) {
if (o instanceof MidiDeviceInfo) {
......@@ -191,9 +170,7 @@ public class MidiDeviceInfo implements Parcelable {
return ("MidiDeviceInfo[mType=" + mType +
",mInputPortCount=" + mInputPortCount +
",mOutputPortCount=" + mOutputPortCount +
",mProperties=" + mProperties +
",mAlsaCard=" + mAlsaCard +
",mAlsaDevice=" + mAlsaDevice);
",mProperties=" + mProperties);
}
public static final Parcelable.Creator<MidiDeviceInfo> CREATOR =
......@@ -204,9 +181,7 @@ public class MidiDeviceInfo implements Parcelable {
int inputPorts = in.readInt();
int outputPorts = in.readInt();
Bundle properties = in.readBundle();
int card = in.readInt();
int device = in.readInt();
return new MidiDeviceInfo(type, id, inputPorts, outputPorts, properties, card, device);
return new MidiDeviceInfo(type, id, inputPorts, outputPorts, properties);
}
public MidiDeviceInfo[] newArray(int size) {
......@@ -224,7 +199,5 @@ public class MidiDeviceInfo implements Parcelable {
parcel.writeInt(mInputPortCount);
parcel.writeInt(mOutputPortCount);
parcel.writeBundle(mProperties);
parcel.writeInt(mAlsaCard);
parcel.writeInt(mAlsaDevice);
}
}
/*
* 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 android.midi;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.system.OsConstants;
import android.util.Log;
import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
/** @hide */
public final class MidiDeviceServer implements Closeable {
private static final String TAG = "MidiDeviceServer";
private final IMidiManager mMidiManager;
// MidiDeviceInfo for the device implemented by this server
private MidiDeviceInfo mDeviceInfo;
private int mInputPortCount;
private int mOutputPortCount;
// output ports for receiving messages from our clients
// we can have only one per port number
private MidiOutputPort[] mInputPortSenders;
// receivers attached to our input ports
private ArrayList<MidiReceiver>[] mInputPortReceivers;
// input ports for sending messages to our clients
// we can have multiple outputs per port number
private ArrayList<MidiInputPort>[] mOutputPortReceivers;
// subclass of MidiInputPort for passing to clients
// that notifies us when the connection has failed
private class ServerInputPort extends MidiInputPort {
ServerInputPort(ParcelFileDescriptor pfd, int portNumber) {
super(pfd, portNumber);
}
@Override
public void onIOException() {
synchronized (mOutputPortReceivers) {
mOutputPortReceivers[getPortNumber()] = null;
}
}
}
// subclass of MidiOutputPort for passing to clients
// that notifies us when the connection has failed
private class ServerOutputPort extends MidiOutputPort {
ServerOutputPort(ParcelFileDescriptor pfd, int portNumber) {
super(pfd, portNumber);
}
@Override
public void onIOException() {
synchronized (mInputPortSenders) {
mInputPortSenders[getPortNumber()] = null;
}
}
}
// Binder interface stub for receiving connection requests from clients
private final IMidiDeviceServer mServer = new IMidiDeviceServer.Stub() {
@Override
public ParcelFileDescriptor openInputPort(int portNumber) {
if (portNumber < 0 || portNumber >= mInputPortCount) {
Log.e(TAG, "portNumber out of range in openInputPort: " + portNumber);
return null;
}
ParcelFileDescriptor result = null;
MidiOutputPort newOutputPort = null;
synchronized (mInputPortSenders) {
if (mInputPortSenders[portNumber] != null) {
Log.d(TAG, "port " + portNumber + " already open");
return null;
}
try {
ParcelFileDescriptor[] pair = ParcelFileDescriptor.createSocketPair(
OsConstants.SOCK_SEQPACKET);
newOutputPort = new ServerOutputPort(pair[0], portNumber);
mInputPortSenders[portNumber] = newOutputPort;
result = pair[1];
} catch (IOException e) {
Log.e(TAG, "unable to create ParcelFileDescriptors in openInputPort");
return null;
}
if (newOutputPort != null) {
ArrayList<MidiReceiver> receivers = mInputPortReceivers[portNumber];
synchronized (receivers) {
for (int i = 0; i < receivers.size(); i++) {
newOutputPort.connect(receivers.get(i));
}
}
}
}
return result;
}
@Override
public ParcelFileDescriptor openOutputPort(int portNumber) {
if (portNumber < 0 || portNumber >= mOutputPortCount) {
Log.e(TAG, "portNumber out of range in openOutputPort: " + portNumber);
return null;
}
synchronized (mOutputPortReceivers) {
try {
ParcelFileDescriptor[] pair = ParcelFileDescriptor.createSocketPair(
OsConstants.SOCK_SEQPACKET);
mOutputPortReceivers[portNumber].add(new ServerInputPort(pair[0], portNumber));
return pair[1];
} catch (IOException e) {
Log.e(TAG, "unable to create ParcelFileDescriptors in openOutputPort");
return null;
}
}
}
};
/* package */ MidiDeviceServer(IMidiManager midiManager) {
mMidiManager = midiManager;
}
/* package */ IMidiDeviceServer getBinderInterface() {
return mServer;
}
/* package */ void setDeviceInfo(MidiDeviceInfo deviceInfo) {
if (mDeviceInfo != null) {
throw new IllegalStateException("setDeviceInfo should only be called once");
}
mDeviceInfo = deviceInfo;
mInputPortCount = deviceInfo.getInputPortCount();
mOutputPortCount = deviceInfo.getOutputPortCount();
mInputPortSenders = new MidiOutputPort[mInputPortCount];
mInputPortReceivers = new ArrayList[mInputPortCount];
for (int i = 0; i < mInputPortCount; i++) {
mInputPortReceivers[i] = new ArrayList<MidiReceiver>();
}
mOutputPortReceivers = new ArrayList[mOutputPortCount];
for (int i = 0; i < mOutputPortCount; i++) {
mOutputPortReceivers[i] = new ArrayList<MidiInputPort>();
}
}
@Override
public void close() throws IOException {
try {
// FIXME - close input and output ports too?
mMidiManager.unregisterDeviceServer(mServer);
} catch (RemoteException e) {
Log.e(TAG, "RemoteException in unregisterDeviceServer");
}
}
/**
* Returns a {@link MidiDeviceInfo} object, which describes this device.
*
* @return the {@link MidiDeviceInfo} object
*/
public MidiDeviceInfo getInfo() {
return mDeviceInfo;
}
/**
* Called to open a {@link MidiSender} to allow receiving MIDI messages
* on the device's input port for the specified port number.
*
* @param portNumber the number of the input port
* @return the {@link MidiSender}
*/
public MidiSender openInputPortSender(int portNumber) {
if (portNumber < 0 || portNumber >= mDeviceInfo.getInputPortCount()) {
throw new IllegalArgumentException("portNumber " + portNumber + " out of range");
}
final int portNumberF = portNumber;
return new MidiSender() {
@Override
public void connect(MidiReceiver receiver) {
// We always synchronize on mInputPortSenders before receivers if we need to
// synchronize on both.
synchronized (mInputPortSenders) {
ArrayList<MidiReceiver> receivers = mInputPortReceivers[portNumberF];
synchronized (receivers) {
receivers.add(receiver);
MidiOutputPort outputPort = mInputPortSenders[portNumberF];
if (outputPort != null) {
outputPort.connect(receiver);
}
}
}
}
@Override
public void disconnect(MidiReceiver receiver) {
// We always synchronize on mInputPortSenders before receivers if we need to
// synchronize on both.
synchronized (mInputPortSenders) {
ArrayList<MidiReceiver> receivers = mInputPortReceivers[portNumberF];
synchronized (receivers) {
receivers.remove(receiver);
MidiOutputPort outputPort = mInputPortSenders[portNumberF];
if (outputPort != null) {
outputPort.disconnect(receiver);
}
}
}
}
};
}
/**
* Called to open a {@link MidiReceiver} to allow sending MIDI messages
* on the virtual device's output port for the specified port number.
*
* @param portNumber the number of the output port
* @return the {@link MidiReceiver}
*/
public MidiReceiver openOutputPortReceiver(int portNumber) {
if (portNumber < 0 || portNumber >= mDeviceInfo.getOutputPortCount()) {
throw new IllegalArgumentException("portNumber " + portNumber + " out of range");
}
final int portNumberF = portNumber;
return new MidiReceiver() {
@Override
public void onPost(byte[] msg, int offset, int count, long timestamp) throws IOException {
ArrayList<MidiInputPort> receivers = mOutputPortReceivers[portNumberF];
synchronized (receivers) {
for (int i = 0; i < receivers.size(); i++) {
// FIXME catch errors and remove dead ones
receivers.get(i).onPost(msg, offset, count, timestamp);
}
}
}
};
}
}
......@@ -16,6 +16,10 @@
package android.midi;
import android.os.ParcelFileDescriptor;
import libcore.io.IoUtils;
import java.io.FileOutputStream;
import java.io.IOException;
......@@ -24,15 +28,16 @@ import java.io.IOException;
*
* @hide
*/
public final class MidiInputPort extends MidiPort implements MidiReceiver {
public class MidiInputPort extends MidiPort implements MidiReceiver {
private final FileOutputStream mOutputStream;
// buffer to use for sending messages out our output stream
private final byte[] mBuffer = new byte[MidiDevice.MAX_PACKED_MESSAGE_SIZE];
private final byte[] mBuffer = new byte[MAX_PACKED_MESSAGE_SIZE];
/* package */ MidiInputPort(FileOutputStream outputStream, int portNumber) {
/* package */ MidiInputPort(ParcelFileDescriptor pfd, int portNumber) {
super(portNumber);
mOutputStream = outputStream;
mOutputStream = new ParcelFileDescriptor.AutoCloseOutputStream(pfd);
}
/**
......@@ -46,9 +51,20 @@ public final class MidiInputPort extends MidiPort implements MidiReceiver {
*/
public void onPost(byte[] msg, int offset, int count, long timestamp) throws IOException {
synchronized (mBuffer) {
int length = MidiDevice.packMessage(msg, offset, count, timestamp, getPortNumber(),
mBuffer);
mOutputStream.write(mBuffer, 0, length);
int length = packMessage(msg, offset, count, timestamp, mBuffer);
try {
mOutputStream.write(mBuffer, 0, length);
} catch (IOException e) {
IoUtils.closeQuietly(mOutputStream);
// report I/O failure
onIOException();
throw e;
}
}
}
@Override
public void close() throws IOException {
mOutputStream.close();
}
}
......@@ -20,7 +20,6 @@ import android.content.Context;
import android.os.Binder;
import android.os.IBinder;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.util.Log;
......@@ -146,40 +145,31 @@ public class MidiManager {
*/
public MidiDevice openDevice(MidiDeviceInfo deviceInfo) {
try {
ParcelFileDescriptor pfd = mService.openDevice(mToken, deviceInfo);
if (pfd == null) {
IMidiDeviceServer server = mService.openDevice(mToken, deviceInfo);
if (server == null) {
Log.e(TAG, "could not open device " + deviceInfo);
return null;
}
MidiDevice device = new MidiDevice(deviceInfo, pfd);
if (device.open()) {
Log.d(TAG, "openDevice returning " + device);
return device;
}
return new MidiDevice(deviceInfo, server);
} catch (RemoteException e) {
Log.e(TAG, "RemoteException in openDevice");
}
return null;
}
/**
* Creates a new MIDI virtual device.
* NOTE: The method for creating virtual devices is likely to change before release.
*
* @param numInputPorts number of input ports for the virtual device
* @param numOutputPorts number of output ports for the virtual device
* @param properties a {@link android.os.Bundle} containing properties describing the device
* @return a {@link MidiDevice} object to locally represent the device
*/
public MidiDevice createVirtualDevice(int numInputPorts, int numOutputPorts,
Bundle properties) {
/** @hide */
public MidiDeviceServer createDeviceServer(int numInputPorts, int numOutputPorts,
Bundle properties, boolean isPrivate, int type) {
try {
MidiDevice device = mService.registerVirtualDevice(mToken,
numInputPorts, numOutputPorts, properties);
if (device != null && !device.open()) {
device = null;
MidiDeviceServer server = new MidiDeviceServer(mService);
MidiDeviceInfo deviceInfo = mService.registerDeviceServer(server.getBinderInterface(),
numInputPorts, numOutputPorts, properties, isPrivate, type);
if (deviceInfo == null) {
Log.e(TAG, "registerVirtualDevice failed");
return null;
}
return device;
server.setDeviceInfo(deviceInfo);
return server;
} catch (RemoteException e) {
Log.e(TAG, "RemoteException in createVirtualDevice");
return null;
......@@ -187,16 +177,19 @@ public class MidiManager {
}
/**
* Removes a MIDI virtual device.
* Creates a new MIDI virtual device.
*
* @param device the {@link MidiDevice} for the virtual device to remove
* @param numInputPorts number of input ports for the virtual device
* @param numOutputPorts number of output ports for the virtual device
* @param properties a {@link android.os.Bundle} containing properties describing the device
* @param isPrivate true if this device should only be visible and accessible to apps
* with the same UID as the caller
* @return a {@link MidiVirtualDevice} object to locally represent the device
*/
public void closeVirtualDevice(MidiDevice device) {
try {
device.close();
mService.unregisterVirtualDevice(mToken, device.getInfo());
} catch (RemoteException e) {
Log.e(TAG, "RemoteException in unregisterVirtualDevice");
}
public MidiDeviceServer createDeviceServer(int numInputPorts, int numOutputPorts,
Bundle properties, boolean isPrivate) {
return createDeviceServer(numInputPorts, numOutputPorts, properties,
isPrivate, MidiDeviceInfo.TYPE_VIRTUAL);
}
}
......@@ -16,20 +16,88 @@
package android.midi;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import libcore.io.IoUtils;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
/**
* This class is used for receiving data to a port on a MIDI device
*
* @hide
*/
public final class MidiOutputPort extends MidiPort implements MidiSender {
public class MidiOutputPort extends MidiPort implements MidiSender {
private static final String TAG = "MidiOutputPort";
private final FileInputStream mInputStream;
// array of receiver lists, indexed by port number
private final ArrayList<MidiReceiver> mReceivers = new ArrayList<MidiReceiver>();
private int mReceiverCount; // total number of receivers for all ports
// This thread reads MIDI events from a socket and distributes them to the list of
// MidiReceivers attached to this device.
private final Thread mThread = new Thread() {
@Override
public void run() {
byte[] buffer = new byte[MAX_PACKED_MESSAGE_SIZE];
ArrayList<MidiReceiver> deadReceivers = new ArrayList<MidiReceiver>();
private final MidiDevice mDevice;
try {
while (true) {
// read next event
int count = mInputStream.read(buffer);
if (count < MIN_PACKED_MESSAGE_SIZE || count > MAX_PACKED_MESSAGE_SIZE) {
Log.e(TAG, "Number of bytes read out of range: " + count);
break;
}
/* package */ MidiOutputPort(MidiDevice device, int portNumber) {
int offset = getMessageOffset(buffer, count);
int size = getMessageSize(buffer, count);
long timestamp = getMessageTimeStamp(buffer, count);
synchronized (mReceivers) {
for (int i = 0; i < mReceivers.size(); i++) {
MidiReceiver receiver = mReceivers.get(i);
try {
receiver.onPost(buffer, offset, size, timestamp);
} catch (IOException e) {
Log.e(TAG, "post failed");
deadReceivers.add(receiver);
}
}
// remove any receivers that failed
if (deadReceivers.size() > 0) {
for (MidiReceiver receiver: deadReceivers) {
mReceivers.remove(receiver);
mReceiverCount--;
}
deadReceivers.clear();
}
// exit if we have no receivers left
if (mReceiverCount == 0) {
break;
}
}
}
} catch (IOException e) {
Log.e(TAG, "read failed");
// report I/O failure
IoUtils.closeQuietly(mInputStream);
onIOException();
}
}
};
/* package */ MidiOutputPort(ParcelFileDescriptor pfd, int portNumber) {
super(portNumber);
mDevice = device;
mInputStream = new ParcelFileDescriptor.AutoCloseInputStream(pfd);
}
/**
......@@ -39,7 +107,12 @@ public final class MidiOutputPort extends MidiPort implements MidiSender {
* @param receiver the receiver to connect
*/
public void connect(MidiReceiver receiver) {
mDevice.connect(receiver, getPortNumber());
synchronized (mReceivers) {
mReceivers.add(receiver);
if (mReceiverCount++ == 0) {
mThread.start();
}
}
}
/**
......@@ -48,6 +121,15 @@ public final class MidiOutputPort extends MidiPort implements MidiSender {
* @param receiver the receiver to connect
*/
public void disconnect(MidiReceiver receiver) {
mDevice.disconnect(receiver, getPortNumber());
synchronized (mReceivers) {
if (mReceivers.remove(receiver)) {
mReceiverCount--;
}
}
}
@Override
public void close() throws IOException {
mInputStream.close();
}
}
......@@ -16,8 +16,9 @@
package android.midi;
import java.io.FileOutputStream;
import java.io.IOException;
import android.util.Log;
import java.io.Closeable;
/**
* This class represents a MIDI input or output port.
......@@ -25,10 +26,24 @@ import java.io.IOException;
*
* @hide
*/
public class MidiPort {
abstract public class MidiPort implements Closeable {
private static final String TAG = "MidiPort";
private final int mPortNumber;
/**
* Minimum size of packed message as sent through our ParcelFileDescriptor
* 8 bytes for timestamp, 1 byte for port number and 1 to 3 bytes for message
*/
protected static final int MIN_PACKED_MESSAGE_SIZE = 10;
/**
* Maximum size of packed message as sent through our ParcelFileDescriptor
* 8 bytes for timestamp, 1 byte for port number and 1 to 3 bytes for message
*/
protected static final int MAX_PACKED_MESSAGE_SIZE = 12;
/* package */ MidiPort(int portNumber) {
mPortNumber = portNumber;
}
......@@ -41,4 +56,69 @@ public class MidiPort {
public final int getPortNumber() {
return mPortNumber;
}
/**
* Called when an IOExeption occurs while sending or receiving data.
* Subclasses can override to be notified of such errors
*
*/
public void onIOException() {
}
/**
* Utility function for packing a MIDI message to be sent through our ParcelFileDescriptor
*
* message byte array contains variable length MIDI message.
* messageSize is size of variable length MIDI message
* timestamp is message timestamp to pack
* dest is buffer to pack into
* returns size of packed message
*/
protected static int packMessage(byte[] message, int offset, int size, long timestamp,
byte[] dest) {
// pack variable length message first
System.arraycopy(message, offset, dest, 0, size);
int destOffset = size;
// timestamp takes 8 bytes
for (int i = 0; i < 8; i++) {
dest[destOffset++] = (byte)timestamp;
timestamp >>= 8;
}
return destOffset;
}
/**
* Utility function for unpacking a MIDI message to be sent through our ParcelFileDescriptor
* returns the offet of of MIDI message in packed buffer
*/
protected static int getMessageOffset(byte[] buffer, int bufferLength) {
// message is at start of buffer
return 0;
}
/**
* Utility function for unpacking a MIDI message to be sent through our ParcelFileDescriptor
* returns size of MIDI message in packed buffer
*/
protected static int getMessageSize(byte[] buffer, int bufferLength) {
// message length is total buffer length minus size of the timestamp and port number
return bufferLength - 8 /* sizeof(timestamp) */;
}
/**
* Utility function for unpacking a MIDI message to be sent through our ParcelFileDescriptor
* unpacks timestamp from packed buffer
*/
protected static long getMessageTimeStamp(byte[] buffer, int bufferLength) {
long timestamp = 0;
// timestamp follows variable length message data
int dataLength = getMessageSize(buffer, bufferLength);
for (int i = dataLength + 7; i >= dataLength; i--) {
int b = (int)buffer[i] & 0xFF;
timestamp = (timestamp << 8) | b;
}
return timestamp;
}
}
......@@ -14,21 +14,25 @@
* limitations under the License.
*/
package com.android.server.midi;
package com.android.server;
import android.content.Context;
import android.hardware.usb.UsbDevice;
import android.midi.IMidiDeviceServer;
import android.midi.IMidiListener;
import android.midi.IMidiManager;
import android.midi.MidiDevice;
import android.midi.MidiDeviceInfo;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.os.RemoteException;
import android.util.Log;
import com.android.internal.util.IndentingPrintWriter;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
......@@ -40,31 +44,34 @@ public class MidiService extends IMidiManager.Stub {
// list of all our clients, keyed by Binder token
private final HashMap<IBinder, Client> mClients = new HashMap<IBinder, Client>();
// list of all devices, keyed by ID
private final HashMap<Integer, MidiDeviceBase> mDevices
= new HashMap<Integer, MidiDeviceBase>();
// list of all devices, keyed by MidiDeviceInfo
private final HashMap<MidiDeviceInfo, Device> mDevicesByInfo
= new HashMap<MidiDeviceInfo, Device>();
// list of all USB devices, keyed by USB device.
private final HashMap<UsbDevice, UsbMidiDevice> mUsbDevices
= new HashMap<UsbDevice, UsbMidiDevice>();
// list of all devices, keyed by IMidiDeviceServer
private final HashMap<IBinder, Device> mDevicesByServer = new HashMap<IBinder, Device>();
// used for assigning IDs to MIDI devices
private int mNextDeviceId = 1;
private final class Client implements IBinder.DeathRecipient {
// Binder token for this client
private final IBinder mToken;
// This client's UID
private final int mUid;
// This client's PID
private final int mPid;
// List of all receivers for this client
private final ArrayList<IMidiListener> mListeners = new ArrayList<IMidiListener>();
private final ArrayList<MidiDeviceBase> mVirtualDevices = new ArrayList<MidiDeviceBase>();
public Client(IBinder token) {
mToken = token;
mUid = Binder.getCallingUid();
mPid = Binder.getCallingPid();
}
public void close() {
for (MidiDeviceBase device : mVirtualDevices) {
device.close();
}
mVirtualDevices.clear();
public int getUid() {
return mUid;
}
public void addListener(IMidiListener listener) {
......@@ -73,33 +80,33 @@ public class MidiService extends IMidiManager.Stub {
public void removeListener(IMidiListener listener) {
mListeners.remove(listener);
if (mListeners.size() == 0 && mVirtualDevices.size() == 0) {
if (mListeners.size() == 0) {
removeClient(mToken);
}
}
public void addVirtualDevice(MidiDeviceBase device) {
mVirtualDevices.add(device);
}
public void deviceAdded(Device device) {
// ignore private devices that our client cannot access
if (!device.isUidAllowed(mUid)) return;
public void removeVirtualDevice(MidiDeviceBase device) {
mVirtualDevices.remove(device);
}
public void deviceAdded(MidiDeviceInfo device) {
MidiDeviceInfo deviceInfo = device.getDeviceInfo();
try {
for (IMidiListener listener : mListeners) {
listener.onDeviceAdded(device);
listener.onDeviceAdded(deviceInfo);
}
} catch (RemoteException e) {
Log.e(TAG, "remote exception", e);
}
}
public void deviceRemoved(MidiDeviceInfo device) {
public void deviceRemoved(Device device) {
// ignore private devices that our client cannot access
if (!device.isUidAllowed(mUid)) return;
MidiDeviceInfo deviceInfo = device.getDeviceInfo();
try {
for (IMidiListener listener : mListeners) {
listener.onDeviceRemoved(device);
listener.onDeviceRemoved(deviceInfo);
}
} catch (RemoteException e) {
Log.e(TAG, "remote exception", e);
......@@ -109,8 +116,18 @@ public class MidiService extends IMidiManager.Stub {
public void binderDied() {
removeClient(mToken);
}
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("Client: UID: ");
sb.append(mUid);
sb.append(" PID: ");
sb.append(mPid);
sb.append(" listener count: ");
sb.append(mListeners.size());
return sb.toString();
}
}
private Client getClient(IBinder token) {
synchronized (mClients) {
......@@ -130,12 +147,53 @@ public class MidiService extends IMidiManager.Stub {
}
private void removeClient(IBinder token) {
synchronized (mClients) {
Client client = mClients.remove(token);
if (client != null) {
client.close();
mClients.remove(token);
}
private final class Device implements IBinder.DeathRecipient {
private final IMidiDeviceServer mServer;
private final MidiDeviceInfo mDeviceInfo;
// UID of device creator
private final int mUid;
// PID of device creator
private final int mPid;
public Device(IMidiDeviceServer server, MidiDeviceInfo deviceInfo) {
mServer = server;
mDeviceInfo = deviceInfo;
mUid = Binder.getCallingUid();
mPid = Binder.getCallingPid();
}
public MidiDeviceInfo getDeviceInfo() {
return mDeviceInfo;
}
public IMidiDeviceServer getDeviceServer() {
return mServer;
}
public boolean isUidAllowed(int uid) {
// FIXME
return true;
}
public void binderDied() {
synchronized (mDevicesByServer) {
removeDeviceLocked(this);
}
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("Device: ");
sb.append(mDeviceInfo);
sb.append(" UID: ");
sb.append(mUid);
sb.append(" PID: ");
sb.append(mPid);
return sb.toString();
}
}
public MidiService(Context context) {
......@@ -155,129 +213,94 @@ public class MidiService extends IMidiManager.Stub {
}
public MidiDeviceInfo[] getDeviceList() {
ArrayList<MidiDeviceInfo> infos = new ArrayList<MidiDeviceInfo>();
for (MidiDeviceBase device : mDevices.values()) {
infos.add(device.getInfo());
}
return infos.toArray(new MidiDeviceInfo[0]);
return mDevicesByInfo.keySet().toArray(new MidiDeviceInfo[0]);
}
public ParcelFileDescriptor openDevice(IBinder token, MidiDeviceInfo deviceInfo) {
MidiDeviceBase device = mDevices.get(deviceInfo.getId());
public IMidiDeviceServer openDevice(IBinder token, MidiDeviceInfo deviceInfo) {
Device device = mDevicesByInfo.get(deviceInfo);
if (device == null) {
Log.e(TAG, "device not found in openDevice: " + deviceInfo);
return null;
}
return device.getFileDescriptor();
if (!device.isUidAllowed(Binder.getCallingUid())) {
throw new SecurityException("Attempt to open private device with wrong UID");
}
return device.getDeviceServer();
}
public MidiDevice registerVirtualDevice(IBinder token, int numInputPorts, int numOutputPorts,
Bundle properties) {
VirtualMidiDevice device;
Client client = getClient(token);
if (client == null) return null;
public MidiDeviceInfo registerDeviceServer(IMidiDeviceServer server, int numInputPorts,
int numOutputPorts, Bundle properties, boolean isPrivate, int type) {
if (type != MidiDeviceInfo.TYPE_VIRTUAL && Binder.getCallingUid() != Process.SYSTEM_UID) {
throw new SecurityException("only system can create non-virtual devices");
}
synchronized (mDevices) {
int id = mNextDeviceId++;
MidiDeviceInfo deviceInfo = new MidiDeviceInfo(MidiDeviceInfo.TYPE_VIRTUAL, id,
numInputPorts, numOutputPorts, properties);
MidiDeviceInfo deviceInfo;
Device device;
device = new VirtualMidiDevice(deviceInfo);
if (!device.open()) {
synchronized (mDevicesByServer) {
int id = mNextDeviceId++;
deviceInfo = new MidiDeviceInfo(type, id, numInputPorts, numOutputPorts, properties);
IBinder binder = server.asBinder();
device = new Device(server, deviceInfo);
try {
binder.linkToDeath(device, 0);
} catch (RemoteException e) {
return null;
}
mDevices.put(id, device);
client.addVirtualDevice(device);
mDevicesByInfo.put(deviceInfo, device);
mDevicesByServer.put(server.asBinder(), device);
}
synchronized (mClients) {
MidiDeviceInfo deviceInfo = device.getInfo();
for (Client c : mClients.values()) {
c.deviceAdded(deviceInfo);
c.deviceAdded(device);
}
}
return device.getProxy();
return deviceInfo;
}
public void unregisterVirtualDevice(IBinder token, MidiDeviceInfo deviceInfo) {
Client client = getClient(token);
if (client == null) return;
MidiDeviceBase device;
synchronized (mDevices) {
device = mDevices.remove(deviceInfo.getId());
public void unregisterDeviceServer(IMidiDeviceServer server) {
synchronized (mDevicesByServer) {
removeDeviceLocked(mDevicesByServer.get(server.asBinder()));
}
}
if (device != null) {
client.removeVirtualDevice(device);
device.close();
// synchronize on mDevicesByServer
private void removeDeviceLocked(Device device) {
if (mDevicesByServer.remove(device.getDeviceServer().asBinder()) != null) {
mDevicesByInfo.remove(device.getDeviceInfo());
synchronized (mClients) {
for (Client c : mClients.values()) {
c.deviceRemoved(deviceInfo);
c.deviceRemoved(device);
}
}
}
}
// called by UsbAudioManager to notify of new USB MIDI devices
public void alsaDeviceAdded(int card, int device, UsbDevice usbDevice) {
Log.d(TAG, "alsaDeviceAdded: card:" + card + " device:" + device);
MidiDeviceInfo deviceInfo;
synchronized (mDevices) {
int id = mNextDeviceId++;
Bundle properties = new Bundle();
properties.putString(MidiDeviceInfo.PROPERTY_MANUFACTURER,
usbDevice.getManufacturerName());
properties.putString(MidiDeviceInfo.PROPERTY_MODEL,
usbDevice.getProductName());
properties.putString(MidiDeviceInfo.PROPERTY_SERIAL_NUMBER,
usbDevice.getSerialNumber());
properties.putParcelable(MidiDeviceInfo.PROPERTY_USB_DEVICE, usbDevice);
// FIXME - multiple ports not supported yet
int inputPorts = 1;
int outputPorts = 1;
deviceInfo = new MidiDeviceInfo(MidiDeviceInfo.TYPE_USB, id, inputPorts, outputPorts,
properties, card, device);
UsbMidiDevice midiDevice = new UsbMidiDevice(deviceInfo);
mDevices.put(id, midiDevice);
mUsbDevices.put(usbDevice, midiDevice);
}
@Override
public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, TAG);
final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ");
synchronized (mClients) {
for (Client client : mClients.values()) {
client.deviceAdded(deviceInfo);
}
}
}
pw.println("MIDI Manager State:");
pw.increaseIndent();
// called by UsbAudioManager to notify of removed USB MIDI devices
public void alsaDeviceRemoved(UsbDevice usbDevice) {
MidiDeviceInfo deviceInfo = null;
synchronized (mDevices) {
MidiDeviceBase device = mUsbDevices.remove(usbDevice);
if (device != null) {
device.close();
deviceInfo = device.getInfo();
mDevices.remove(deviceInfo.getId());
}
pw.println("Devices:");
pw.increaseIndent();
for (Device device : mDevicesByInfo.values()) {
pw.println(device.toString());
}
pw.decreaseIndent();
Log.d(TAG, "alsaDeviceRemoved: " + deviceInfo);
if (deviceInfo != null) {
synchronized (mClients) {
for (Client client : mClients.values()) {
client.deviceRemoved(deviceInfo);
}
}
pw.println("Clients:");
pw.increaseIndent();
for (Client client : mClients.values()) {
pw.println(client.toString());
}
pw.decreaseIndent();
}
}
/*
* 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.midi;
import android.midi.MidiDevice;
import android.midi.MidiDeviceInfo;
import android.os.Binder;
import android.os.ParcelFileDescriptor;
import android.system.OsConstants;
import android.util.Log;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
/**
* Abstract internal base class for entities in MidiService.
* This class contains two threads for reading and writing MIDI events.
* On one end we have the readMessage() and writeMessage() methods, which must be
* implemented by a subclass. On the other end we have file descriptors for sockets
* attached to client applications.
*/
abstract class MidiDeviceBase {
private static final String TAG = "MidiDeviceBase";
final MidiDeviceInfo mDeviceInfo;
private ReaderThread mReaderThread;
private WriterThread mWriterThread;
private ParcelFileDescriptor mParcelFileDescriptor;
// Reads MIDI messages from readMessage() and write them to one or more clients.
private class ReaderThread extends Thread {
private final ArrayList<FileOutputStream> mOutputStreams =
new ArrayList<FileOutputStream>();
@Override
public void run() {
byte[] buffer = new byte[MidiDevice.MAX_PACKED_MESSAGE_SIZE];
while (true) {
try {
int count = readMessage(buffer);
if (count > 0) {
synchronized (mOutputStreams) {
for (int i = 0; i < mOutputStreams.size(); i++) {
FileOutputStream fos = mOutputStreams.get(i);
try {
fos.write(buffer, 0, count);
} catch (IOException e) {
Log.e(TAG, "write failed", e);
mOutputStreams.remove(fos);
}
}
}
}
} catch (IOException e) {
Log.e(TAG, "read failed", e);
break;
}
}
}
public void addListener(FileOutputStream fos) {
synchronized (mOutputStreams) {
mOutputStreams.add(fos);
}
}
public void removeListener(FileOutputStream fos) {
synchronized (mOutputStreams) {
mOutputStreams.remove(fos);
}
}
}
// Reads MIDI messages from our client and writes them by calling writeMessage()
private class WriterThread extends Thread {
private final FileInputStream mInputStream;
public WriterThread(FileInputStream fis) {
mInputStream = fis;
}
@Override
public void run() {
byte[] buffer = new byte[MidiDevice.MAX_PACKED_MESSAGE_SIZE];
while (true) {
try {
int count = mInputStream.read(buffer);
writeMessage(buffer, count);
} catch (IOException e) {
Log.e(TAG, "WriterThread failed", e);
break;
}
}
}
}
public MidiDeviceBase(MidiDeviceInfo info) {
mDeviceInfo = info;
}
public MidiDeviceInfo getInfo() {
return mDeviceInfo;
}
public ParcelFileDescriptor getFileDescriptor() {
synchronized (this) {
if (mReaderThread == null) {
if (!open()) {
return null;
}
mReaderThread = new ReaderThread();
mReaderThread.start();
}
}
try {
ParcelFileDescriptor[] pair = ParcelFileDescriptor.createSocketPair(
OsConstants.SOCK_SEQPACKET);
mParcelFileDescriptor = pair[0];
FileOutputStream fos = new FileOutputStream(mParcelFileDescriptor.getFileDescriptor());
mReaderThread.addListener(fos);
// return an error if the device is already open for writing?
if (mWriterThread == null) {
FileInputStream fis = new FileInputStream(
mParcelFileDescriptor.getFileDescriptor());
mWriterThread = new WriterThread(fis);
mWriterThread.start();
}
return pair[1];
} catch (IOException e) {
Log.e(TAG, "could not create ParcelFileDescriptor pair", e);
return null;
}
}
abstract boolean open();
void close() {
try {
if (mParcelFileDescriptor != null) {
mParcelFileDescriptor.close();
}
} catch (IOException e) {
}
}
abstract int readMessage(byte[] buffer) throws IOException;
abstract void writeMessage(byte[] buffer, int count) throws IOException;
}
/*
* 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.midi;
import android.midi.MidiDevice;
import android.midi.MidiDeviceInfo;
import android.midi.MidiUtils;
import android.os.Binder;
import android.util.Log;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
// This is our subclass of MidiDeviceBase for communicating with USB MIDI devices
// via the ALSA driver file system.
class UsbMidiDevice extends MidiDeviceBase {
private static final String TAG = "UsbMidiDevice";
private FileInputStream mInputStream;
private FileOutputStream mOutputStream;
private final byte[] mBuffer = new byte[3];
public UsbMidiDevice(MidiDeviceInfo info) {
super(info);
}
public boolean open() {
if (mInputStream != null && mOutputStream != null) {
// already open
return true;
}
int card = mDeviceInfo.getAlsaCard();
int device = mDeviceInfo.getAlsaDevice();
if (card == -1 || device == -1) {
Log.e(TAG, "Not a USB device!");
return false;
}
// clear calling identity so we can access the driver file.
long identity = Binder.clearCallingIdentity();
File file = new File("/dev/snd/midiC" + card + "D" + device);
try {
mInputStream = new FileInputStream(file);
mOutputStream = new FileOutputStream(file);
} catch (Exception e) {
Log.e(TAG, "could not open " + file);
return false;
} finally {
Binder.restoreCallingIdentity(identity);
}
return true;
}
void close() {
super.close();
try {
if (mInputStream != null) {
mInputStream.close();
}
if (mOutputStream != null) {
mOutputStream.close();
}
} catch (IOException e) {
}
}
// Reads a message from the ALSA driver.
// The driver may return multiple messages, so we have to read byte at a time.
int readMessage(byte[] buffer) throws IOException {
if (mInputStream.read(mBuffer, 0, 1) != 1) {
Log.e(TAG, "could not read command byte");
return -1;
}
int dataSize = MidiUtils.getMessageDataSize(mBuffer[0]);
if (dataSize < 0) {
return -1;
}
if (dataSize > 0) {
if (mInputStream.read(mBuffer, 1, dataSize) != dataSize) {
Log.e(TAG, "could not read command data");
return -1;
}
}
return MidiDevice.packMessage(mBuffer, 0, dataSize + 1, System.nanoTime(),
0, // FIXME - multiple ports not supported yet
buffer);
}
// writes a message to the ALSA driver
void writeMessage(byte[] buffer, int count) throws IOException {
int offset = MidiDevice.getMessageOffset(buffer, count);
int size = MidiDevice.getMessageSize(buffer, count);
// FIXME - multiple ports not supported yet
mOutputStream.write(buffer, offset, count);
}
}
/*
* 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.midi;
import android.midi.MidiDevice;
import android.midi.MidiDeviceInfo;
import android.os.ParcelFileDescriptor;
import android.system.OsConstants;
import android.util.Log;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
// Our subclass of MidiDeviceBase to implement a virtual MIDI device
class VirtualMidiDevice extends MidiDeviceBase {
private static final String TAG = "VirtualMidiDevice";
private ParcelFileDescriptor[] mFileDescriptors;
private FileInputStream mInputStream;
private FileOutputStream mOutputStream;
public VirtualMidiDevice(MidiDeviceInfo info) {
super(info);
}
public boolean open() {
if (mInputStream != null && mOutputStream != null) {
// already open
return true;
}
try {
mFileDescriptors = ParcelFileDescriptor.createSocketPair(
OsConstants.SOCK_SEQPACKET);
FileDescriptor fd = mFileDescriptors[0].getFileDescriptor();
mInputStream = new FileInputStream(fd);
mOutputStream = new FileOutputStream(fd);
return true;
} catch (IOException e) {
Log.e(TAG, "failed to create ParcelFileDescriptor pair");
return false;
}
}
void close() {
super.close();
try {
if (mInputStream != null) {
mInputStream.close();
}
if (mOutputStream != null) {
mOutputStream.close();
}
if (mFileDescriptors != null && mFileDescriptors[0] != null) {
mFileDescriptors[0].close();
// file descriptor 1 is passed to client process
}
} catch (IOException e) {
}
}
MidiDevice getProxy() {
return new MidiDevice(mDeviceInfo, mFileDescriptors[1]);
}
int readMessage(byte[] buffer) throws IOException {
int ret = mInputStream.read(buffer);
// for now, throw away the timestamp
return ret - 8;
}
void writeMessage(byte[] buffer, int count) throws IOException {
mOutputStream.write(buffer, 0, count);
}
}
......@@ -22,6 +22,7 @@ LOCAL_SRC_FILES += \
$(LOCAL_REL_DIR)/com_android_server_SystemServer.cpp \
$(LOCAL_REL_DIR)/com_android_server_tv_TvInputHal.cpp \
$(LOCAL_REL_DIR)/com_android_server_UsbDeviceManager.cpp \
$(LOCAL_REL_DIR)/com_android_server_UsbMidiDevice.cpp \
$(LOCAL_REL_DIR)/com_android_server_UsbHostManager.cpp \
$(LOCAL_REL_DIR)/com_android_server_VibratorService.cpp \
$(LOCAL_REL_DIR)/com_android_server_PersistentDataBlockService.cpp \
......
/*
* Copyright (C) 2010 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.
*/
#define LOG_TAG "UsbMidiDeviceJNI"
#define LOG_NDEBUG 0
#include "utils/Log.h"
#include "jni.h"
#include "JNIHelp.h"
#include "android_runtime/AndroidRuntime.h"
#include "android_runtime/Log.h"
#include <stdio.h>
#include <errno.h>
#include <asm/byteorder.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sound/asound.h>
namespace android
{
static jclass sFileDescriptorClass;
static jint
android_server_UsbMidiDevice_get_subdevice_count(JNIEnv *env, jobject /* thiz */,
jint card, jint device)
{
char path[100];
snprintf(path, sizeof(path), "/dev/snd/controlC%d", card);
int fd = open(path, O_RDWR);
if (fd < 0) {
ALOGE("could not open %s", path);
return 0;
}
struct snd_rawmidi_info info;
memset(&info, 0, sizeof(info));
info.device = device;
int ret = ioctl(fd, SNDRV_CTL_IOCTL_RAWMIDI_INFO, &info);
close(fd);
if (ret < 0) {
ALOGE("SNDRV_CTL_IOCTL_RAWMIDI_INFO failed, errno: %d path: %s", errno, path);
return -1;
}
ALOGD("subdevices_count: %d", info.subdevices_count);
return info.subdevices_count;
}
static jobjectArray
android_server_UsbMidiDevice_open(JNIEnv *env, jobject /* thiz */, jint card, jint device,
jint subdevice_count)
{
char path[100];
snprintf(path, sizeof(path), "/dev/snd/midiC%dD%d", card, device);
jobjectArray fds = env->NewObjectArray(subdevice_count, sFileDescriptorClass, NULL);
if (!fds) {
return NULL;
}
// to support multiple subdevices we open the same file multiple times
for (int i = 0; i < subdevice_count; i++) {
int fd = open(path, O_RDWR);
if (fd < 0) {
ALOGE("open failed on %s for index %d", path, i);
return NULL;
}
jobject fileDescriptor = jniCreateFileDescriptor(env, fd);
env->SetObjectArrayElement(fds, i, fileDescriptor);
env->DeleteLocalRef(fileDescriptor);
}
return fds;
}
static JNINativeMethod method_table[] = {
{ "nativeGetSubdeviceCount", "(II)I", (void*)android_server_UsbMidiDevice_get_subdevice_count },
{ "nativeOpen", "(III)[Ljava/io/FileDescriptor;", (void*)android_server_UsbMidiDevice_open },
};
int register_android_server_UsbMidiDevice(JNIEnv *env)
{
jclass clazz = env->FindClass("java/io/FileDescriptor");
if (clazz == NULL) {
ALOGE("Can't find java/io/FileDescriptor");
return -1;
}
sFileDescriptorClass = (jclass)env->NewGlobalRef(clazz);;
clazz = env->FindClass("com/android/server/usb/UsbMidiDevice");
if (clazz == NULL) {
ALOGE("Can't find com/android/server/usb/UsbMidiDevice");
return -1;
}
return jniRegisterNativeMethods(env, "com/android/server/usb/UsbMidiDevice",
method_table, NELEM(method_table));
}
};
......@@ -32,6 +32,7 @@ int register_android_server_PowerManagerService(JNIEnv* env);
int register_android_server_SerialService(JNIEnv* env);
int register_android_server_SystemServer(JNIEnv* env);
int register_android_server_UsbDeviceManager(JNIEnv* env);
int register_android_server_UsbMidiDevice(JNIEnv* env);
int register_android_server_UsbHostManager(JNIEnv* env);
int register_android_server_VibratorService(JNIEnv* env);
int register_android_server_location_GpsLocationProvider(JNIEnv* env);
......@@ -65,6 +66,7 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
register_android_server_LightsService(env);
register_android_server_AlarmManagerService(env);
register_android_server_UsbDeviceManager(env);
register_android_server_UsbMidiDevice(env);
register_android_server_UsbHostManager(env);
register_android_server_VibratorService(env);
register_android_server_SystemServer(env);
......
......@@ -74,7 +74,7 @@ import com.android.server.lights.LightsService;
import com.android.server.media.MediaRouterService;
import com.android.server.media.MediaSessionService;
import com.android.server.media.projection.MediaProjectionManagerService;
import com.android.server.midi.MidiService;
import com.android.server.MidiService;
import com.android.server.net.NetworkPolicyManagerService;
import com.android.server.net.NetworkStatsService;
import com.android.server.notification.NotificationManagerService;
......
......@@ -24,15 +24,15 @@ import android.hardware.usb.UsbConstants;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbInterface;
import android.media.AudioManager;
import android.midi.IMidiManager;
import android.os.FileObserver;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemClock;
import android.os.UserHandle;
import android.util.Slog;
import libcore.io.IoUtils;
import java.io.File;
import java.io.FileDescriptor;
import java.io.PrintWriter;
......@@ -42,14 +42,13 @@ import java.util.ArrayList;
/**
* UsbAlsaManager manages USB audio and MIDI devices.
*/
public class UsbAlsaManager {
public final class UsbAlsaManager {
private static final String TAG = UsbAlsaManager.class.getSimpleName();
private static final boolean DEBUG = true;
private static final String ALSA_DIRECTORY = "/dev/snd/";
private final Context mContext;
private IMidiManager mMidiManager;
private final AlsaCardsParser mCardsParser = new AlsaCardsParser();
private final AlsaDevicesParser mDevicesParser = new AlsaDevicesParser();
......@@ -60,6 +59,9 @@ public class UsbAlsaManager {
private final HashMap<UsbDevice,UsbAudioDevice>
mAudioDevices = new HashMap<UsbDevice,UsbAudioDevice>();
private final HashMap<UsbDevice,UsbMidiDevice>
mMidiDevices = new HashMap<UsbDevice,UsbMidiDevice>();
private final HashMap<String,AlsaDevice>
mAlsaDevices = new HashMap<String,AlsaDevice>();
......@@ -121,8 +123,6 @@ public class UsbAlsaManager {
}
public void systemReady() {
final IBinder b = ServiceManager.getService(Context.MIDI_SERVICE);
mMidiManager = IMidiManager.Stub.asInterface(b);
mAlsaObserver.startWatching();
// add existing alsa devices
......@@ -151,7 +151,6 @@ public class UsbAlsaManager {
intent.putExtra("device", audioDevice.mDevice);
intent.putExtra("hasPlayback", audioDevice.mHasPlayback);
intent.putExtra("hasCapture", audioDevice.mHasCapture);
intent.putExtra("hasMIDI", audioDevice.mHasMIDI);
intent.putExtra("class", audioDevice.mDeviceClass);
mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
}
......@@ -235,9 +234,9 @@ public class UsbAlsaManager {
/*
* Select the default device of the specified card.
*/
/* package */ boolean selectCard(int card) {
/* package */ boolean selectAudioCard(int card) {
if (DEBUG) {
Slog.d(TAG, "selectCard() card:" + card);
Slog.d(TAG, "selectAudioCard() card:" + card);
}
if (!mCardsParser.isCardUsb(card)) {
// Don't. AudioPolicyManager has logic for falling back to internal devices.
......@@ -259,7 +258,6 @@ public class UsbAlsaManager {
boolean hasPlayback = mDevicesParser.hasPlaybackDevices(card);
boolean hasCapture = mDevicesParser.hasCaptureDevices(card);
boolean hasMidi = mDevicesParser.hasMIDIDevices(card);
int deviceClass =
(mCardsParser.isCardUsb(card)
? UsbAudioDevice.kAudioDeviceClass_External
......@@ -275,21 +273,13 @@ public class UsbAlsaManager {
if (hasCapture && (waitForAlsaDevice(card, device, AlsaDevice.TYPE_CAPTURE) == null)) {
return false;
}
//TODO - seems to me that we need to decouple the above tests for audio
// from the one below for MIDI.
// MIDI device file needed/present?
AlsaDevice midiDevice = null;
if (hasMidi) {
midiDevice = waitForAlsaDevice(card, device, AlsaDevice.TYPE_MIDI);
}
if (DEBUG) {
Slog.d(TAG, "usb: hasPlayback:" + hasPlayback + " hasCapture:" + hasCapture);
}
mSelectedAudioDevice =
new UsbAudioDevice(card, device, hasPlayback, hasCapture, hasMidi, deviceClass);
new UsbAudioDevice(card, device, hasPlayback, hasCapture, deviceClass);
mSelectedAudioDevice.mDeviceName = mCardsParser.getCardRecordFor(card).mCardName;
mSelectedAudioDevice.mDeviceDescription =
mCardsParser.getCardRecordFor(card).mCardDescription;
......@@ -304,7 +294,7 @@ public class UsbAlsaManager {
Slog.d(TAG, "UsbAudioManager.selectDefaultDevice()");
}
mCardsParser.scan();
return selectCard(mCardsParser.getDefaultCard());
return selectAudioCard(mCardsParser.getDefaultCard());
}
/* package */ void deviceAdded(UsbDevice usbDevice) {
......@@ -314,7 +304,6 @@ public class UsbAlsaManager {
// Is there an audio interface in there?
boolean isAudioDevice = false;
AlsaDevice midiDevice = null;
// FIXME - handle multiple configurations?
int interfaceCount = usbDevice.getInterfaceCount();
......@@ -347,36 +336,44 @@ public class UsbAlsaManager {
// If the default isn't a USB device, let the existing "select internal mechanism"
// handle the selection.
if (mCardsParser.isCardUsb(addedCard)) {
selectCard(addedCard);
selectAudioCard(addedCard);
mAudioDevices.put(usbDevice, mSelectedAudioDevice);
}
if (midiDevice != null && mMidiManager != null) {
try {
mMidiManager.alsaDeviceAdded(midiDevice.mCard, midiDevice.mDevice, usbDevice);
} catch (RemoteException e) {
Slog.e(TAG, "MIDI Manager dead", e);
// look for MIDI devices
// Don't need to call mDevicesParser.scan() because selectAudioCard() does this above.
// Uncomment this next line if that behavior changes in the fugure.
// mDevicesParser.scan()
boolean hasMidi = mDevicesParser.hasMIDIDevices(addedCard);
if (hasMidi) {
int device = mDevicesParser.getDefaultDeviceNum(addedCard);
AlsaDevice alsaDevice = waitForAlsaDevice(addedCard, device, AlsaDevice.TYPE_MIDI);
if (alsaDevice != null) {
UsbMidiDevice usbMidiDevice = UsbMidiDevice.create(mContext, usbDevice,
alsaDevice.mCard, alsaDevice.mDevice);
if (usbMidiDevice != null) {
mMidiDevices.put(usbDevice, usbMidiDevice);
}
}
}
}
}
/* package */ void deviceRemoved(UsbDevice device) {
/* package */ void deviceRemoved(UsbDevice usbDevice) {
if (DEBUG) {
Slog.d(TAG, "deviceRemoved(): " + device);
Slog.d(TAG, "deviceRemoved(): " + usbDevice);
}
UsbAudioDevice audioDevice = mAudioDevices.remove(device);
UsbAudioDevice audioDevice = mAudioDevices.remove(usbDevice);
if (audioDevice != null) {
if (audioDevice.mHasPlayback || audioDevice.mHasPlayback) {
sendDeviceNotification(audioDevice, false);
}
if (audioDevice.mHasMIDI) {
try {
mMidiManager.alsaDeviceRemoved(device);
} catch (RemoteException e) {
Slog.e(TAG, "MIDI Manager dead", e);
}
}
}
UsbMidiDevice usbMidiDevice = mMidiDevices.remove(usbDevice);
if (usbMidiDevice != null) {
IoUtils.closeQuietly(usbMidiDevice);
}
mSelectedAudioDevice = null;
......@@ -400,10 +397,14 @@ public class UsbAlsaManager {
// Logging
//
public void dump(FileDescriptor fd, PrintWriter pw) {
pw.println(" USB AudioDevices:");
pw.println(" USB Audio Devices:");
for (UsbDevice device : mAudioDevices.keySet()) {
pw.println(" " + device.getDeviceName() + ": " + mAudioDevices.get(device));
}
pw.println(" USB MIDI Devices:");
for (UsbDevice device : mMidiDevices.keySet()) {
pw.println(" " + device.getDeviceName() + ": " + mMidiDevices.get(device));
}
}
public void logDevicesList(String title) {
......
......@@ -24,7 +24,6 @@ public final class UsbAudioDevice {
public int mDevice;
public boolean mHasPlayback;
public boolean mHasCapture;
public boolean mHasMIDI;
// Device "class" flags
public static final int kAudioDeviceClassMask = 0x00FFFFFF;
......@@ -41,12 +40,11 @@ public final class UsbAudioDevice {
public String mDeviceDescription = "";
public UsbAudioDevice(int card, int device,
boolean hasPlayback, boolean hasCapture, boolean hasMidi, int deviceClass) {
boolean hasPlayback, boolean hasCapture, int deviceClass) {
mCard = card;
mDevice = device;
mHasPlayback = hasPlayback;
mHasCapture = hasCapture;
mHasMIDI = hasMidi;
mDeviceClass = deviceClass;
}
......@@ -58,7 +56,6 @@ public final class UsbAudioDevice {
sb.append(", description: " + mDeviceDescription);
sb.append(", hasPlayback: " + mHasPlayback);
sb.append(", hasCapture: " + mHasCapture);
sb.append(", hasMidi: " + mHasMIDI);
sb.append(", class: 0x" + Integer.toHexString(mDeviceClass) + "]");
return sb.toString();
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment