MIDI Manager work in progress
Still to do:
Add MidiInputPort and MidiOutputPort classes
Schedule sending MIDI events in the future
Security/permissions
Reconsider interface for virtual devices
Look into performance optimizations
Change-Id: I9b7d63b196996a04be0a830efa913043da1328a8
diff --git a/Android.mk b/Android.mk
index ec0c235..2dc210f 100644
--- a/Android.mk
+++ b/Android.mk
@@ -172,6 +172,8 @@
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/IMidiListener.aidl \
+ core/java/android/midi/IMidiManager.aidl \
core/java/android/net/IConnectivityManager.aidl \
core/java/android/net/IEthernetManager.aidl \
core/java/android/net/IEthernetServiceListener.aidl \
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index 12fee2d..eadf5e9 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -80,6 +80,8 @@
import android.media.session.MediaSessionManager;
import android.media.tv.ITvInputManager;
import android.media.tv.TvInputManager;
+import android.midi.IMidiManager;
+import android.midi.MidiManager;
import android.net.ConnectivityManager;
import android.net.IConnectivityManager;
import android.net.EthernetManager;
@@ -777,6 +779,12 @@
IBinder b = ServiceManager.getService(APPWIDGET_SERVICE);
return new AppWidgetManager(ctx, IAppWidgetService.Stub.asInterface(b));
}});
+
+ registerService(MIDI_SERVICE, new ServiceFetcher() {
+ public Object createService(ContextImpl ctx) {
+ IBinder b = ServiceManager.getService(MIDI_SERVICE);
+ return new MidiManager(ctx, IMidiManager.Stub.asInterface(b));
+ }});
}
static ContextImpl getImpl(Context context) {
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index e6bb09f..b0bfdb6 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -2142,6 +2142,7 @@
MEDIA_SESSION_SERVICE,
BATTERY_SERVICE,
JOB_SCHEDULER_SERVICE,
+ MIDI_SERVICE,
})
@Retention(RetentionPolicy.SOURCE)
public @interface ServiceName {}
@@ -2915,6 +2916,15 @@
public static final String MEDIA_PROJECTION_SERVICE = "media_projection";
/**
+ * Use with {@link #getSystemService} to retrieve a
+ * {@link android.midi.MidiManager} for accessing the MIDI service.
+ *
+ * @see #getSystemService
+ * @hide
+ */
+ public static final String MIDI_SERVICE = "midi";
+
+ /**
* Determine whether the given permission is allowed for a particular
* process and user ID running in the system.
*
diff --git a/core/java/android/midi/IMidiListener.aidl b/core/java/android/midi/IMidiListener.aidl
new file mode 100644
index 0000000..b650593
--- /dev/null
+++ b/core/java/android/midi/IMidiListener.aidl
@@ -0,0 +1,26 @@
+/*
+ * 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.midi.MidiDeviceInfo;
+
+/** @hide */
+oneway interface IMidiListener
+{
+ void onDeviceAdded(in MidiDeviceInfo device);
+ void onDeviceRemoved(in MidiDeviceInfo device);
+}
diff --git a/core/java/android/midi/IMidiManager.aidl b/core/java/android/midi/IMidiManager.aidl
new file mode 100644
index 0000000..06b675b
--- /dev/null
+++ b/core/java/android/midi/IMidiManager.aidl
@@ -0,0 +1,46 @@
+/*
+ * 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.hardware.usb.UsbDevice;
+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
+{
+ MidiDeviceInfo[] getDeviceList();
+
+ // for device creation & removal notifications
+ void registerListener(IBinder token, in IMidiListener listener);
+ void unregisterListener(IBinder token, in IMidiListener listener);
+
+ // for communicating with MIDI devices
+ ParcelFileDescriptor openDevice(IBinder token, in MidiDeviceInfo device);
+
+ // for implementing virtual MIDI devices
+ MidiDevice registerVirtualDevice(IBinder token, 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);
+}
diff --git a/core/java/android/midi/MidiDevice.aidl b/core/java/android/midi/MidiDevice.aidl
new file mode 100644
index 0000000..11bb497
--- /dev/null
+++ b/core/java/android/midi/MidiDevice.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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;
+
+parcelable MidiDevice;
diff --git a/core/java/android/midi/MidiDevice.java b/core/java/android/midi/MidiDevice.java
new file mode 100644
index 0000000..8954d2b
--- /dev/null
+++ b/core/java/android/midi/MidiDevice.java
@@ -0,0 +1,292 @@
+/*
+ * 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.Parcel;
+import android.os.Parcelable;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+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 {
+ private static final String TAG = "MidiDevice";
+
+ private final MidiDeviceInfo mDeviceInfo;
+ private ParcelFileDescriptor mParcelFileDescriptor;
+ private FileInputStream mInputStream;
+ private FileOutputStream mOutputStream;
+ private final ArrayList<MidiReceiver> mReceivers = new ArrayList<MidiReceiver>();
+
+ /**
+ * Minimum size of packed message as sent through our ParcelFileDescriptor
+ * 8 bytes for timestamp and 1 to 3 bytes for message
+ * @hide
+ */
+ public static final int MIN_PACKED_MESSAGE_SIZE = 9;
+
+ /**
+ * Maximum size of packed message as sent through our ParcelFileDescriptor
+ * 8 bytes for timestamp and 1 to 3 bytes for message
+ * @hide
+ */
+ public static final int MAX_PACKED_MESSAGE_SIZE = 11;
+
+ // 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);
+
+ synchronized (mReceivers) {
+ for (int i = 0; i < mReceivers.size(); i++) {
+ MidiReceiver receiver = mReceivers.get(i);
+ try {
+ mReceivers.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) {
+ mReceivers.remove(receiver);
+ }
+ deadReceivers.clear();
+ }
+ // exit if we have no receivers left
+ if (mReceivers.size() == 0) {
+ break;
+ }
+ }
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "read failed");
+ }
+ }
+ };
+
+ // This is the receiver that clients use for sending events to this device.
+ private final MidiReceiver mReceiver = new MidiReceiver() {
+ private final byte[] mBuffer = new byte[MAX_PACKED_MESSAGE_SIZE];
+ public void onPost(byte[] msg, int offset, int count, long timestamp) throws IOException {
+ synchronized (mBuffer) {
+ int length = packMessage(msg, offset, count, timestamp, mBuffer);
+ mOutputStream.write(mBuffer, 0, length);
+ }
+ }
+ };
+
+ // Our MidiSender object, to which clients can attach MidiReceivers.
+ private final MidiSender mSender = new MidiSender() {
+ public void connect(MidiReceiver receiver) {
+ synchronized (mReceivers) {
+ if (mReceivers.size() == 0) {
+ mThread.start();
+ }
+ mReceivers.add(receiver);
+ }
+ }
+
+ public void disconnect(MidiReceiver receiver) {
+ synchronized (mReceivers) {
+ mReceivers.remove(receiver);
+ if (mReceivers.size() == 0) {
+ // ???
+ }
+ }
+ }
+ };
+
+ /**
+ * MidiDevice should only be instantiated by MidiManager or MidiService
+ * @hide
+ */
+ public MidiDevice(MidiDeviceInfo deviceInfo, ParcelFileDescriptor pfd) {
+ mDeviceInfo = deviceInfo;
+ mParcelFileDescriptor = pfd;
+ }
+
+ public 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;
+ }
+
+ void close() {
+ try {
+ if (mInputStream != null) {
+ mInputStream.close();
+ }
+ if (mOutputStream != null) {
+ mOutputStream.close();
+ }
+ mParcelFileDescriptor.close();
+ } catch (IOException e) {
+ }
+ }
+
+ // returns our MidiDeviceInfo object, which describes this device
+ public MidiDeviceInfo getInfo() {
+ return mDeviceInfo;
+ }
+
+ // returns our MidiReceiver, which clients can use for sending events to this device.
+ public MidiReceiver getReceiver() {
+ return mReceiver;
+ }
+
+ // Returns our MidiSender object, to which clients can attach MidiReceivers.
+ public MidiSender getSender() {
+ return mSender;
+ }
+
+ @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];
+ }
+ };
+
+ public int describeContents() {
+ return 0;
+ }
+
+ 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,
+ 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
+ *
+ * @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
+ return bufferLength - 8;
+ }
+
+ /**
+ * 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;
+ }
+}
diff --git a/core/java/android/midi/MidiDeviceInfo.aidl b/core/java/android/midi/MidiDeviceInfo.aidl
new file mode 100644
index 0000000..59be059
--- /dev/null
+++ b/core/java/android/midi/MidiDeviceInfo.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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;
+
+parcelable MidiDeviceInfo;
diff --git a/core/java/android/midi/MidiDeviceInfo.java b/core/java/android/midi/MidiDeviceInfo.java
new file mode 100644
index 0000000..def6878c
--- /dev/null
+++ b/core/java/android/midi/MidiDeviceInfo.java
@@ -0,0 +1,193 @@
+/*
+ * 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.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * This class contains information to describe a MIDI device.
+ * For now we only have information that can be retrieved easily for USB devices,
+ * but we will probably expand this in the future.
+ *
+ * This class is just an immutable object to encapsulate the MIDI device description.
+ * Use the MidiDevice class to actually communicate with devices.
+ *
+ * @hide
+ */
+public class MidiDeviceInfo implements Parcelable {
+
+ private static final String TAG = "MidiDeviceInfo";
+
+ public static final int TYPE_USB = 1;
+ public static final int TYPE_VIRTUAL = 2;
+
+ private final int mType; // USB or virtual
+ private final int mId; // unique ID generated by MidiService
+ 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}.
+ * Matches the USB device manufacturer name string for USB MIDI devices.
+ */
+ public static final String PROPERTY_MANUFACTURER = "manufacturer";
+
+ /**
+ * Bundle key for the device's model name property.
+ * Used with the {@link android.os.Bundle} returned by {@link #getProperties}
+ * Matches the USB device product name string for USB MIDI devices.
+ */
+ public static final String PROPERTY_MODEL = "model";
+
+ /**
+ * Bundle key for the device's serial number property.
+ * Used with the {@link android.os.Bundle} returned by {@link #getProperties}
+ * Matches the USB device serial number for USB MIDI devices.
+ */
+ public static final String PROPERTY_SERIAL_NUMBER = "serial_number";
+
+ /**
+ * Bundle key for the device's {@link android.hardware.usb.UsbDevice}.
+ * Only set for USB MIDI devices.
+ * Used with the {@link android.os.Bundle} returned by {@link #getProperties}
+ */
+ public static final String PROPERTY_USB_DEVICE = "usb_device";
+
+ /**
+ * MidiDeviceInfo should only be instantiated by MidiService implementation
+ * @hide
+ */
+ public MidiDeviceInfo(int type, int id, Bundle properties) {
+ mType = type;
+ mId = id;
+ mProperties = properties;
+ mAlsaCard = -1;
+ mAlsaDevice = -1;
+ }
+
+ /**
+ * MidiDeviceInfo should only be instantiated by MidiService implementation
+ * @hide
+ */
+ public MidiDeviceInfo(int type, int id, Bundle properties,
+ int alsaCard, int alsaDevice) {
+ mType = type;
+ mId = id;
+ mProperties = properties;
+ mAlsaCard = alsaCard;
+ mAlsaDevice = alsaDevice;
+ }
+
+ /**
+ * Returns the type of the device.
+ *
+ * @return the device's type
+ */
+ public int getType() {
+ return mType;
+ }
+
+ /**
+ * Returns the ID of the device.
+ * This ID is generated by the MIDI service and is not persistent across device unplugs.
+ *
+ * @return the device's ID
+ */
+ public int getId() {
+ return mId;
+ }
+
+ /**
+ * Returns the {@link android.os.Bundle} containing the device's properties.
+ *
+ * @return the device's properties
+ */
+ public Bundle getProperties() {
+ return mProperties;
+ }
+
+ /**
+ * @hide
+ */
+ public int getAlsaCard() {
+ return mAlsaCard;
+ }
+
+ /**
+ * @hide
+ */
+ public int getAlsaDevice() {
+ return mAlsaDevice;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof MidiDeviceInfo) {
+ return (((MidiDeviceInfo)o).mId == mId);
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return mId;
+ }
+
+ @Override
+ public String toString() {
+ return ("MidiDeviceInfo[mType=" + mType +
+ ",mId=" + mId +
+ ",mProperties=" + mProperties +
+ ",mAlsaCard=" + mAlsaCard +
+ ",mAlsaDevice=" + mAlsaDevice);
+ }
+
+ public static final Parcelable.Creator<MidiDeviceInfo> CREATOR =
+ new Parcelable.Creator<MidiDeviceInfo>() {
+ public MidiDeviceInfo createFromParcel(Parcel in) {
+ int type = in.readInt();
+ int id = in.readInt();
+ Bundle properties = in.readBundle();
+ int card = in.readInt();
+ int device = in.readInt();
+ return new MidiDeviceInfo(type, id, properties, card, device);
+ }
+
+ public MidiDeviceInfo[] newArray(int size) {
+ return new MidiDeviceInfo[size];
+ }
+ };
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeInt(mType);
+ parcel.writeInt(mId);
+ parcel.writeBundle(mProperties);
+ parcel.writeInt(mAlsaCard);
+ parcel.writeInt(mAlsaDevice);
+ }
+}
diff --git a/core/java/android/midi/MidiManager.java b/core/java/android/midi/MidiManager.java
new file mode 100644
index 0000000..ec869b7
--- /dev/null
+++ b/core/java/android/midi/MidiManager.java
@@ -0,0 +1,156 @@
+/*
+ * 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.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;
+
+import java.io.IOException;
+import java.util.HashMap;
+
+/**
+ * This class is the public application interface to the MIDI service.
+ *
+ * <p>You can obtain an instance of this class by calling
+ * {@link android.content.Context#getSystemService(java.lang.String) Context.getSystemService()}.
+ *
+ * {@samplecode
+ * MidiManager manager = (MidiManager) getSystemService(Context.MIDI_SERVICE);}
+ * @hide
+ */
+public class MidiManager {
+ private static final String TAG = "MidiManager";
+
+ private final Context mContext;
+ private final IMidiManager mService;
+ private final IBinder mToken = new Binder();
+
+ private HashMap<DeviceCallback,DeviceListener> mDeviceListeners =
+ new HashMap<DeviceCallback,DeviceListener>();
+
+ // Binder stub for receiving device notifications from MidiService
+ private class DeviceListener extends IMidiListener.Stub {
+ private DeviceCallback mCallback;
+
+ public DeviceListener(DeviceCallback callback) {
+ mCallback = callback;
+ }
+
+ public void onDeviceAdded(MidiDeviceInfo device) {
+ mCallback.onDeviceAdded(device);
+ }
+
+ public void onDeviceRemoved(MidiDeviceInfo device) {
+ mCallback.onDeviceRemoved(device);
+ }
+ }
+
+ // Callback interface clients to receive Device added and removed notifications
+ public interface DeviceCallback {
+ void onDeviceAdded(MidiDeviceInfo device);
+ void onDeviceRemoved(MidiDeviceInfo device);
+ }
+
+ /**
+ * @hide
+ */
+ public MidiManager(Context context, IMidiManager service) {
+ mContext = context;
+ mService = service;
+ }
+
+ // Used by clients to register for Device added and removed notifications
+ public void registerDeviceCallback(DeviceCallback callback) {
+ DeviceListener deviceListener = new DeviceListener(callback);
+ try {
+ mService.registerListener(mToken, deviceListener);
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException in registerDeviceListener");
+ return;
+ }
+ mDeviceListeners.put(callback, deviceListener);
+ }
+
+ // Used by clients to unregister for device added and removed notifications
+ public void unregisterDeviceCallback(DeviceCallback callback) {
+ DeviceListener deviceListener = mDeviceListeners.remove(callback);
+ if (deviceListener != null) {
+ try {
+ mService.unregisterListener(mToken, deviceListener);
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException in unregisterDeviceListener");
+ }
+ }
+ }
+
+ public MidiDeviceInfo[] getDeviceList() {
+ try {
+ return mService.getDeviceList();
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException in getDeviceList");
+ return new MidiDeviceInfo[0];
+ }
+ }
+
+ // Use this if you want to communicate with a MIDI device.
+ public MidiDevice openDevice(MidiDeviceInfo deviceInfo) {
+ try {
+ ParcelFileDescriptor pfd = mService.openDevice(mToken, deviceInfo);
+ if (pfd == 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;
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException in openDevice");
+ }
+ return null;
+ }
+
+ // Use this if you want to register and implement a virtual device.
+ // The MidiDevice returned by this method is the proxy you use to implement the device.
+ public MidiDevice createVirtualDevice(Bundle properties) {
+ try {
+ MidiDevice device = mService.registerVirtualDevice(mToken, properties);
+ if (device != null && !device.open()) {
+ device = null;
+ }
+ return device;
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException in createVirtualDevice");
+ return null;
+ }
+ }
+
+ public void closeVirtualDevice(MidiDevice device) {
+ try {
+ device.close();
+ mService.unregisterVirtualDevice(mToken, device.getInfo());
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException in unregisterVirtualDevice");
+ }
+ }
+}
diff --git a/core/java/android/midi/MidiReceiver.java b/core/java/android/midi/MidiReceiver.java
new file mode 100644
index 0000000..1101105
--- /dev/null
+++ b/core/java/android/midi/MidiReceiver.java
@@ -0,0 +1,30 @@
+/*
+ * 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 java.io.IOException;
+
+/**
+ * Interface for receiving events from a MIDI device.
+ *
+ * @hide
+ */
+public interface MidiReceiver {
+ // NOTE: the msg array is only valid within the context of this call.
+ // the byte array may get reused by the MIDI device for the next message.
+ public void onPost(byte[] msg, int offset, int count, long timestamp) throws IOException;
+}
diff --git a/core/java/android/midi/MidiSender.java b/core/java/android/midi/MidiSender.java
new file mode 100644
index 0000000..cba7079
--- /dev/null
+++ b/core/java/android/midi/MidiSender.java
@@ -0,0 +1,28 @@
+/*
+ * 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;
+
+/**
+ * Interface provided by a device to allow attaching
+ * MidiReceivers to a MIDI device.
+ *
+ * @hide
+ */
+public interface MidiSender {
+ public void connect(MidiReceiver receiver);
+ public void disconnect(MidiReceiver receiver);
+}
diff --git a/core/java/android/midi/MidiUtils.java b/core/java/android/midi/MidiUtils.java
new file mode 100644
index 0000000..f80e83a
--- /dev/null
+++ b/core/java/android/midi/MidiUtils.java
@@ -0,0 +1,63 @@
+/*
+ * 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.util.Log;
+
+/**
+ * Class containing miscellaneous MIDI utilities.
+ *
+ * @hide
+ */
+public final class MidiUtils {
+ private static final String TAG = "MidiUtils";
+
+ /**
+ * Returns data size of a MIDI message based on the message's command byte
+ * @param b the message command byte
+ * @return the message's data length
+ */
+ public static int getMessageDataSize(byte b) {
+ switch (b & 0xF0) {
+ case 0x80:
+ case 0x90:
+ case 0xA0:
+ case 0xB0:
+ case 0xE0:
+ return 2;
+ case 0xC0:
+ case 0xD0:
+ return 1;
+ case 0xF0:
+ switch (b & 0x0F) {
+ case 0x00:
+ Log.e(TAG, "System Exclusive not supported yet");
+ return -1;
+ case 0x01:
+ case 0x03:
+ return 1;
+ case 0x02:
+ return 2;
+ default:
+ return 0;
+ }
+ default:
+ Log.e(TAG, "unknown MIDI command " + b);
+ return -1;
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/midi/MidiDeviceBase.java b/services/core/java/com/android/server/midi/MidiDeviceBase.java
new file mode 100644
index 0000000..4289584
--- /dev/null
+++ b/services/core/java/com/android/server/midi/MidiDeviceBase.java
@@ -0,0 +1,170 @@
+/*
+ * 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
new file mode 100644
index 0000000..ff8dda0
--- /dev/null
+++ b/services/core/java/com/android/server/midi/MidiService.java
@@ -0,0 +1,277 @@
+/*
+ * 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, 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,
+ 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);
+
+ deviceInfo = new MidiDeviceInfo(MidiDeviceInfo.TYPE_USB, id, 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
new file mode 100644
index 0000000..1bc91f05
--- /dev/null
+++ b/services/core/java/com/android/server/midi/UsbMidiDevice.java
@@ -0,0 +1,113 @@
+/*
+ * 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(), 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);
+ 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
new file mode 100644
index 0000000..5b39045
--- /dev/null
+++ b/services/core/java/com/android/server/midi/VirtualMidiDevice.java
@@ -0,0 +1,91 @@
+/*
+ * 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/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 0ccb25c..db60866 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -74,6 +74,7 @@
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.net.NetworkPolicyManagerService;
import com.android.server.net.NetworkStatsService;
import com.android.server.notification.NotificationManagerService;
@@ -410,6 +411,7 @@
AudioService audioService = null;
MmsServiceBroker mmsService = null;
EntropyMixer entropyMixer = null;
+ MidiService midiService = null;
boolean disableStorage = SystemProperties.getBoolean("config.disable_storage", false);
boolean disableMedia = SystemProperties.getBoolean("config.disable_media", false);
@@ -521,6 +523,7 @@
LockSettingsService lockSettings = null;
AssetAtlasService atlas = null;
MediaRouterService mediaRouter = null;
+ MidiService midi = null;
// Bring up services needed for UI.
if (mFactoryTestMode != FactoryTest.FACTORY_TEST_LOW_LEVEL) {
@@ -827,6 +830,16 @@
}
}
+ if (!disableNonCoreServices) {
+ try {
+ Slog.i(TAG, "MIDI Service");
+ ServiceManager.addService(Context.MIDI_SERVICE,
+ new MidiService(context));
+ } catch (Throwable e) {
+ reportWtf("starting MIDI Service", e);
+ }
+ }
+
mSystemServiceManager.startService(TwilightService.class);
mSystemServiceManager.startService(UiModeManagerService.class);
diff --git a/services/usb/java/com/android/server/usb/UsbAudioManager.java b/services/usb/java/com/android/server/usb/UsbAudioManager.java
index 95eb92b..4142032 100644
--- a/services/usb/java/com/android/server/usb/UsbAudioManager.java
+++ b/services/usb/java/com/android/server/usb/UsbAudioManager.java
@@ -24,7 +24,11 @@
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;
@@ -44,6 +48,7 @@
private static final String ALSA_DIRECTORY = "/dev/snd/";
private final Context mContext;
+ private IMidiManager mMidiManager;
private final class AudioDevice {
public int mCard;
@@ -132,6 +137,8 @@
}
public void systemReady() {
+ final IBinder b = ServiceManager.getService(Context.MIDI_SERVICE);
+ mMidiManager = IMidiManager.Stub.asInterface(b);
mAlsaObserver.startWatching();
// add existing alsa devices
@@ -241,6 +248,7 @@
// Is there an audio interface in there?
boolean isAudioDevice = false;
+ AlsaDevice midiDevice = null;
// FIXME - handle multiple configurations?
int interfaceCount = usbDevice.getInterfaceCount();
@@ -289,6 +297,11 @@
return;
}
+ // MIDI device file needed/present?
+ if (hasMidi) {
+ midiDevice = waitForAlsaDevice(card, device, AlsaDevice.TYPE_MIDI);
+ }
+
if (DEBUG) {
Slog.d(TAG,
"usb: hasPlayback:" + hasPlayback +
@@ -299,6 +312,14 @@
AudioDevice audioDevice = new AudioDevice(card, device, hasPlayback, hasCapture, hasMidi);
mAudioDevices.put(usbDevice, audioDevice);
sendDeviceNotification(audioDevice, true);
+
+ if (midiDevice != null && mMidiManager != null) {
+ try {
+ mMidiManager.alsaDeviceAdded(midiDevice.mCard, midiDevice.mDevice, usbDevice);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "MIDI Manager dead", e);
+ }
+ }
}
/* package */ void deviceRemoved(UsbDevice device) {
@@ -311,6 +332,13 @@
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);
+ }
+ }
}
}