diff options
22 files changed, 1210 insertions, 1086 deletions
@@ -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 \ diff --git a/core/java/android/midi/MidiDevice.aidl b/core/java/android/midi/IMidiDeviceServer.aidl index 11bb497..31fdbbb 100644 --- a/core/java/android/midi/MidiDevice.aidl +++ b/core/java/android/midi/IMidiDeviceServer.aidl @@ -1,11 +1,11 @@ /* - * 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); +} diff --git a/core/java/android/midi/IMidiManager.aidl b/core/java/android/midi/IMidiManager.aidl index 7a9f887..575b525 100644 --- a/core/java/android/midi/IMidiManager.aidl +++ b/core/java/android/midi/IMidiManager.aidl @@ -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); } diff --git a/core/java/android/midi/MidiDevice.java b/core/java/android/midi/MidiDevice.java index 7f83896..8aaa86d 100644 --- a/core/java/android/midi/MidiDevice.java +++ b/core/java/android/midi/MidiDevice.java @@ -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) */]; - } } diff --git a/core/java/android/midi/MidiDeviceInfo.java b/core/java/android/midi/MidiDeviceInfo.java index 5b57696..5cf62b5 100644 --- a/core/java/android/midi/MidiDeviceInfo.java +++ b/core/java/android/midi/MidiDeviceInfo.java @@ -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); } } diff --git a/core/java/android/midi/MidiDeviceServer.java b/core/java/android/midi/MidiDeviceServer.java new file mode 100644 index 0000000..6ce4bf7 --- /dev/null +++ b/core/java/android/midi/MidiDeviceServer.java @@ -0,0 +1,264 @@ +/* + * 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); + } + } + } + }; + } +} diff --git a/core/java/android/midi/MidiInputPort.java b/core/java/android/midi/MidiInputPort.java index 31449a5..5d806cf 100644 --- a/core/java/android/midi/MidiInputPort.java +++ b/core/java/android/midi/MidiInputPort.java @@ -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(); + } } diff --git a/core/java/android/midi/MidiManager.java b/core/java/android/midi/MidiManager.java index 64cd4fe..2c1c7bf 100644 --- a/core/java/android/midi/MidiManager.java +++ b/core/java/android/midi/MidiManager.java @@ -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); } + } diff --git a/core/java/android/midi/MidiOutputPort.java b/core/java/android/midi/MidiOutputPort.java index 7ce286b..8b2d65d 100644 --- a/core/java/android/midi/MidiOutputPort.java +++ b/core/java/android/midi/MidiOutputPort.java @@ -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(); } } diff --git a/core/java/android/midi/MidiPort.java b/core/java/android/midi/MidiPort.java index fdd0233..53cba21 100644 --- a/core/java/android/midi/MidiPort.java +++ b/core/java/android/midi/MidiPort.java @@ -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; + } } diff --git a/services/core/java/com/android/server/MidiService.java b/services/core/java/com/android/server/MidiService.java new file mode 100644 index 0000000..38f1cb8 --- /dev/null +++ b/services/core/java/com/android/server/MidiService.java @@ -0,0 +1,306 @@ +/* + * 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; + +import android.content.Context; +import android.midi.IMidiDeviceServer; +import android.midi.IMidiListener; +import android.midi.IMidiManager; +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; + +public class MidiService extends IMidiManager.Stub { + private static final String TAG = "MidiService"; + + private final Context mContext; + + // 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 MidiDeviceInfo + private final HashMap<MidiDeviceInfo, Device> mDevicesByInfo + = new HashMap<MidiDeviceInfo, Device>(); + + // 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>(); + + public Client(IBinder token) { + mToken = token; + mUid = Binder.getCallingUid(); + mPid = Binder.getCallingPid(); + } + + public int getUid() { + return mUid; + } + + public void addListener(IMidiListener listener) { + mListeners.add(listener); + } + + public void removeListener(IMidiListener listener) { + mListeners.remove(listener); + if (mListeners.size() == 0) { + removeClient(mToken); + } + } + + public void deviceAdded(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.onDeviceAdded(deviceInfo); + } + } catch (RemoteException e) { + Log.e(TAG, "remote exception", e); + } + } + + 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(deviceInfo); + } + } catch (RemoteException e) { + Log.e(TAG, "remote exception", e); + } + } + + 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) { + Client client = mClients.get(token); + if (client == null) { + client = new Client(token); + + try { + token.linkToDeath(client, 0); + } catch (RemoteException e) { + return null; + } + mClients.put(token, client); + } + return client; + } + } + + private void removeClient(IBinder token) { + 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) { + mContext = context; + } + + public void registerListener(IBinder token, IMidiListener listener) { + Client client = getClient(token); + if (client == null) return; + client.addListener(listener); + } + + public void unregisterListener(IBinder token, IMidiListener listener) { + Client client = getClient(token); + if (client == null) return; + client.removeListener(listener); + } + + public MidiDeviceInfo[] getDeviceList() { + return mDevicesByInfo.keySet().toArray(new MidiDeviceInfo[0]); + } + + 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; + } + + if (!device.isUidAllowed(Binder.getCallingUid())) { + throw new SecurityException("Attempt to open private device with wrong UID"); + } + + return device.getDeviceServer(); + } + + 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"); + } + + MidiDeviceInfo deviceInfo; + Device device; + + 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; + } + mDevicesByInfo.put(deviceInfo, device); + mDevicesByServer.put(server.asBinder(), device); + } + + synchronized (mClients) { + for (Client c : mClients.values()) { + c.deviceAdded(device); + } + } + + return deviceInfo; + } + + public void unregisterDeviceServer(IMidiDeviceServer server) { + synchronized (mDevicesByServer) { + removeDeviceLocked(mDevicesByServer.get(server.asBinder())); + } + } + + // 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(device); + } + } + } + } + + @Override + public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, TAG); + final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " "); + + pw.println("MIDI Manager State:"); + pw.increaseIndent(); + + pw.println("Devices:"); + pw.increaseIndent(); + for (Device device : mDevicesByInfo.values()) { + pw.println(device.toString()); + } + pw.decreaseIndent(); + + pw.println("Clients:"); + pw.increaseIndent(); + for (Client client : mClients.values()) { + pw.println(client.toString()); + } + pw.decreaseIndent(); + } +} diff --git a/services/core/java/com/android/server/midi/MidiDeviceBase.java b/services/core/java/com/android/server/midi/MidiDeviceBase.java deleted file mode 100644 index 4289584..0000000 --- a/services/core/java/com/android/server/midi/MidiDeviceBase.java +++ /dev/null @@ -1,170 +0,0 @@ -/* - * 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; -} diff --git a/services/core/java/com/android/server/midi/MidiService.java b/services/core/java/com/android/server/midi/MidiService.java deleted file mode 100644 index 29a8339..0000000 --- a/services/core/java/com/android/server/midi/MidiService.java +++ /dev/null @@ -1,283 +0,0 @@ -/* - * 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.content.Context; -import android.hardware.usb.UsbDevice; -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.RemoteException; -import android.util.Log; - -import java.util.ArrayList; -import java.util.HashMap; - -public class MidiService extends IMidiManager.Stub { - private static final String TAG = "MidiService"; - - private final Context mContext; - - // 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 USB devices, keyed by USB device. - private final HashMap<UsbDevice, UsbMidiDevice> mUsbDevices - = new HashMap<UsbDevice, UsbMidiDevice>(); - - // used for assigning IDs to MIDI devices - private int mNextDeviceId = 1; - - private final class Client implements IBinder.DeathRecipient { - private final IBinder mToken; - private final ArrayList<IMidiListener> mListeners = new ArrayList<IMidiListener>(); - private final ArrayList<MidiDeviceBase> mVirtualDevices = new ArrayList<MidiDeviceBase>(); - - public Client(IBinder token) { - mToken = token; - } - - public void close() { - for (MidiDeviceBase device : mVirtualDevices) { - device.close(); - } - mVirtualDevices.clear(); - } - - public void addListener(IMidiListener listener) { - mListeners.add(listener); - } - - public void removeListener(IMidiListener listener) { - mListeners.remove(listener); - if (mListeners.size() == 0 && mVirtualDevices.size() == 0) { - removeClient(mToken); - } - } - - public void addVirtualDevice(MidiDeviceBase device) { - mVirtualDevices.add(device); - } - - public void removeVirtualDevice(MidiDeviceBase device) { - mVirtualDevices.remove(device); - } - - public void deviceAdded(MidiDeviceInfo device) { - try { - for (IMidiListener listener : mListeners) { - listener.onDeviceAdded(device); - } - } catch (RemoteException e) { - Log.e(TAG, "remote exception", e); - } - } - - public void deviceRemoved(MidiDeviceInfo device) { - try { - for (IMidiListener listener : mListeners) { - listener.onDeviceRemoved(device); - } - } catch (RemoteException e) { - Log.e(TAG, "remote exception", e); - } - } - - public void binderDied() { - removeClient(mToken); - } - } - - - private Client getClient(IBinder token) { - synchronized (mClients) { - Client client = mClients.get(token); - if (client == null) { - client = new Client(token); - - try { - token.linkToDeath(client, 0); - } catch (RemoteException e) { - return null; - } - mClients.put(token, client); - } - return client; - } - } - - private void removeClient(IBinder token) { - synchronized (mClients) { - Client client = mClients.remove(token); - if (client != null) { - client.close(); - } - } - } - - public MidiService(Context context) { - mContext = context; - } - - public void registerListener(IBinder token, IMidiListener listener) { - Client client = getClient(token); - if (client == null) return; - client.addListener(listener); - } - - public void unregisterListener(IBinder token, IMidiListener listener) { - Client client = getClient(token); - if (client == null) return; - client.removeListener(listener); - } - - public MidiDeviceInfo[] getDeviceList() { - ArrayList<MidiDeviceInfo> infos = new ArrayList<MidiDeviceInfo>(); - for (MidiDeviceBase device : mDevices.values()) { - infos.add(device.getInfo()); - } - return infos.toArray(new MidiDeviceInfo[0]); - } - - public ParcelFileDescriptor openDevice(IBinder token, MidiDeviceInfo deviceInfo) { - MidiDeviceBase device = mDevices.get(deviceInfo.getId()); - if (device == null) { - Log.e(TAG, "device not found in openDevice: " + deviceInfo); - return null; - } - - return device.getFileDescriptor(); - } - - public MidiDevice registerVirtualDevice(IBinder token, int numInputPorts, int numOutputPorts, - Bundle properties) { - VirtualMidiDevice device; - Client client = getClient(token); - if (client == null) return null; - - synchronized (mDevices) { - int id = mNextDeviceId++; - MidiDeviceInfo deviceInfo = new MidiDeviceInfo(MidiDeviceInfo.TYPE_VIRTUAL, id, - numInputPorts, numOutputPorts, properties); - - device = new VirtualMidiDevice(deviceInfo); - if (!device.open()) { - return null; - } - mDevices.put(id, device); - client.addVirtualDevice(device); - } - - synchronized (mClients) { - MidiDeviceInfo deviceInfo = device.getInfo(); - for (Client c : mClients.values()) { - c.deviceAdded(deviceInfo); - } - } - - return device.getProxy(); - } - - public void unregisterVirtualDevice(IBinder token, MidiDeviceInfo deviceInfo) { - Client client = getClient(token); - if (client == null) return; - - MidiDeviceBase device; - synchronized (mDevices) { - device = mDevices.remove(deviceInfo.getId()); - } - - if (device != null) { - client.removeVirtualDevice(device); - device.close(); - - synchronized (mClients) { - for (Client c : mClients.values()) { - c.deviceRemoved(deviceInfo); - } - } - } - } - - // 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); - } - - synchronized (mClients) { - for (Client client : mClients.values()) { - client.deviceAdded(deviceInfo); - } - } - } - - // 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()); - } - } - - Log.d(TAG, "alsaDeviceRemoved: " + deviceInfo); - - if (deviceInfo != null) { - synchronized (mClients) { - for (Client client : mClients.values()) { - client.deviceRemoved(deviceInfo); - } - } - } - } -} diff --git a/services/core/java/com/android/server/midi/UsbMidiDevice.java b/services/core/java/com/android/server/midi/UsbMidiDevice.java deleted file mode 100644 index 3d42c67..0000000 --- a/services/core/java/com/android/server/midi/UsbMidiDevice.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * 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); - } -} - diff --git a/services/core/java/com/android/server/midi/VirtualMidiDevice.java b/services/core/java/com/android/server/midi/VirtualMidiDevice.java deleted file mode 100644 index 5b39045..0000000 --- a/services/core/java/com/android/server/midi/VirtualMidiDevice.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * 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); - } -} diff --git a/services/core/jni/Android.mk b/services/core/jni/Android.mk index dc073ad..7b74e91 100644 --- a/services/core/jni/Android.mk +++ b/services/core/jni/Android.mk @@ -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 \ diff --git a/services/core/jni/com_android_server_UsbMidiDevice.cpp b/services/core/jni/com_android_server_UsbMidiDevice.cpp new file mode 100644 index 0000000..94853b8 --- /dev/null +++ b/services/core/jni/com_android_server_UsbMidiDevice.cpp @@ -0,0 +1,121 @@ +/* + * 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)); +} + +}; diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp index c65b3be..7db7414 100644 --- a/services/core/jni/onload.cpp +++ b/services/core/jni/onload.cpp @@ -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); diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index eca398c..7f9af31 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -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; diff --git a/services/usb/java/com/android/server/usb/UsbAlsaManager.java b/services/usb/java/com/android/server/usb/UsbAlsaManager.java index 32ca723..6589135 100644 --- a/services/usb/java/com/android/server/usb/UsbAlsaManager.java +++ b/services/usb/java/com/android/server/usb/UsbAlsaManager.java @@ -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) { diff --git a/services/usb/java/com/android/server/usb/UsbAudioDevice.java b/services/usb/java/com/android/server/usb/UsbAudioDevice.java index b7b9563..069d917 100644 --- a/services/usb/java/com/android/server/usb/UsbAudioDevice.java +++ b/services/usb/java/com/android/server/usb/UsbAudioDevice.java @@ -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(); } diff --git a/services/usb/java/com/android/server/usb/UsbMidiDevice.java b/services/usb/java/com/android/server/usb/UsbMidiDevice.java new file mode 100644 index 0000000..aaac331 --- /dev/null +++ b/services/usb/java/com/android/server/usb/UsbMidiDevice.java @@ -0,0 +1,194 @@ +/* + * 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.hardware.usb.UsbDevice; +import android.midi.MidiDeviceInfo; +import android.midi.MidiDeviceServer; +import android.midi.MidiManager; +import android.midi.MidiReceiver; +import android.midi.MidiSender; +import android.midi.MidiUtils; +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 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 MidiDeviceServer mServer; + private final MidiReceiver[] mOutputPortReceivers; + + // for polling multiple FileDescriptors for MIDI events + private final StructPollfd[] mPollFDs; + private final FileInputStream[] mInputStreams; + private final FileOutputStream[] mOutputStreams; + + public static UsbMidiDevice create(Context context, UsbDevice usbDevice, int card, int device) { + MidiManager midiManager = (MidiManager)context.getSystemService(Context.MIDI_SERVICE); + if (midiManager == null) { + Log.e(TAG, "No MidiManager in UsbMidiDevice.create()"); + return null; + } + + // FIXME - support devices with different number of input and output ports + int subDevices = nativeGetSubdeviceCount(card, device); + if (subDevices <= 0) { + Log.e(TAG, "nativeGetSubdeviceCount failed"); + return null; + } + + // FIXME - support devices with different number of input and output ports + FileDescriptor[] fileDescriptors = nativeOpen(card, device, subDevices); + if (fileDescriptors == null) { + Log.e(TAG, "nativeOpen failed"); + return null; + } + + 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.putInt(MidiDeviceInfo.PROPERTY_ALSA_CARD, card); + properties.putInt(MidiDeviceInfo.PROPERTY_ALSA_DEVICE, device); + properties.putParcelable(MidiDeviceInfo.PROPERTY_USB_DEVICE, usbDevice); + MidiDeviceServer server = midiManager.createDeviceServer(subDevices, subDevices, properties, + false, MidiDeviceInfo.TYPE_USB); + if (server == null) { + Log.e(TAG, "createDeviceServer failed"); + return null; + } + + return new UsbMidiDevice(server, fileDescriptors, fileDescriptors); + } + + private UsbMidiDevice(MidiDeviceServer server, FileDescriptor[] inputFiles, + FileDescriptor[] outputFiles) { + mServer = server; + int inputCount = inputFiles.length; + int outputCount = outputFiles.length; + + mPollFDs = new StructPollfd[inputCount]; + mInputStreams = new FileInputStream[inputCount]; + for (int i = 0; i < inputCount; i++) { + FileDescriptor fd = inputFiles[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]; + for (int i = 0; i < outputCount; i++) { + mOutputStreams[i] = new FileOutputStream(outputFiles[i]); + } + + mOutputPortReceivers = new MidiReceiver[outputCount]; + for (int port = 0; port < outputCount; port++) { + mOutputPortReceivers[port] = server.openOutputPortReceiver(port); + } + + for (int port = 0; port < inputCount; port++) { + final int portNumberF = port; + MidiReceiver receiver = new MidiReceiver() { + + @Override + public void onPost(byte[] data, int offset, int count, long timestamp) + throws IOException { + // FIXME - timestamps are ignored, future posting not supported yet. + mOutputStreams[portNumberF].write(data, offset, count); + } + }; + MidiSender sender = server.openInputPortSender(port); + sender.connect(receiver); + } + + new Thread() { + @Override + public void run() { + byte[] buffer = new byte[3]; + try { + while (true) { + // look for a readable FileDescriptor + for (int index = 0; index < mPollFDs.length; index++) { + StructPollfd pfd = mPollFDs[index]; + if ((pfd.revents & OsConstants.POLLIN) != 0) { + // clear readable flag + pfd.revents = 0; + int count = readMessage(buffer, index); + mOutputPortReceivers[index].onPost(buffer, 0, count, System.nanoTime()); + } + } + + // poll if none are readable + Os.poll(mPollFDs, -1 /* infinite timeout */); + } + } catch (IOException e) { + Log.d(TAG, "reader thread exiting"); + } catch (ErrnoException e) { + Log.d(TAG, "reader thread exiting"); + } + } + }.start(); + } + + @Override + public void close() throws IOException { + mServer.close(); + + for (int i = 0; i < mInputStreams.length; i++) { + mInputStreams[i].close(); + } + for (int i = 0; i < mOutputStreams.length; i++) { + mOutputStreams[i].close(); + } + } + + private int readMessage(byte[] buffer, int index) throws IOException { + FileInputStream inputStream = mInputStreams[index]; + + if (inputStream.read(buffer, 0, 1) != 1) { + Log.e(TAG, "could not read command byte"); + return -1; + } + int dataSize = MidiUtils.getMessageDataSize(buffer[0]); + if (dataSize < 0) { + return -1; + } + if (dataSize > 0) { + if (inputStream.read(buffer, 1, dataSize) != dataSize) { + Log.e(TAG, "could not read command data"); + return -1; + } + } + return dataSize + 1; + } + + private static native int nativeGetSubdeviceCount(int card, int device); + private static native FileDescriptor[] nativeOpen(int card, int device, int subdeviceCount); +} |