summaryrefslogtreecommitdiff
path: root/android/media/midi
diff options
context:
space:
mode:
authorJustin Klaassen <justinklaassen@google.com>2017-09-15 17:58:39 -0400
committerJustin Klaassen <justinklaassen@google.com>2017-09-15 17:58:39 -0400
commit10d07c88d69cc64f73a069163e7ea5ba2519a099 (patch)
tree8dbd149eb350320a29c3d10e7ad3201de1c5cbee /android/media/midi
parent677516fb6b6f207d373984757d3d9450474b6b00 (diff)
downloadandroid-28-10d07c88d69cc64f73a069163e7ea5ba2519a099.tar.gz
Import Android SDK Platform PI [4335822]
/google/data/ro/projects/android/fetch_artifact \ --bid 4335822 \ --target sdk_phone_armv7-win_sdk \ sdk-repo-linux-sources-4335822.zip AndroidVersion.ApiLevel has been modified to appear as 28 Change-Id: Ic8f04be005a71c2b9abeaac754d8da8d6f9a2c32
Diffstat (limited to 'android/media/midi')
-rw-r--r--android/media/midi/MidiDevice.java308
-rw-r--r--android/media/midi/MidiDeviceInfo.java390
-rw-r--r--android/media/midi/MidiDeviceServer.java452
-rw-r--r--android/media/midi/MidiDeviceService.java145
-rw-r--r--android/media/midi/MidiDeviceStatus.java138
-rw-r--r--android/media/midi/MidiInputPort.java173
-rw-r--r--android/media/midi/MidiManager.java327
-rw-r--r--android/media/midi/MidiOutputPort.java159
-rw-r--r--android/media/midi/MidiPortImpl.java134
-rw-r--r--android/media/midi/MidiReceiver.java133
-rw-r--r--android/media/midi/MidiSender.java62
11 files changed, 2421 insertions, 0 deletions
diff --git a/android/media/midi/MidiDevice.java b/android/media/midi/MidiDevice.java
new file mode 100644
index 00000000..a9957369
--- /dev/null
+++ b/android/media/midi/MidiDevice.java
@@ -0,0 +1,308 @@
+/*
+ * 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.media.midi;
+
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.Process;
+import android.os.RemoteException;
+import android.util.Log;
+
+import dalvik.system.CloseGuard;
+
+import libcore.io.IoUtils;
+
+import java.io.Closeable;
+import java.io.FileDescriptor;
+import java.io.IOException;
+
+import java.util.HashSet;
+
+/**
+ * This class is used for sending and receiving data to and from a MIDI device
+ * Instances of this class are created by {@link MidiManager#openDevice}.
+ */
+public final class MidiDevice implements Closeable {
+ static {
+ System.loadLibrary("media_jni");
+ }
+
+ private static final String TAG = "MidiDevice";
+
+ private final MidiDeviceInfo mDeviceInfo;
+ private final IMidiDeviceServer mDeviceServer;
+ private final IMidiManager mMidiManager;
+ private final IBinder mClientToken;
+ private final IBinder mDeviceToken;
+ private boolean mIsDeviceClosed;
+
+ // Native API Helpers
+ /**
+ * Keep a static list of MidiDevice objects that are mirrorToNative()'d so they
+ * don't get inadvertantly garbage collected.
+ */
+ private static HashSet<MidiDevice> mMirroredDevices = new HashSet<MidiDevice>();
+
+ /**
+ * If this device is mirrorToNatived(), this is the native device handler.
+ */
+ private long mNativeHandle;
+
+ private final CloseGuard mGuard = CloseGuard.get();
+
+ /**
+ * This class represents a connection between the output port of one device
+ * and the input port of another. Created by {@link #connectPorts}.
+ * Close this object to terminate the connection.
+ */
+ public class MidiConnection implements Closeable {
+ private final IMidiDeviceServer mInputPortDeviceServer;
+ private final IBinder mInputPortToken;
+ private final IBinder mOutputPortToken;
+ private final CloseGuard mGuard = CloseGuard.get();
+ private boolean mIsClosed;
+
+ MidiConnection(IBinder outputPortToken, MidiInputPort inputPort) {
+ mInputPortDeviceServer = inputPort.getDeviceServer();
+ mInputPortToken = inputPort.getToken();
+ mOutputPortToken = outputPortToken;
+ mGuard.open("close");
+ }
+
+ @Override
+ public void close() throws IOException {
+ synchronized (mGuard) {
+ if (mIsClosed) return;
+ mGuard.close();
+ try {
+ // close input port
+ mInputPortDeviceServer.closePort(mInputPortToken);
+ // close output port
+ mDeviceServer.closePort(mOutputPortToken);
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException in MidiConnection.close");
+ }
+ mIsClosed = true;
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ if (mGuard != null) {
+ mGuard.warnIfOpen();
+ }
+
+ close();
+ } finally {
+ super.finalize();
+ }
+ }
+ }
+
+ /* package */ MidiDevice(MidiDeviceInfo deviceInfo, IMidiDeviceServer server,
+ IMidiManager midiManager, IBinder clientToken, IBinder deviceToken) {
+ mDeviceInfo = deviceInfo;
+ mDeviceServer = server;
+ mMidiManager = midiManager;
+ mClientToken = clientToken;
+ mDeviceToken = deviceToken;
+ mGuard.open("close");
+ }
+
+ /**
+ * Returns a {@link MidiDeviceInfo} object, which describes this device.
+ *
+ * @return the {@link MidiDeviceInfo} object
+ */
+ public MidiDeviceInfo getInfo() {
+ return mDeviceInfo;
+ }
+
+ /**
+ * Called to open a {@link MidiInputPort} for the specified port number.
+ *
+ * An input port can only be used by one sender at a time.
+ * Opening an input port will fail if another application has already opened it for use.
+ * A {@link MidiDeviceStatus} can be used to determine if an input port is already open.
+ *
+ * @param portNumber the number of the input port to open
+ * @return the {@link MidiInputPort} if the open is successful,
+ * or null in case of failure.
+ */
+ public MidiInputPort openInputPort(int portNumber) {
+ if (mIsDeviceClosed) {
+ return null;
+ }
+ try {
+ IBinder token = new Binder();
+ FileDescriptor fd = mDeviceServer.openInputPort(token, portNumber);
+ if (fd == null) {
+ return null;
+ }
+ return new MidiInputPort(mDeviceServer, token, fd, portNumber);
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException in openInputPort");
+ return null;
+ }
+ }
+
+ /**
+ * Called to open a {@link MidiOutputPort} for the specified port number.
+ *
+ * An output port may be opened by multiple applications.
+ *
+ * @param portNumber the number of the output port to open
+ * @return the {@link MidiOutputPort} if the open is successful,
+ * or null in case of failure.
+ */
+ public MidiOutputPort openOutputPort(int portNumber) {
+ if (mIsDeviceClosed) {
+ return null;
+ }
+ try {
+ IBinder token = new Binder();
+ FileDescriptor fd = mDeviceServer.openOutputPort(token, portNumber);
+ if (fd == null) {
+ return null;
+ }
+ return new MidiOutputPort(mDeviceServer, token, fd, portNumber);
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException in openOutputPort");
+ return null;
+ }
+ }
+
+ /**
+ * Connects the supplied {@link MidiInputPort} to the output port of this device
+ * with the specified port number. Once the connection is made, the MidiInput port instance
+ * can no longer receive data via its {@link MidiReceiver#onSend} method.
+ * This method returns a {@link MidiDevice.MidiConnection} object, which can be used
+ * to close the connection.
+ *
+ * @param inputPort the inputPort to connect
+ * @param outputPortNumber the port number of the output port to connect inputPort to.
+ * @return {@link MidiDevice.MidiConnection} object if the connection is successful,
+ * or null in case of failure.
+ */
+ public MidiConnection connectPorts(MidiInputPort inputPort, int outputPortNumber) {
+ if (outputPortNumber < 0 || outputPortNumber >= mDeviceInfo.getOutputPortCount()) {
+ throw new IllegalArgumentException("outputPortNumber out of range");
+ }
+ if (mIsDeviceClosed) {
+ return null;
+ }
+
+ FileDescriptor fd = inputPort.claimFileDescriptor();
+ if (fd == null) {
+ return null;
+ }
+ try {
+ IBinder token = new Binder();
+ int calleePid = mDeviceServer.connectPorts(token, fd, outputPortNumber);
+ // If the service is a different Process then it will duplicate the fd
+ // and we can safely close this one.
+ // But if the service is in the same Process then closing the fd will
+ // kill the connection. So don't do that.
+ if (calleePid != Process.myPid()) {
+ // close our copy of the file descriptor
+ IoUtils.closeQuietly(fd);
+ }
+
+ return new MidiConnection(token, inputPort);
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException in connectPorts");
+ return null;
+ }
+ }
+
+ /**
+ * Makes Midi Device available to the Native API
+ * @hide
+ */
+ public long mirrorToNative() throws IOException {
+ if (mIsDeviceClosed || mNativeHandle != 0) {
+ return 0;
+ }
+
+ mNativeHandle = native_mirrorToNative(mDeviceServer.asBinder(), mDeviceInfo.getId());
+ if (mNativeHandle == 0) {
+ throw new IOException("Failed mirroring to native");
+ }
+
+ synchronized (mMirroredDevices) {
+ mMirroredDevices.add(this);
+ }
+ return mNativeHandle;
+ }
+
+ /**
+ * Makes Midi Device no longer available to the Native API
+ * @hide
+ */
+ public void removeFromNative() {
+ if (mNativeHandle == 0) {
+ return;
+ }
+
+ synchronized (mGuard) {
+ native_removeFromNative(mNativeHandle);
+ mNativeHandle = 0;
+ }
+
+ synchronized (mMirroredDevices) {
+ mMirroredDevices.remove(this);
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ synchronized (mGuard) {
+ if (!mIsDeviceClosed) {
+ removeFromNative();
+ mGuard.close();
+ mIsDeviceClosed = true;
+ try {
+ mMidiManager.closeDevice(mClientToken, mDeviceToken);
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException in closeDevice");
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ if (mGuard != null) {
+ mGuard.warnIfOpen();
+ }
+
+ close();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return ("MidiDevice: " + mDeviceInfo.toString());
+ }
+
+ private native long native_mirrorToNative(IBinder deviceServerBinder, int id);
+ private native void native_removeFromNative(long deviceHandle);
+}
diff --git a/android/media/midi/MidiDeviceInfo.java b/android/media/midi/MidiDeviceInfo.java
new file mode 100644
index 00000000..5fd9006d
--- /dev/null
+++ b/android/media/midi/MidiDeviceInfo.java
@@ -0,0 +1,390 @@
+/*
+ * 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.media.midi;
+
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import android.util.Log;
+
+/**
+ * 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.
+ */
+public final class MidiDeviceInfo implements Parcelable {
+
+ private static final String TAG = "MidiDeviceInfo";
+
+ /*
+ * Please note that constants and (un)marshalling code need to be kept in sync
+ * with the native implementation (MidiDeviceInfo.h|cpp)
+ */
+
+ /**
+ * Constant representing USB MIDI devices for {@link #getType}
+ */
+ public static final int TYPE_USB = 1;
+
+ /**
+ * Constant representing virtual (software based) MIDI devices for {@link #getType}
+ */
+ public static final int TYPE_VIRTUAL = 2;
+
+ /**
+ * Constant representing Bluetooth MIDI devices for {@link #getType}
+ */
+ public static final int TYPE_BLUETOOTH = 3;
+
+ /**
+ * Bundle key for the device's user visible name property.
+ * The value for this property is of type {@link java.lang.String}.
+ * Used with the {@link android.os.Bundle} returned by {@link #getProperties}.
+ * For USB devices, this is a concatenation of the manufacturer and product names.
+ */
+ public static final String PROPERTY_NAME = "name";
+
+ /**
+ * Bundle key for the device's manufacturer name property.
+ * The value for this property is of type {@link java.lang.String}.
+ * 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 product name property.
+ * The value for this property is of type {@link java.lang.String}.
+ * 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_PRODUCT = "product";
+
+ /**
+ * Bundle key for the device's version property.
+ * The value for this property is of type {@link java.lang.String}.
+ * Used with the {@link android.os.Bundle} returned by {@link #getProperties}
+ * Matches the USB device version number for USB MIDI devices.
+ */
+ public static final String PROPERTY_VERSION = "version";
+
+ /**
+ * Bundle key for the device's serial number property.
+ * The value for this property is of type {@link java.lang.String}.
+ * 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 corresponding USB device.
+ * The value for this property is of type {@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";
+
+ /**
+ * Bundle key for the device's corresponding Bluetooth device.
+ * The value for this property is of type {@link android.bluetooth.BluetoothDevice}.
+ * Only set for Bluetooth MIDI devices.
+ * Used with the {@link android.os.Bundle} returned by {@link #getProperties}
+ */
+ public static final String PROPERTY_BLUETOOTH_DEVICE = "bluetooth_device";
+
+ /**
+ * Bundle key for the device's ALSA card number.
+ * The value for this property is an integer.
+ * Only set for USB MIDI devices.
+ * Used with the {@link android.os.Bundle} returned by {@link #getProperties}
+ *
+ * @hide
+ */
+ public static final String PROPERTY_ALSA_CARD = "alsa_card";
+
+ /**
+ * Bundle key for the device's ALSA device number.
+ * The value for this property is an integer.
+ * Only set for USB MIDI devices.
+ * Used with the {@link android.os.Bundle} returned by {@link #getProperties}
+ *
+ * @hide
+ */
+ public static final String PROPERTY_ALSA_DEVICE = "alsa_device";
+
+ /**
+ * ServiceInfo for the service hosting the device implementation.
+ * The value for this property is of type {@link android.content.pm.ServiceInfo}.
+ * Only set for Virtual MIDI devices.
+ * Used with the {@link android.os.Bundle} returned by {@link #getProperties}
+ *
+ * @hide
+ */
+ public static final String PROPERTY_SERVICE_INFO = "service_info";
+
+ /**
+ * Contains information about an input or output port.
+ */
+ public static final class PortInfo {
+ /**
+ * Port type for input ports
+ */
+ public static final int TYPE_INPUT = 1;
+
+ /**
+ * Port type for output ports
+ */
+ public static final int TYPE_OUTPUT = 2;
+
+ private final int mPortType;
+ private final int mPortNumber;
+ private final String mName;
+
+ PortInfo(int type, int portNumber, String name) {
+ mPortType = type;
+ mPortNumber = portNumber;
+ mName = (name == null ? "" : name);
+ }
+
+ /**
+ * Returns the port type of the port (either {@link #TYPE_INPUT} or {@link #TYPE_OUTPUT})
+ * @return the port type
+ */
+ public int getType() {
+ return mPortType;
+ }
+
+ /**
+ * Returns the port number of the port
+ * @return the port number
+ */
+ public int getPortNumber() {
+ return mPortNumber;
+ }
+
+ /**
+ * Returns the name of the port, or empty string if the port has no name
+ * @return the port name
+ */
+ public String getName() {
+ return mName;
+ }
+ }
+
+ private final int mType; // USB or virtual
+ private final int mId; // unique ID generated by MidiService
+ private final int mInputPortCount;
+ private final int mOutputPortCount;
+ private final String[] mInputPortNames;
+ private final String[] mOutputPortNames;
+ private final Bundle mProperties;
+ private final boolean mIsPrivate;
+
+ /**
+ * MidiDeviceInfo should only be instantiated by MidiService implementation
+ * @hide
+ */
+ public MidiDeviceInfo(int type, int id, int numInputPorts, int numOutputPorts,
+ String[] inputPortNames, String[] outputPortNames, Bundle properties,
+ boolean isPrivate) {
+ mType = type;
+ mId = id;
+ mInputPortCount = numInputPorts;
+ mOutputPortCount = numOutputPorts;
+ if (inputPortNames == null) {
+ mInputPortNames = new String[numInputPorts];
+ } else {
+ mInputPortNames = inputPortNames;
+ }
+ if (outputPortNames == null) {
+ mOutputPortNames = new String[numOutputPorts];
+ } else {
+ mOutputPortNames = outputPortNames;
+ }
+ mProperties = properties;
+ mIsPrivate = isPrivate;
+ }
+
+ /**
+ * 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 device's number of input ports.
+ *
+ * @return the number of input ports
+ */
+ public int getInputPortCount() {
+ return mInputPortCount;
+ }
+
+ /**
+ * Returns the device's number of output ports.
+ *
+ * @return the number of output ports
+ */
+ public int getOutputPortCount() {
+ return mOutputPortCount;
+ }
+
+ /**
+ * Returns information about the device's ports.
+ * The ports are in unspecified order.
+ *
+ * @return array of {@link PortInfo}
+ */
+ public PortInfo[] getPorts() {
+ PortInfo[] ports = new PortInfo[mInputPortCount + mOutputPortCount];
+
+ int index = 0;
+ for (int i = 0; i < mInputPortCount; i++) {
+ ports[index++] = new PortInfo(PortInfo.TYPE_INPUT, i, mInputPortNames[i]);
+ }
+ for (int i = 0; i < mOutputPortCount; i++) {
+ ports[index++] = new PortInfo(PortInfo.TYPE_OUTPUT, i, mOutputPortNames[i]);
+ }
+
+ return ports;
+ }
+
+ /**
+ * Returns the {@link android.os.Bundle} containing the device's properties.
+ *
+ * @return the device's properties
+ */
+ public Bundle getProperties() {
+ return mProperties;
+ }
+
+ /**
+ * Returns true if the device is private. Private devices are only visible and accessible
+ * to clients with the same UID as the application that is hosting the device.
+ *
+ * @return true if the device is private
+ */
+ public boolean isPrivate() {
+ return mIsPrivate;
+ }
+
+ @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() {
+ // This is a hack to force the mProperties Bundle to unparcel so we can
+ // print all the names and values.
+ mProperties.getString(PROPERTY_NAME);
+ return ("MidiDeviceInfo[mType=" + mType +
+ ",mInputPortCount=" + mInputPortCount +
+ ",mOutputPortCount=" + mOutputPortCount +
+ ",mProperties=" + mProperties +
+ ",mIsPrivate=" + mIsPrivate);
+ }
+
+ public static final Parcelable.Creator<MidiDeviceInfo> CREATOR =
+ new Parcelable.Creator<MidiDeviceInfo>() {
+ public MidiDeviceInfo createFromParcel(Parcel in) {
+ // Needs to be kept in sync with code in MidiDeviceInfo.cpp
+ int type = in.readInt();
+ int id = in.readInt();
+ int inputPortCount = in.readInt();
+ int outputPortCount = in.readInt();
+ String[] inputPortNames = in.createStringArray();
+ String[] outputPortNames = in.createStringArray();
+ boolean isPrivate = (in.readInt() == 1);
+ Bundle basicPropertiesIgnored = in.readBundle();
+ Bundle properties = in.readBundle();
+ return new MidiDeviceInfo(type, id, inputPortCount, outputPortCount,
+ inputPortNames, outputPortNames, properties, isPrivate);
+ }
+
+ public MidiDeviceInfo[] newArray(int size) {
+ return new MidiDeviceInfo[size];
+ }
+ };
+
+ public int describeContents() {
+ return 0;
+ }
+
+ private Bundle getBasicProperties(String[] keys) {
+ Bundle basicProperties = new Bundle();
+ for (String key : keys) {
+ Object val = mProperties.get(key);
+ if (val != null) {
+ if (val instanceof String) {
+ basicProperties.putString(key, (String) val);
+ } else if (val instanceof Integer) {
+ basicProperties.putInt(key, (Integer) val);
+ } else {
+ Log.w(TAG, "Unsupported property type: " + val.getClass().getName());
+ }
+ }
+ }
+ return basicProperties;
+ }
+
+ public void writeToParcel(Parcel parcel, int flags) {
+ // Needs to be kept in sync with code in MidiDeviceInfo.cpp
+ parcel.writeInt(mType);
+ parcel.writeInt(mId);
+ parcel.writeInt(mInputPortCount);
+ parcel.writeInt(mOutputPortCount);
+ parcel.writeStringArray(mInputPortNames);
+ parcel.writeStringArray(mOutputPortNames);
+ parcel.writeInt(mIsPrivate ? 1 : 0);
+ // "Basic" properties only contain properties of primitive types
+ // and thus can be read back by native code. "Extra" properties is
+ // a superset that contains all properties.
+ parcel.writeBundle(getBasicProperties(new String[] {
+ PROPERTY_NAME, PROPERTY_MANUFACTURER, PROPERTY_PRODUCT, PROPERTY_VERSION,
+ PROPERTY_SERIAL_NUMBER, PROPERTY_ALSA_CARD, PROPERTY_ALSA_DEVICE
+ }));
+ // Must be serialized last so native code can safely ignore it.
+ parcel.writeBundle(mProperties);
+ }
+}
diff --git a/android/media/midi/MidiDeviceServer.java b/android/media/midi/MidiDeviceServer.java
new file mode 100644
index 00000000..51d55206
--- /dev/null
+++ b/android/media/midi/MidiDeviceServer.java
@@ -0,0 +1,452 @@
+/*
+ * 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.media.midi;
+
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.Process;
+import android.os.RemoteException;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+import android.util.Log;
+
+import com.android.internal.midi.MidiDispatcher;
+
+import dalvik.system.CloseGuard;
+
+import libcore.io.IoUtils;
+
+import java.io.Closeable;
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * Internal class used for providing an implementation for a MIDI device.
+ *
+ * @hide
+ */
+public final class MidiDeviceServer implements Closeable {
+ private static final String TAG = "MidiDeviceServer";
+
+ private final IMidiManager mMidiManager;
+
+ // MidiDeviceInfo for the device implemented by this server
+ private MidiDeviceInfo mDeviceInfo;
+ private final int mInputPortCount;
+ private final int mOutputPortCount;
+
+ // MidiReceivers for receiving data on our input ports
+ private final MidiReceiver[] mInputPortReceivers;
+
+ // MidiDispatchers for sending data on our output ports
+ private MidiDispatcher[] mOutputPortDispatchers;
+
+ // MidiOutputPorts for clients connected to our input ports
+ private final MidiOutputPort[] mInputPortOutputPorts;
+
+ // List of all MidiInputPorts we created
+ private final CopyOnWriteArrayList<MidiInputPort> mInputPorts
+ = new CopyOnWriteArrayList<MidiInputPort>();
+
+
+ // for reporting device status
+ private final boolean[] mInputPortOpen;
+ private final int[] mOutputPortOpenCount;
+
+ private final CloseGuard mGuard = CloseGuard.get();
+ private boolean mIsClosed;
+
+ private final Callback mCallback;
+
+ private final HashMap<IBinder, PortClient> mPortClients = new HashMap<IBinder, PortClient>();
+ private final HashMap<MidiInputPort, PortClient> mInputPortClients =
+ new HashMap<MidiInputPort, PortClient>();
+
+ public interface Callback {
+ /**
+ * Called to notify when an our device status has changed
+ * @param server the {@link MidiDeviceServer} that changed
+ * @param status the {@link MidiDeviceStatus} for the device
+ */
+ public void onDeviceStatusChanged(MidiDeviceServer server, MidiDeviceStatus status);
+
+ /**
+ * Called to notify when the device is closed
+ */
+ public void onClose();
+ }
+
+ abstract private class PortClient implements IBinder.DeathRecipient {
+ final IBinder mToken;
+
+ PortClient(IBinder token) {
+ mToken = token;
+
+ try {
+ token.linkToDeath(this, 0);
+ } catch (RemoteException e) {
+ close();
+ }
+ }
+
+ abstract void close();
+
+ MidiInputPort getInputPort() {
+ return null;
+ }
+
+ @Override
+ public void binderDied() {
+ close();
+ }
+ }
+
+ private class InputPortClient extends PortClient {
+ private final MidiOutputPort mOutputPort;
+
+ InputPortClient(IBinder token, MidiOutputPort outputPort) {
+ super(token);
+ mOutputPort = outputPort;
+ }
+
+ @Override
+ void close() {
+ mToken.unlinkToDeath(this, 0);
+ synchronized (mInputPortOutputPorts) {
+ int portNumber = mOutputPort.getPortNumber();
+ mInputPortOutputPorts[portNumber] = null;
+ mInputPortOpen[portNumber] = false;
+ updateDeviceStatus();
+ }
+ IoUtils.closeQuietly(mOutputPort);
+ }
+ }
+
+ private class OutputPortClient extends PortClient {
+ private final MidiInputPort mInputPort;
+
+ OutputPortClient(IBinder token, MidiInputPort inputPort) {
+ super(token);
+ mInputPort = inputPort;
+ }
+
+ @Override
+ void close() {
+ mToken.unlinkToDeath(this, 0);
+ int portNumber = mInputPort.getPortNumber();
+ MidiDispatcher dispatcher = mOutputPortDispatchers[portNumber];
+ synchronized (dispatcher) {
+ dispatcher.getSender().disconnect(mInputPort);
+ int openCount = dispatcher.getReceiverCount();
+ mOutputPortOpenCount[portNumber] = openCount;
+ updateDeviceStatus();
+ }
+
+ mInputPorts.remove(mInputPort);
+ IoUtils.closeQuietly(mInputPort);
+ }
+
+ @Override
+ MidiInputPort getInputPort() {
+ return mInputPort;
+ }
+ }
+
+ private static FileDescriptor[] createSeqPacketSocketPair() throws IOException {
+ try {
+ final FileDescriptor fd0 = new FileDescriptor();
+ final FileDescriptor fd1 = new FileDescriptor();
+ Os.socketpair(OsConstants.AF_UNIX, OsConstants.SOCK_SEQPACKET, 0, fd0, fd1);
+ return new FileDescriptor[] { fd0, fd1 };
+ } catch (ErrnoException e) {
+ throw e.rethrowAsIOException();
+ }
+ }
+
+ // Binder interface stub for receiving connection requests from clients
+ private final IMidiDeviceServer mServer = new IMidiDeviceServer.Stub() {
+
+ @Override
+ public FileDescriptor openInputPort(IBinder token, int portNumber) {
+ if (mDeviceInfo.isPrivate()) {
+ if (Binder.getCallingUid() != Process.myUid()) {
+ throw new SecurityException("Can't access private device from different UID");
+ }
+ }
+
+ if (portNumber < 0 || portNumber >= mInputPortCount) {
+ Log.e(TAG, "portNumber out of range in openInputPort: " + portNumber);
+ return null;
+ }
+
+ synchronized (mInputPortOutputPorts) {
+ if (mInputPortOutputPorts[portNumber] != null) {
+ Log.d(TAG, "port " + portNumber + " already open");
+ return null;
+ }
+
+ try {
+ FileDescriptor[] pair = createSeqPacketSocketPair();
+ MidiOutputPort outputPort = new MidiOutputPort(pair[0], portNumber);
+ mInputPortOutputPorts[portNumber] = outputPort;
+ outputPort.connect(mInputPortReceivers[portNumber]);
+ InputPortClient client = new InputPortClient(token, outputPort);
+ synchronized (mPortClients) {
+ mPortClients.put(token, client);
+ }
+ mInputPortOpen[portNumber] = true;
+ updateDeviceStatus();
+ return pair[1];
+ } catch (IOException e) {
+ Log.e(TAG, "unable to create FileDescriptors in openInputPort");
+ return null;
+ }
+ }
+ }
+
+ @Override
+ public FileDescriptor openOutputPort(IBinder token, int portNumber) {
+ if (mDeviceInfo.isPrivate()) {
+ if (Binder.getCallingUid() != Process.myUid()) {
+ throw new SecurityException("Can't access private device from different UID");
+ }
+ }
+
+ if (portNumber < 0 || portNumber >= mOutputPortCount) {
+ Log.e(TAG, "portNumber out of range in openOutputPort: " + portNumber);
+ return null;
+ }
+
+ try {
+ FileDescriptor[] pair = createSeqPacketSocketPair();
+ MidiInputPort inputPort = new MidiInputPort(pair[0], portNumber);
+ // Undo the default blocking-mode of the server-side socket for
+ // physical devices to avoid stalling the Java device handler if
+ // client app code gets stuck inside 'onSend' handler.
+ if (mDeviceInfo.getType() != MidiDeviceInfo.TYPE_VIRTUAL) {
+ IoUtils.setBlocking(pair[0], false);
+ }
+ MidiDispatcher dispatcher = mOutputPortDispatchers[portNumber];
+ synchronized (dispatcher) {
+ dispatcher.getSender().connect(inputPort);
+ int openCount = dispatcher.getReceiverCount();
+ mOutputPortOpenCount[portNumber] = openCount;
+ updateDeviceStatus();
+ }
+
+ mInputPorts.add(inputPort);
+ OutputPortClient client = new OutputPortClient(token, inputPort);
+ synchronized (mPortClients) {
+ mPortClients.put(token, client);
+ }
+ synchronized (mInputPortClients) {
+ mInputPortClients.put(inputPort, client);
+ }
+ return pair[1];
+ } catch (IOException e) {
+ Log.e(TAG, "unable to create FileDescriptors in openOutputPort");
+ return null;
+ }
+ }
+
+ @Override
+ public void closePort(IBinder token) {
+ MidiInputPort inputPort = null;
+ synchronized (mPortClients) {
+ PortClient client = mPortClients.remove(token);
+ if (client != null) {
+ inputPort = client.getInputPort();
+ client.close();
+ }
+ }
+ if (inputPort != null) {
+ synchronized (mInputPortClients) {
+ mInputPortClients.remove(inputPort);
+ }
+ }
+ }
+
+ @Override
+ public void closeDevice() {
+ if (mCallback != null) {
+ mCallback.onClose();
+ }
+ IoUtils.closeQuietly(MidiDeviceServer.this);
+ }
+
+ @Override
+ public int connectPorts(IBinder token, FileDescriptor fd,
+ int outputPortNumber) {
+ MidiInputPort inputPort = new MidiInputPort(fd, outputPortNumber);
+ MidiDispatcher dispatcher = mOutputPortDispatchers[outputPortNumber];
+ synchronized (dispatcher) {
+ dispatcher.getSender().connect(inputPort);
+ int openCount = dispatcher.getReceiverCount();
+ mOutputPortOpenCount[outputPortNumber] = openCount;
+ updateDeviceStatus();
+ }
+
+ mInputPorts.add(inputPort);
+ OutputPortClient client = new OutputPortClient(token, inputPort);
+ synchronized (mPortClients) {
+ mPortClients.put(token, client);
+ }
+ synchronized (mInputPortClients) {
+ mInputPortClients.put(inputPort, client);
+ }
+ return Process.myPid(); // for caller to detect same process ID
+ }
+
+ @Override
+ public MidiDeviceInfo getDeviceInfo() {
+ return mDeviceInfo;
+ }
+
+ @Override
+ public void setDeviceInfo(MidiDeviceInfo deviceInfo) {
+ if (Binder.getCallingUid() != Process.SYSTEM_UID) {
+ throw new SecurityException("setDeviceInfo should only be called by MidiService");
+ }
+ if (mDeviceInfo != null) {
+ throw new IllegalStateException("setDeviceInfo should only be called once");
+ }
+ mDeviceInfo = deviceInfo;
+ }
+ };
+
+ // Constructor for MidiManager.createDeviceServer()
+ /* package */ MidiDeviceServer(IMidiManager midiManager, MidiReceiver[] inputPortReceivers,
+ int numOutputPorts, Callback callback) {
+ mMidiManager = midiManager;
+ mInputPortReceivers = inputPortReceivers;
+ mInputPortCount = inputPortReceivers.length;
+ mOutputPortCount = numOutputPorts;
+ mCallback = callback;
+
+ mInputPortOutputPorts = new MidiOutputPort[mInputPortCount];
+
+ mOutputPortDispatchers = new MidiDispatcher[numOutputPorts];
+ for (int i = 0; i < numOutputPorts; i++) {
+ mOutputPortDispatchers[i] = new MidiDispatcher(mInputPortFailureHandler);
+ }
+
+ mInputPortOpen = new boolean[mInputPortCount];
+ mOutputPortOpenCount = new int[numOutputPorts];
+
+ mGuard.open("close");
+ }
+
+ private final MidiDispatcher.MidiReceiverFailureHandler mInputPortFailureHandler =
+ new MidiDispatcher.MidiReceiverFailureHandler() {
+ public void onReceiverFailure(MidiReceiver receiver, IOException failure) {
+ Log.e(TAG, "MidiInputPort failed to send data", failure);
+ PortClient client = null;
+ synchronized (mInputPortClients) {
+ client = mInputPortClients.remove(receiver);
+ }
+ if (client != null) {
+ client.close();
+ }
+ }
+ };
+
+ // Constructor for MidiDeviceService.onCreate()
+ /* package */ MidiDeviceServer(IMidiManager midiManager, MidiReceiver[] inputPortReceivers,
+ MidiDeviceInfo deviceInfo, Callback callback) {
+ this(midiManager, inputPortReceivers, deviceInfo.getOutputPortCount(), callback);
+ mDeviceInfo = deviceInfo;
+ }
+
+ /* package */ IMidiDeviceServer getBinderInterface() {
+ return mServer;
+ }
+
+ public IBinder asBinder() {
+ return mServer.asBinder();
+ }
+
+ private void updateDeviceStatus() {
+ // clear calling identity, since we may be in a Binder call from one of our clients
+ long identityToken = Binder.clearCallingIdentity();
+
+ MidiDeviceStatus status = new MidiDeviceStatus(mDeviceInfo, mInputPortOpen,
+ mOutputPortOpenCount);
+ if (mCallback != null) {
+ mCallback.onDeviceStatusChanged(this, status);
+ }
+ try {
+ mMidiManager.setDeviceStatus(mServer, status);
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException in updateDeviceStatus");
+ } finally {
+ Binder.restoreCallingIdentity(identityToken);
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ synchronized (mGuard) {
+ if (mIsClosed) return;
+ mGuard.close();
+
+ for (int i = 0; i < mInputPortCount; i++) {
+ MidiOutputPort outputPort = mInputPortOutputPorts[i];
+ if (outputPort != null) {
+ IoUtils.closeQuietly(outputPort);
+ mInputPortOutputPorts[i] = null;
+ }
+ }
+ for (MidiInputPort inputPort : mInputPorts) {
+ IoUtils.closeQuietly(inputPort);
+ }
+ mInputPorts.clear();
+ try {
+ mMidiManager.unregisterDeviceServer(mServer);
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException in unregisterDeviceServer");
+ }
+ mIsClosed = true;
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ if (mGuard != null) {
+ mGuard.warnIfOpen();
+ }
+
+ close();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ /**
+ * Returns an array of {@link MidiReceiver} for the device's output ports.
+ * Clients can use these receivers to send data out the device's output ports.
+ * @return array of MidiReceivers
+ */
+ public MidiReceiver[] getOutputPortReceivers() {
+ MidiReceiver[] receivers = new MidiReceiver[mOutputPortCount];
+ System.arraycopy(mOutputPortDispatchers, 0, receivers, 0, mOutputPortCount);
+ return receivers;
+ }
+}
diff --git a/android/media/midi/MidiDeviceService.java b/android/media/midi/MidiDeviceService.java
new file mode 100644
index 00000000..388d95bb
--- /dev/null
+++ b/android/media/midi/MidiDeviceService.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2015 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.media.midi;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Log;
+
+/**
+ * A service that implements a virtual MIDI device.
+ * Subclasses must implement the {@link #onGetInputPortReceivers} method to provide a
+ * list of {@link MidiReceiver}s to receive data sent to the device's input ports.
+ * Similarly, subclasses can call {@link #getOutputPortReceivers} to fetch a list
+ * of {@link MidiReceiver}s for sending data out the output ports.
+ *
+ * <p>To extend this class, you must declare the service in your manifest file with
+ * an intent filter with the {@link #SERVICE_INTERFACE} action
+ * and meta-data to describe the virtual device.
+ For example:</p>
+ * <pre>
+ * &lt;service android:name=".VirtualDeviceService"
+ * android:label="&#64;string/service_name">
+ * &lt;intent-filter>
+ * &lt;action android:name="android.media.midi.MidiDeviceService" />
+ * &lt;/intent-filter>
+ * &lt;meta-data android:name="android.media.midi.MidiDeviceService"
+ android:resource="@xml/device_info" />
+ * &lt;/service></pre>
+ */
+abstract public class MidiDeviceService extends Service {
+ private static final String TAG = "MidiDeviceService";
+
+ public static final String SERVICE_INTERFACE = "android.media.midi.MidiDeviceService";
+
+ private IMidiManager mMidiManager;
+ private MidiDeviceServer mServer;
+ private MidiDeviceInfo mDeviceInfo;
+
+ private final MidiDeviceServer.Callback mCallback = new MidiDeviceServer.Callback() {
+ @Override
+ public void onDeviceStatusChanged(MidiDeviceServer server, MidiDeviceStatus status) {
+ MidiDeviceService.this.onDeviceStatusChanged(status);
+ }
+
+ @Override
+ public void onClose() {
+ MidiDeviceService.this.onClose();
+ }
+ };
+
+ @Override
+ public void onCreate() {
+ mMidiManager = IMidiManager.Stub.asInterface(
+ ServiceManager.getService(Context.MIDI_SERVICE));
+ MidiDeviceServer server;
+ try {
+ MidiDeviceInfo deviceInfo = mMidiManager.getServiceDeviceInfo(getPackageName(),
+ this.getClass().getName());
+ if (deviceInfo == null) {
+ Log.e(TAG, "Could not find MidiDeviceInfo for MidiDeviceService " + this);
+ return;
+ }
+ mDeviceInfo = deviceInfo;
+ MidiReceiver[] inputPortReceivers = onGetInputPortReceivers();
+ if (inputPortReceivers == null) {
+ inputPortReceivers = new MidiReceiver[0];
+ }
+ server = new MidiDeviceServer(mMidiManager, inputPortReceivers, deviceInfo, mCallback);
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException in IMidiManager.getServiceDeviceInfo");
+ server = null;
+ }
+ mServer = server;
+ }
+
+ /**
+ * Returns an array of {@link MidiReceiver} for the device's input ports.
+ * Subclasses must override this to provide the receivers which will receive
+ * data sent to the device's input ports. An empty array should be returned if
+ * the device has no input ports.
+ * @return array of MidiReceivers
+ */
+ abstract public MidiReceiver[] onGetInputPortReceivers();
+
+ /**
+ * Returns an array of {@link MidiReceiver} for the device's output ports.
+ * These can be used to send data out the device's output ports.
+ * @return array of MidiReceivers
+ */
+ public final MidiReceiver[] getOutputPortReceivers() {
+ if (mServer == null) {
+ return null;
+ } else {
+ return mServer.getOutputPortReceivers();
+ }
+ }
+
+ /**
+ * returns the {@link MidiDeviceInfo} instance for this service
+ * @return our MidiDeviceInfo
+ */
+ public final MidiDeviceInfo getDeviceInfo() {
+ return mDeviceInfo;
+ }
+
+ /**
+ * Called to notify when an our {@link MidiDeviceStatus} has changed
+ * @param status the number of the port that was opened
+ */
+ public void onDeviceStatusChanged(MidiDeviceStatus status) {
+ }
+
+ /**
+ * Called to notify when our device has been closed by all its clients
+ */
+ public void onClose() {
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ if (SERVICE_INTERFACE.equals(intent.getAction()) && mServer != null) {
+ return mServer.getBinderInterface().asBinder();
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/android/media/midi/MidiDeviceStatus.java b/android/media/midi/MidiDeviceStatus.java
new file mode 100644
index 00000000..acb54de0
--- /dev/null
+++ b/android/media/midi/MidiDeviceStatus.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2015 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.media.midi;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * This is an immutable class that describes the current status of a MIDI device's ports.
+ */
+public final class MidiDeviceStatus implements Parcelable {
+
+ private static final String TAG = "MidiDeviceStatus";
+
+ private final MidiDeviceInfo mDeviceInfo;
+ // true if input ports are open
+ private final boolean mInputPortOpen[];
+ // open counts for output ports
+ private final int mOutputPortOpenCount[];
+
+ /**
+ * @hide
+ */
+ public MidiDeviceStatus(MidiDeviceInfo deviceInfo, boolean inputPortOpen[],
+ int outputPortOpenCount[]) {
+ // MidiDeviceInfo is immutable so we can share references
+ mDeviceInfo = deviceInfo;
+
+ // make copies of the arrays
+ mInputPortOpen = new boolean[inputPortOpen.length];
+ System.arraycopy(inputPortOpen, 0, mInputPortOpen, 0, inputPortOpen.length);
+ mOutputPortOpenCount = new int[outputPortOpenCount.length];
+ System.arraycopy(outputPortOpenCount, 0, mOutputPortOpenCount, 0,
+ outputPortOpenCount.length);
+ }
+
+ /**
+ * Creates a MidiDeviceStatus with zero for all port open counts
+ * @hide
+ */
+ public MidiDeviceStatus(MidiDeviceInfo deviceInfo) {
+ mDeviceInfo = deviceInfo;
+ mInputPortOpen = new boolean[deviceInfo.getInputPortCount()];
+ mOutputPortOpenCount = new int[deviceInfo.getOutputPortCount()];
+ }
+
+ /**
+ * Returns the {@link MidiDeviceInfo} of the device.
+ *
+ * @return the device info
+ */
+ public MidiDeviceInfo getDeviceInfo() {
+ return mDeviceInfo;
+ }
+
+ /**
+ * Returns true if an input port is open.
+ * An input port can only be opened by one client at a time.
+ *
+ * @param portNumber the input port's port number
+ * @return input port open status
+ */
+ public boolean isInputPortOpen(int portNumber) {
+ return mInputPortOpen[portNumber];
+ }
+
+ /**
+ * Returns the number of clients currently connected to the specified output port.
+ * Unlike input ports, an output port can be opened by multiple clients at the same time.
+ *
+ * @param portNumber the output port's port number
+ * @return output port open count
+ */
+ public int getOutputPortOpenCount(int portNumber) {
+ return mOutputPortOpenCount[portNumber];
+ }
+
+ @Override
+ public String toString() {
+ int inputPortCount = mDeviceInfo.getInputPortCount();
+ int outputPortCount = mDeviceInfo.getOutputPortCount();
+ StringBuilder builder = new StringBuilder("mInputPortOpen=[");
+ for (int i = 0; i < inputPortCount; i++) {
+ builder.append(mInputPortOpen[i]);
+ if (i < inputPortCount -1) {
+ builder.append(",");
+ }
+ }
+ builder.append("] mOutputPortOpenCount=[");
+ for (int i = 0; i < outputPortCount; i++) {
+ builder.append(mOutputPortOpenCount[i]);
+ if (i < outputPortCount -1) {
+ builder.append(",");
+ }
+ }
+ builder.append("]");
+ return builder.toString();
+ }
+
+ public static final Parcelable.Creator<MidiDeviceStatus> CREATOR =
+ new Parcelable.Creator<MidiDeviceStatus>() {
+ public MidiDeviceStatus createFromParcel(Parcel in) {
+ ClassLoader classLoader = MidiDeviceInfo.class.getClassLoader();
+ MidiDeviceInfo deviceInfo = in.readParcelable(classLoader);
+ boolean[] inputPortOpen = in.createBooleanArray();
+ int[] outputPortOpenCount = in.createIntArray();
+ return new MidiDeviceStatus(deviceInfo, inputPortOpen, outputPortOpenCount);
+ }
+
+ public MidiDeviceStatus[] newArray(int size) {
+ return new MidiDeviceStatus[size];
+ }
+ };
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeParcelable(mDeviceInfo, flags);
+ parcel.writeBooleanArray(mInputPortOpen);
+ parcel.writeIntArray(mOutputPortOpenCount);
+ }
+}
diff --git a/android/media/midi/MidiInputPort.java b/android/media/midi/MidiInputPort.java
new file mode 100644
index 00000000..a300886e
--- /dev/null
+++ b/android/media/midi/MidiInputPort.java
@@ -0,0 +1,173 @@
+/*
+ * 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.media.midi;
+
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import dalvik.system.CloseGuard;
+
+import libcore.io.IoUtils;
+
+import java.io.Closeable;
+import java.io.FileDescriptor;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+/**
+ * This class is used for sending data to a port on a MIDI device
+ */
+public final class MidiInputPort extends MidiReceiver implements Closeable {
+ private static final String TAG = "MidiInputPort";
+
+ private IMidiDeviceServer mDeviceServer;
+ private final IBinder mToken;
+ private final int mPortNumber;
+ private FileDescriptor mFileDescriptor;
+ private FileOutputStream mOutputStream;
+
+ private final CloseGuard mGuard = CloseGuard.get();
+ private boolean mIsClosed;
+
+ // buffer to use for sending data out our output stream
+ private final byte[] mBuffer = new byte[MidiPortImpl.MAX_PACKET_SIZE];
+
+ /* package */ MidiInputPort(IMidiDeviceServer server, IBinder token,
+ FileDescriptor fd, int portNumber) {
+ super(MidiPortImpl.MAX_PACKET_DATA_SIZE);
+
+ mDeviceServer = server;
+ mToken = token;
+ mFileDescriptor = fd;
+ mPortNumber = portNumber;
+ mOutputStream = new FileOutputStream(fd);
+ mGuard.open("close");
+ }
+
+ /* package */ MidiInputPort(FileDescriptor fd, int portNumber) {
+ this(null, null, fd, portNumber);
+ }
+
+ /**
+ * Returns the port number of this port
+ *
+ * @return the port's port number
+ */
+ public final int getPortNumber() {
+ return mPortNumber;
+ }
+
+ @Override
+ public void onSend(byte[] msg, int offset, int count, long timestamp) throws IOException {
+ if (offset < 0 || count < 0 || offset + count > msg.length) {
+ throw new IllegalArgumentException("offset or count out of range");
+ }
+ if (count > MidiPortImpl.MAX_PACKET_DATA_SIZE) {
+ throw new IllegalArgumentException("count exceeds max message size");
+ }
+
+ synchronized (mBuffer) {
+ if (mOutputStream == null) {
+ throw new IOException("MidiInputPort is closed");
+ }
+ int length = MidiPortImpl.packData(msg, offset, count, timestamp, mBuffer);
+ mOutputStream.write(mBuffer, 0, length);
+ }
+ }
+
+ @Override
+ public void onFlush() throws IOException {
+ synchronized (mBuffer) {
+ if (mOutputStream == null) {
+ throw new IOException("MidiInputPort is closed");
+ }
+ int length = MidiPortImpl.packFlush(mBuffer);
+ mOutputStream.write(mBuffer, 0, length);
+ }
+ }
+
+ // used by MidiDevice.connectInputPort() to connect our socket directly to another device
+ /* package */ FileDescriptor claimFileDescriptor() {
+ synchronized (mGuard) {
+ FileDescriptor fd;
+ synchronized (mBuffer) {
+ fd = mFileDescriptor;
+ if (fd == null) return null;
+ IoUtils.closeQuietly(mOutputStream);
+ mFileDescriptor = null;
+ mOutputStream = null;
+ }
+
+ // Set mIsClosed = true so we will not call mDeviceServer.closePort() in close().
+ // MidiDevice.MidiConnection.close() will do the cleanup instead.
+ mIsClosed = true;
+ return fd;
+ }
+ }
+
+ // used by MidiDevice.MidiConnection to close this port after the connection is closed
+ /* package */ IBinder getToken() {
+ return mToken;
+ }
+
+ // used by MidiDevice.MidiConnection to close this port after the connection is closed
+ /* package */ IMidiDeviceServer getDeviceServer() {
+ return mDeviceServer;
+ }
+
+ @Override
+ public void close() throws IOException {
+ synchronized (mGuard) {
+ if (mIsClosed) return;
+ mGuard.close();
+ synchronized (mBuffer) {
+ if (mFileDescriptor != null) {
+ IoUtils.closeQuietly(mFileDescriptor);
+ mFileDescriptor = null;
+ }
+ if (mOutputStream != null) {
+ mOutputStream.close();
+ mOutputStream = null;
+ }
+ }
+ if (mDeviceServer != null) {
+ try {
+ mDeviceServer.closePort(mToken);
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException in MidiInputPort.close()");
+ }
+ }
+ mIsClosed = true;
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ if (mGuard != null) {
+ mGuard.warnIfOpen();
+ }
+
+ // not safe to make binder calls from finalize()
+ mDeviceServer = null;
+ close();
+ } finally {
+ super.finalize();
+ }
+ }
+}
diff --git a/android/media/midi/MidiManager.java b/android/media/midi/MidiManager.java
new file mode 100644
index 00000000..a015732d
--- /dev/null
+++ b/android/media/midi/MidiManager.java
@@ -0,0 +1,327 @@
+/*
+ * 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.media.midi;
+
+import android.annotation.SystemService;
+import android.bluetooth.BluetoothDevice;
+import android.content.Context;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * This class is the public application interface to the MIDI service.
+ */
+@SystemService(Context.MIDI_SERVICE)
+public final class MidiManager {
+ private static final String TAG = "MidiManager";
+
+ /**
+ * Intent for starting BluetoothMidiService
+ * @hide
+ */
+ public static final String BLUETOOTH_MIDI_SERVICE_INTENT =
+ "android.media.midi.BluetoothMidiService";
+
+ /**
+ * BluetoothMidiService package name
+ * @hide
+ */
+ public static final String BLUETOOTH_MIDI_SERVICE_PACKAGE = "com.android.bluetoothmidiservice";
+
+ /**
+ * BluetoothMidiService class name
+ * @hide
+ */
+ public static final String BLUETOOTH_MIDI_SERVICE_CLASS =
+ "com.android.bluetoothmidiservice.BluetoothMidiService";
+
+ private final IMidiManager mService;
+ private final IBinder mToken = new Binder();
+
+ private ConcurrentHashMap<DeviceCallback,DeviceListener> mDeviceListeners =
+ new ConcurrentHashMap<DeviceCallback,DeviceListener>();
+
+ // Binder stub for receiving device notifications from MidiService
+ private class DeviceListener extends IMidiDeviceListener.Stub {
+ private final DeviceCallback mCallback;
+ private final Handler mHandler;
+
+ public DeviceListener(DeviceCallback callback, Handler handler) {
+ mCallback = callback;
+ mHandler = handler;
+ }
+
+ @Override
+ public void onDeviceAdded(MidiDeviceInfo device) {
+ if (mHandler != null) {
+ final MidiDeviceInfo deviceF = device;
+ mHandler.post(new Runnable() {
+ @Override public void run() {
+ mCallback.onDeviceAdded(deviceF);
+ }
+ });
+ } else {
+ mCallback.onDeviceAdded(device);
+ }
+ }
+
+ @Override
+ public void onDeviceRemoved(MidiDeviceInfo device) {
+ if (mHandler != null) {
+ final MidiDeviceInfo deviceF = device;
+ mHandler.post(new Runnable() {
+ @Override public void run() {
+ mCallback.onDeviceRemoved(deviceF);
+ }
+ });
+ } else {
+ mCallback.onDeviceRemoved(device);
+ }
+ }
+
+ @Override
+ public void onDeviceStatusChanged(MidiDeviceStatus status) {
+ if (mHandler != null) {
+ final MidiDeviceStatus statusF = status;
+ mHandler.post(new Runnable() {
+ @Override public void run() {
+ mCallback.onDeviceStatusChanged(statusF);
+ }
+ });
+ } else {
+ mCallback.onDeviceStatusChanged(status);
+ }
+ }
+ }
+
+ /**
+ * Callback class used for clients to receive MIDI device added and removed notifications
+ */
+ public static class DeviceCallback {
+ /**
+ * Called to notify when a new MIDI device has been added
+ *
+ * @param device a {@link MidiDeviceInfo} for the newly added device
+ */
+ public void onDeviceAdded(MidiDeviceInfo device) {
+ }
+
+ /**
+ * Called to notify when a MIDI device has been removed
+ *
+ * @param device a {@link MidiDeviceInfo} for the removed device
+ */
+ public void onDeviceRemoved(MidiDeviceInfo device) {
+ }
+
+ /**
+ * Called to notify when the status of a MIDI device has changed
+ *
+ * @param status a {@link MidiDeviceStatus} for the changed device
+ */
+ public void onDeviceStatusChanged(MidiDeviceStatus status) {
+ }
+ }
+
+ /**
+ * Listener class used for receiving the results of {@link #openDevice} and
+ * {@link #openBluetoothDevice}
+ */
+ public interface OnDeviceOpenedListener {
+ /**
+ * Called to respond to a {@link #openDevice} request
+ *
+ * @param device a {@link MidiDevice} for opened device, or null if opening failed
+ */
+ abstract public void onDeviceOpened(MidiDevice device);
+ }
+
+ /**
+ * @hide
+ */
+ public MidiManager(IMidiManager service) {
+ mService = service;
+ }
+
+ /**
+ * Registers a callback to receive notifications when MIDI devices are added and removed.
+ *
+ * The {@link DeviceCallback#onDeviceStatusChanged} method will be called immediately
+ * for any devices that have open ports. This allows applications to know which input
+ * ports are already in use and, therefore, unavailable.
+ *
+ * Applications should call {@link #getDevices} before registering the callback
+ * to get a list of devices already added.
+ *
+ * @param callback a {@link DeviceCallback} for MIDI device notifications
+ * @param handler The {@link android.os.Handler Handler} that will be used for delivering the
+ * device notifications. If handler is null, then the thread used for the
+ * callback is unspecified.
+ */
+ public void registerDeviceCallback(DeviceCallback callback, Handler handler) {
+ DeviceListener deviceListener = new DeviceListener(callback, handler);
+ try {
+ mService.registerListener(mToken, deviceListener);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ mDeviceListeners.put(callback, deviceListener);
+ }
+
+ /**
+ * Unregisters a {@link DeviceCallback}.
+ *
+ * @param callback a {@link DeviceCallback} to unregister
+ */
+ public void unregisterDeviceCallback(DeviceCallback callback) {
+ DeviceListener deviceListener = mDeviceListeners.remove(callback);
+ if (deviceListener != null) {
+ try {
+ mService.unregisterListener(mToken, deviceListener);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Gets the list of all connected MIDI devices.
+ *
+ * @return an array of all MIDI devices
+ */
+ public MidiDeviceInfo[] getDevices() {
+ try {
+ return mService.getDevices();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ private void sendOpenDeviceResponse(final MidiDevice device,
+ final OnDeviceOpenedListener listener, Handler handler) {
+ if (handler != null) {
+ handler.post(new Runnable() {
+ @Override public void run() {
+ listener.onDeviceOpened(device);
+ }
+ });
+ } else {
+ listener.onDeviceOpened(device);
+ }
+ }
+
+ /**
+ * Opens a MIDI device for reading and writing.
+ *
+ * @param deviceInfo a {@link android.media.midi.MidiDeviceInfo} to open
+ * @param listener a {@link MidiManager.OnDeviceOpenedListener} to be called
+ * to receive the result
+ * @param handler the {@link android.os.Handler Handler} that will be used for delivering
+ * the result. If handler is null, then the thread used for the
+ * listener is unspecified.
+ */
+ public void openDevice(MidiDeviceInfo deviceInfo, OnDeviceOpenedListener listener,
+ Handler handler) {
+ final MidiDeviceInfo deviceInfoF = deviceInfo;
+ final OnDeviceOpenedListener listenerF = listener;
+ final Handler handlerF = handler;
+
+ IMidiDeviceOpenCallback callback = new IMidiDeviceOpenCallback.Stub() {
+ @Override
+ public void onDeviceOpened(IMidiDeviceServer server, IBinder deviceToken) {
+ MidiDevice device;
+ if (server != null) {
+ device = new MidiDevice(deviceInfoF, server, mService, mToken, deviceToken);
+ } else {
+ device = null;
+ }
+ sendOpenDeviceResponse(device, listenerF, handlerF);
+ }
+ };
+
+ try {
+ mService.openDevice(mToken, deviceInfo, callback);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Opens a Bluetooth MIDI device for reading and writing.
+ *
+ * @param bluetoothDevice a {@link android.bluetooth.BluetoothDevice} to open as a MIDI device
+ * @param listener a {@link MidiManager.OnDeviceOpenedListener} to be called to receive the
+ * result
+ * @param handler the {@link android.os.Handler Handler} that will be used for delivering
+ * the result. If handler is null, then the thread used for the
+ * listener is unspecified.
+ */
+ public void openBluetoothDevice(BluetoothDevice bluetoothDevice,
+ OnDeviceOpenedListener listener, Handler handler) {
+ final OnDeviceOpenedListener listenerF = listener;
+ final Handler handlerF = handler;
+
+ IMidiDeviceOpenCallback callback = new IMidiDeviceOpenCallback.Stub() {
+ @Override
+ public void onDeviceOpened(IMidiDeviceServer server, IBinder deviceToken) {
+ MidiDevice device = null;
+ if (server != null) {
+ try {
+ // fetch MidiDeviceInfo from the server
+ MidiDeviceInfo deviceInfo = server.getDeviceInfo();
+ device = new MidiDevice(deviceInfo, server, mService, mToken, deviceToken);
+ } catch (RemoteException e) {
+ Log.e(TAG, "remote exception in getDeviceInfo()");
+ }
+ }
+ sendOpenDeviceResponse(device, listenerF, handlerF);
+ }
+ };
+
+ try {
+ mService.openBluetoothDevice(mToken, bluetoothDevice, callback);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ public MidiDeviceServer createDeviceServer(MidiReceiver[] inputPortReceivers,
+ int numOutputPorts, String[] inputPortNames, String[] outputPortNames,
+ Bundle properties, int type, MidiDeviceServer.Callback callback) {
+ try {
+ MidiDeviceServer server = new MidiDeviceServer(mService, inputPortReceivers,
+ numOutputPorts, callback);
+ MidiDeviceInfo deviceInfo = mService.registerDeviceServer(server.getBinderInterface(),
+ inputPortReceivers.length, numOutputPorts, inputPortNames, outputPortNames,
+ properties, type);
+ if (deviceInfo == null) {
+ Log.e(TAG, "registerVirtualDevice failed");
+ return null;
+ }
+ return server;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+}
diff --git a/android/media/midi/MidiOutputPort.java b/android/media/midi/MidiOutputPort.java
new file mode 100644
index 00000000..511f6cd5
--- /dev/null
+++ b/android/media/midi/MidiOutputPort.java
@@ -0,0 +1,159 @@
+/*
+ * 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.media.midi;
+
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.midi.MidiDispatcher;
+
+import dalvik.system.CloseGuard;
+
+import libcore.io.IoUtils;
+
+import java.io.Closeable;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.IOException;
+
+/**
+ * This class is used for receiving data from a port on a MIDI device
+ */
+public final class MidiOutputPort extends MidiSender implements Closeable {
+ private static final String TAG = "MidiOutputPort";
+
+ private IMidiDeviceServer mDeviceServer;
+ private final IBinder mToken;
+ private final int mPortNumber;
+ private final FileInputStream mInputStream;
+ private final MidiDispatcher mDispatcher = new MidiDispatcher();
+
+ private final CloseGuard mGuard = CloseGuard.get();
+ private boolean mIsClosed;
+
+ // 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[MidiPortImpl.MAX_PACKET_SIZE];
+
+ try {
+ while (true) {
+ // read next event
+ int count = mInputStream.read(buffer);
+ if (count < 0) {
+ break;
+ // FIXME - inform receivers here?
+ }
+
+ int packetType = MidiPortImpl.getPacketType(buffer, count);
+ switch (packetType) {
+ case MidiPortImpl.PACKET_TYPE_DATA: {
+ int offset = MidiPortImpl.getDataOffset(buffer, count);
+ int size = MidiPortImpl.getDataSize(buffer, count);
+ long timestamp = MidiPortImpl.getPacketTimestamp(buffer, count);
+
+ // dispatch to all our receivers
+ mDispatcher.send(buffer, offset, size, timestamp);
+ break;
+ }
+ case MidiPortImpl.PACKET_TYPE_FLUSH:
+ mDispatcher.flush();
+ break;
+ default:
+ Log.e(TAG, "Unknown packet type " + packetType);
+ break;
+ }
+ }
+ } catch (IOException e) {
+ // FIXME report I/O failure?
+ Log.e(TAG, "read failed", e);
+ } finally {
+ IoUtils.closeQuietly(mInputStream);
+ }
+ }
+ };
+
+ /* package */ MidiOutputPort(IMidiDeviceServer server, IBinder token,
+ FileDescriptor fd, int portNumber) {
+ mDeviceServer = server;
+ mToken = token;
+ mPortNumber = portNumber;
+ mInputStream = new ParcelFileDescriptor.AutoCloseInputStream(new ParcelFileDescriptor(fd));
+ mThread.start();
+ mGuard.open("close");
+ }
+
+ /* package */ MidiOutputPort(FileDescriptor fd, int portNumber) {
+ this(null, null, fd, portNumber);
+ }
+
+ /**
+ * Returns the port number of this port
+ *
+ * @return the port's port number
+ */
+ public final int getPortNumber() {
+ return mPortNumber;
+ }
+
+ @Override
+ public void onConnect(MidiReceiver receiver) {
+ mDispatcher.getSender().connect(receiver);
+ }
+
+ @Override
+ public void onDisconnect(MidiReceiver receiver) {
+ mDispatcher.getSender().disconnect(receiver);
+ }
+
+ @Override
+ public void close() throws IOException {
+ synchronized (mGuard) {
+ if (mIsClosed) return;
+
+ mGuard.close();
+ mInputStream.close();
+ if (mDeviceServer != null) {
+ try {
+ mDeviceServer.closePort(mToken);
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException in MidiOutputPort.close()");
+ }
+ }
+ mIsClosed = true;
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ if (mGuard != null) {
+ mGuard.warnIfOpen();
+ }
+
+ // not safe to make binder calls from finalize()
+ mDeviceServer = null;
+ close();
+ } finally {
+ super.finalize();
+ }
+ }
+}
diff --git a/android/media/midi/MidiPortImpl.java b/android/media/midi/MidiPortImpl.java
new file mode 100644
index 00000000..1cd9ed22
--- /dev/null
+++ b/android/media/midi/MidiPortImpl.java
@@ -0,0 +1,134 @@
+/*
+ * 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.media.midi;
+
+/**
+ * This class contains utilities for socket communication between a
+ * MidiInputPort and MidiOutputPort
+ */
+/* package */ class MidiPortImpl {
+ private static final String TAG = "MidiPort";
+
+ /**
+ * Packet type for data packet
+ */
+ public static final int PACKET_TYPE_DATA = 1;
+
+ /**
+ * Packet type for flush packet
+ */
+ public static final int PACKET_TYPE_FLUSH = 2;
+
+ /**
+ * Maximum size of a packet that can be passed between processes.
+ */
+ public static final int MAX_PACKET_SIZE = 1024;
+
+ /**
+ * size of message timestamp in bytes
+ */
+ private static final int TIMESTAMP_SIZE = 8;
+
+ /**
+ * Data packet overhead is timestamp size plus packet type byte
+ */
+ private static final int DATA_PACKET_OVERHEAD = TIMESTAMP_SIZE + 1;
+
+ /**
+ * Maximum amount of MIDI data that can be included in a packet
+ */
+ public static final int MAX_PACKET_DATA_SIZE = MAX_PACKET_SIZE - DATA_PACKET_OVERHEAD;
+
+ /**
+ * Utility function for packing MIDI data to be passed between processes
+ *
+ * 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
+ */
+ public static int packData(byte[] message, int offset, int size, long timestamp,
+ byte[] dest) {
+ if (size > MAX_PACKET_DATA_SIZE) {
+ size = MAX_PACKET_DATA_SIZE;
+ }
+ int length = 0;
+ // packet type goes first
+ dest[length++] = PACKET_TYPE_DATA;
+ // data goes next
+ System.arraycopy(message, offset, dest, length, size);
+ length += size;
+
+ // followed by timestamp
+ for (int i = 0; i < TIMESTAMP_SIZE; i++) {
+ dest[length++] = (byte)timestamp;
+ timestamp >>= 8;
+ }
+
+ return length;
+ }
+
+ /**
+ * Utility function for packing a flush command to be passed between processes
+ */
+ public static int packFlush(byte[] dest) {
+ dest[0] = PACKET_TYPE_FLUSH;
+ return 1;
+ }
+
+ /**
+ * Returns the packet type (PACKET_TYPE_DATA or PACKET_TYPE_FLUSH)
+ */
+ public static int getPacketType(byte[] buffer, int bufferLength) {
+ return buffer[0];
+ }
+
+ /**
+ * Utility function for unpacking MIDI data received from other process
+ * returns the offset of the MIDI message in packed buffer
+ */
+ public static int getDataOffset(byte[] buffer, int bufferLength) {
+ // data follows packet type byte
+ return 1;
+ }
+
+ /**
+ * Utility function for unpacking MIDI data received from other process
+ * returns size of MIDI data in packed buffer
+ */
+ public static int getDataSize(byte[] buffer, int bufferLength) {
+ // message length is total buffer length minus size of the timestamp
+ return bufferLength - DATA_PACKET_OVERHEAD;
+ }
+
+ /**
+ * Utility function for unpacking MIDI data received from other process
+ * unpacks timestamp from packed buffer
+ */
+ public static long getPacketTimestamp(byte[] buffer, int bufferLength) {
+ // timestamp is at end of the packet
+ int offset = bufferLength;
+ long timestamp = 0;
+
+ for (int i = 0; i < TIMESTAMP_SIZE; i++) {
+ int b = (int)buffer[--offset] & 0xFF;
+ timestamp = (timestamp << 8) | b;
+ }
+ return timestamp;
+ }
+}
diff --git a/android/media/midi/MidiReceiver.java b/android/media/midi/MidiReceiver.java
new file mode 100644
index 00000000..12a5f044
--- /dev/null
+++ b/android/media/midi/MidiReceiver.java
@@ -0,0 +1,133 @@
+/*
+ * 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.media.midi;
+
+import java.io.IOException;
+
+/**
+ * Interface for sending and receiving data to and from a MIDI device.
+ */
+abstract public class MidiReceiver {
+
+ private final int mMaxMessageSize;
+
+ /**
+ * Default MidiReceiver constructor. Maximum message size is set to
+ * {@link java.lang.Integer#MAX_VALUE}
+ */
+ public MidiReceiver() {
+ mMaxMessageSize = Integer.MAX_VALUE;
+ }
+
+ /**
+ * MidiReceiver constructor.
+ * @param maxMessageSize the maximum size of a message this receiver can receive
+ */
+ public MidiReceiver(int maxMessageSize) {
+ mMaxMessageSize = maxMessageSize;
+ }
+
+ /**
+ * Called whenever the receiver is passed new MIDI data.
+ * Subclasses override this method to receive MIDI data.
+ * May fail if count exceeds {@link #getMaxMessageSize}.
+ *
+ * NOTE: the msg array parameter is only valid within the context of this call.
+ * The msg bytes should be copied by the receiver rather than retaining a reference
+ * to this parameter.
+ * Also, modifying the contents of the msg array parameter may result in other receivers
+ * in the same application receiving incorrect values in their {link #onSend} method.
+ *
+ * @param msg a byte array containing the MIDI data
+ * @param offset the offset of the first byte of the data in the array to be processed
+ * @param count the number of bytes of MIDI data in the array to be processed
+ * @param timestamp the timestamp of the message (based on {@link java.lang.System#nanoTime}
+ * @throws IOException
+ */
+ abstract public void onSend(byte[] msg, int offset, int count, long timestamp)
+ throws IOException;
+
+ /**
+ * Instructs the receiver to discard all pending MIDI data.
+ * @throws IOException
+ */
+ public void flush() throws IOException {
+ onFlush();
+ }
+
+ /**
+ * Called when the receiver is instructed to discard all pending MIDI data.
+ * Subclasses should override this method if they maintain a list or queue of MIDI data
+ * to be processed in the future.
+ * @throws IOException
+ */
+ public void onFlush() throws IOException {
+ }
+
+ /**
+ * Returns the maximum size of a message this receiver can receive.
+ * @return maximum message size
+ */
+ public final int getMaxMessageSize() {
+ return mMaxMessageSize;
+ }
+
+ /**
+ * Called to send MIDI data to the receiver without a timestamp.
+ * Data will be processed by receiver in the order sent.
+ * Data will get split into multiple calls to {@link #onSend} if count exceeds
+ * {@link #getMaxMessageSize}. Blocks until all the data is sent or an exception occurs.
+ * In the latter case, the amount of data sent prior to the exception is not provided to caller.
+ * The communication should be considered corrupt. The sender should reestablish
+ * communication, reset all controllers and send all notes off.
+ *
+ * @param msg a byte array containing the MIDI data
+ * @param offset the offset of the first byte of the data in the array to be sent
+ * @param count the number of bytes of MIDI data in the array to be sent
+ * @throws IOException if the data could not be sent in entirety
+ */
+ public void send(byte[] msg, int offset, int count) throws IOException {
+ // TODO add public static final TIMESTAMP_NONE = 0L
+ send(msg, offset, count, 0L);
+ }
+
+ /**
+ * Called to send MIDI data to the receiver with a specified timestamp.
+ * Data will be processed by receiver in order first by timestamp, then in the order sent.
+ * Data will get split into multiple calls to {@link #onSend} if count exceeds
+ * {@link #getMaxMessageSize}. Blocks until all the data is sent or an exception occurs.
+ * In the latter case, the amount of data sent prior to the exception is not provided to caller.
+ * The communication should be considered corrupt. The sender should reestablish
+ * communication, reset all controllers and send all notes off.
+ *
+ * @param msg a byte array containing the MIDI data
+ * @param offset the offset of the first byte of the data in the array to be sent
+ * @param count the number of bytes of MIDI data in the array to be sent
+ * @param timestamp the timestamp of the message, based on {@link java.lang.System#nanoTime}
+ * @throws IOException if the data could not be sent in entirety
+ */
+ public void send(byte[] msg, int offset, int count, long timestamp)
+ throws IOException {
+ int messageSize = getMaxMessageSize();
+ while (count > 0) {
+ int length = (count > messageSize ? messageSize : count);
+ onSend(msg, offset, length, timestamp);
+ offset += length;
+ count -= length;
+ }
+ }
+}
diff --git a/android/media/midi/MidiSender.java b/android/media/midi/MidiSender.java
new file mode 100644
index 00000000..c5f1edc4
--- /dev/null
+++ b/android/media/midi/MidiSender.java
@@ -0,0 +1,62 @@
+/*
+ * 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.media.midi;
+
+/**
+ * Interface provided by a device to allow attaching
+ * MidiReceivers to a MIDI device.
+ */
+abstract public class MidiSender {
+
+ /**
+ * Connects a {@link MidiReceiver} to the sender
+ *
+ * @param receiver the receiver to connect
+ */
+ public void connect(MidiReceiver receiver) {
+ if (receiver == null) {
+ throw new NullPointerException("receiver null in MidiSender.connect");
+ }
+ onConnect(receiver);
+ }
+
+ /**
+ * Disconnects a {@link MidiReceiver} from the sender
+ *
+ * @param receiver the receiver to disconnect
+ */
+ public void disconnect(MidiReceiver receiver) {
+ if (receiver == null) {
+ throw new NullPointerException("receiver null in MidiSender.disconnect");
+ }
+ onDisconnect(receiver);
+ }
+
+ /**
+ * Called to connect a {@link MidiReceiver} to the sender
+ *
+ * @param receiver the receiver to connect
+ */
+ abstract public void onConnect(MidiReceiver receiver);
+
+ /**
+ * Called to disconnect a {@link MidiReceiver} from the sender
+ *
+ * @param receiver the receiver to disconnect
+ */
+ abstract public void onDisconnect(MidiReceiver receiver);
+}