diff options
author | Justin Klaassen <justinklaassen@google.com> | 2017-09-15 17:58:39 -0400 |
---|---|---|
committer | Justin Klaassen <justinklaassen@google.com> | 2017-09-15 17:58:39 -0400 |
commit | 10d07c88d69cc64f73a069163e7ea5ba2519a099 (patch) | |
tree | 8dbd149eb350320a29c3d10e7ad3201de1c5cbee /android/media/midi | |
parent | 677516fb6b6f207d373984757d3d9450474b6b00 (diff) | |
download | android-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.java | 308 | ||||
-rw-r--r-- | android/media/midi/MidiDeviceInfo.java | 390 | ||||
-rw-r--r-- | android/media/midi/MidiDeviceServer.java | 452 | ||||
-rw-r--r-- | android/media/midi/MidiDeviceService.java | 145 | ||||
-rw-r--r-- | android/media/midi/MidiDeviceStatus.java | 138 | ||||
-rw-r--r-- | android/media/midi/MidiInputPort.java | 173 | ||||
-rw-r--r-- | android/media/midi/MidiManager.java | 327 | ||||
-rw-r--r-- | android/media/midi/MidiOutputPort.java | 159 | ||||
-rw-r--r-- | android/media/midi/MidiPortImpl.java | 134 | ||||
-rw-r--r-- | android/media/midi/MidiReceiver.java | 133 | ||||
-rw-r--r-- | android/media/midi/MidiSender.java | 62 |
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> + * <service android:name=".VirtualDeviceService" + * android:label="@string/service_name"> + * <intent-filter> + * <action android:name="android.media.midi.MidiDeviceService" /> + * </intent-filter> + * <meta-data android:name="android.media.midi.MidiDeviceService" + android:resource="@xml/device_info" /> + * </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); +} |