From 67f8e8bd8927f24147d7a1edd6ec3db1a54233b2 Mon Sep 17 00:00:00 2001 From: Mike Lockwood Date: Mon, 1 Dec 2014 13:54:59 -0800 Subject: [PATCH] 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 --- Android.mk | 2 + core/java/android/app/ContextImpl.java | 8 + core/java/android/content/Context.java | 10 + core/java/android/midi/IMidiListener.aidl | 26 ++ core/java/android/midi/IMidiManager.aidl | 46 +++ core/java/android/midi/MidiDevice.aidl | 19 ++ core/java/android/midi/MidiDevice.java | 292 ++++++++++++++++++ core/java/android/midi/MidiDeviceInfo.aidl | 19 ++ core/java/android/midi/MidiDeviceInfo.java | 193 ++++++++++++ core/java/android/midi/MidiManager.java | 156 ++++++++++ core/java/android/midi/MidiReceiver.java | 30 ++ core/java/android/midi/MidiSender.java | 28 ++ core/java/android/midi/MidiUtils.java | 63 ++++ .../android/server/midi/MidiDeviceBase.java | 170 ++++++++++ .../com/android/server/midi/MidiService.java | 277 +++++++++++++++++ .../android/server/midi/UsbMidiDevice.java | 113 +++++++ .../server/midi/VirtualMidiDevice.java | 91 ++++++ .../java/com/android/server/SystemServer.java | 13 + .../android/server/usb/UsbAudioManager.java | 28 ++ 19 files changed, 1584 insertions(+) create mode 100644 core/java/android/midi/IMidiListener.aidl create mode 100644 core/java/android/midi/IMidiManager.aidl create mode 100644 core/java/android/midi/MidiDevice.aidl create mode 100644 core/java/android/midi/MidiDevice.java create mode 100644 core/java/android/midi/MidiDeviceInfo.aidl create mode 100644 core/java/android/midi/MidiDeviceInfo.java create mode 100644 core/java/android/midi/MidiManager.java create mode 100644 core/java/android/midi/MidiReceiver.java create mode 100644 core/java/android/midi/MidiSender.java create mode 100644 core/java/android/midi/MidiUtils.java create mode 100644 services/core/java/com/android/server/midi/MidiDeviceBase.java create mode 100644 services/core/java/com/android/server/midi/MidiService.java create mode 100644 services/core/java/com/android/server/midi/UsbMidiDevice.java create mode 100644 services/core/java/com/android/server/midi/VirtualMidiDevice.java diff --git a/Android.mk b/Android.mk index ec0c2357283b3..2dc210f6cdea7 100644 --- a/Android.mk +++ b/Android.mk @@ -172,6 +172,8 @@ 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/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 12fee2d60a8ae..eadf5e90a17c1 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -80,6 +80,8 @@ import android.media.projection.MediaProjectionManager; 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 @@ class ContextImpl extends Context { 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 e6bb09fcc3a25..b0bfdb6e4729d 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -2142,6 +2142,7 @@ public abstract class Context { MEDIA_SESSION_SERVICE, BATTERY_SERVICE, JOB_SCHEDULER_SERVICE, + MIDI_SERVICE, }) @Retention(RetentionPolicy.SOURCE) public @interface ServiceName {} @@ -2914,6 +2915,15 @@ public abstract class Context { */ 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 0000000000000..b6505939f2a51 --- /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 0000000000000..06b675b2f81ef --- /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 0000000000000..11bb497473b26 --- /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 0000000000000..8954d2be55b93 --- /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 mReceivers = new ArrayList(); + + /** + * 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 deadReceivers = new ArrayList(); + + 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 CREATOR = + new Parcelable.Creator() { + 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 0000000000000..59be0593e3ba7 --- /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 0000000000000..def6878cc1c53 --- /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 CREATOR = + new Parcelable.Creator() { + 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 0000000000000..ec869b7fd4baf --- /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. + * + *

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 mDeviceListeners = + new HashMap(); + + // 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 0000000000000..1101105ebabfc --- /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 0000000000000..cba707905517a --- /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 0000000000000..f80e83acb3d5b --- /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 0000000000000..428958464f330 --- /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 mOutputStreams = + new ArrayList(); + + @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 0000000000000..ff8dda001475c --- /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 mClients = new HashMap(); + + // list of all devices, keyed by ID + private final HashMap mDevices + = new HashMap(); + + // list of all USB devices, keyed by USB device. + private final HashMap mUsbDevices + = new HashMap(); + + // 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 mListeners = new ArrayList(); + private final ArrayList mVirtualDevices = new ArrayList(); + + 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 infos = new ArrayList(); + 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 0000000000000..1bc91f05b88a5 --- /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 0000000000000..5b3904583880d --- /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 0ccb25c679c80..db608660f1ec4 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.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.net.NetworkPolicyManagerService; import com.android.server.net.NetworkStatsService; import com.android.server.notification.NotificationManagerService; @@ -410,6 +411,7 @@ public final class SystemServer { 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 @@ public final class SystemServer { 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 @@ public final class SystemServer { } } + 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 95eb92b07ee86..41420323e33cf 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.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; @@ -44,6 +48,7 @@ public class UsbAudioManager { 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 class UsbAudioManager { } 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 @@ public class UsbAudioManager { // 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 @@ public class UsbAudioManager { 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 @@ public class UsbAudioManager { 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 @@ public class UsbAudioManager { 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); + } + } } }