Merge "MidiManager: Add MIDI device status notifications"

This commit is contained in:
Mike Lockwood
2015-03-11 19:43:47 +00:00
committed by Android (Google) Code Review
10 changed files with 380 additions and 29 deletions

View File

@@ -17,10 +17,12 @@
package android.media.midi;
import android.media.midi.MidiDeviceInfo;
import android.media.midi.MidiDeviceStatus;
/** @hide */
oneway interface IMidiDeviceListener
{
void onDeviceAdded(in MidiDeviceInfo device);
void onDeviceRemoved(in MidiDeviceInfo device);
void onDeviceStatusChanged(in MidiDeviceStatus status);
}

View File

@@ -19,6 +19,7 @@ package android.media.midi;
import android.media.midi.IMidiDeviceListener;
import android.media.midi.IMidiDeviceServer;
import android.media.midi.MidiDeviceInfo;
import android.media.midi.MidiDeviceStatus;
import android.os.Bundle;
import android.os.IBinder;
@@ -44,4 +45,11 @@ interface IMidiManager
// used by MidiDeviceService to access the MidiDeviceInfo that was created based on its
// manifest's meta-data
MidiDeviceInfo getServiceDeviceInfo(String packageName, String className);
// used for client's to retrieve a device's MidiDeviceStatus
MidiDeviceStatus getDeviceStatus(in MidiDeviceInfo deviceInfo);
// used by MIDI devices to report their status
// the token is used by MidiService for death notification
void setDeviceStatus(IBinder token, in MidiDeviceStatus status);
}

View File

@@ -16,8 +16,8 @@
package android.media.midi;
import android.os.IBinder;
import android.os.Binder;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.os.RemoteException;
@@ -61,7 +61,25 @@ public final class MidiDeviceServer implements Closeable {
private final CopyOnWriteArrayList<MidiInputPort> mInputPorts
= new CopyOnWriteArrayList<MidiInputPort>();
// for reporting device status
private final IBinder mDeviceStatusToken = new Binder();
private final boolean[] mInputPortBusy;
private final int[] mOutputPortOpenCount;
private final CloseGuard mGuard = CloseGuard.get();
private boolean mIsClosed;
private final Callback mCallback;
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);
}
abstract private class PortClient implements IBinder.DeathRecipient {
final IBinder mToken;
@@ -96,7 +114,10 @@ public final class MidiDeviceServer implements Closeable {
void close() {
mToken.unlinkToDeath(this, 0);
synchronized (mInputPortOutputPorts) {
mInputPortOutputPorts[mOutputPort.getPortNumber()] = null;
int portNumber = mOutputPort.getPortNumber();
mInputPortOutputPorts[portNumber] = null;
mInputPortBusy[portNumber] = false;
updateDeviceStatus();
}
IoUtils.closeQuietly(mOutputPort);
}
@@ -113,7 +134,15 @@ public final class MidiDeviceServer implements Closeable {
@Override
void close() {
mToken.unlinkToDeath(this, 0);
mOutputPortDispatchers[mInputPort.getPortNumber()].getSender().disconnect(mInputPort);
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);
}
@@ -153,6 +182,8 @@ public final class MidiDeviceServer implements Closeable {
synchronized (mPortClients) {
mPortClients.put(token, client);
}
mInputPortBusy[portNumber] = true;
updateDeviceStatus();
return pair[1];
} catch (IOException e) {
Log.e(TAG, "unable to create ParcelFileDescriptors in openInputPort");
@@ -178,7 +209,14 @@ public final class MidiDeviceServer implements Closeable {
ParcelFileDescriptor[] pair = ParcelFileDescriptor.createSocketPair(
OsConstants.SOCK_SEQPACKET);
MidiInputPort inputPort = new MidiInputPort(pair[0], portNumber);
mOutputPortDispatchers[portNumber].getSender().connect(inputPort);
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) {
@@ -215,11 +253,12 @@ public final class MidiDeviceServer implements Closeable {
};
/* package */ MidiDeviceServer(IMidiManager midiManager, MidiReceiver[] inputPortReceivers,
int numOutputPorts) {
int numOutputPorts, Callback callback) {
mMidiManager = midiManager;
mInputPortReceivers = inputPortReceivers;
mInputPortCount = inputPortReceivers.length;
mOutputPortCount = numOutputPorts;
mCallback = callback;
mInputPortOutputPorts = new MidiOutputPort[mInputPortCount];
@@ -228,6 +267,9 @@ public final class MidiDeviceServer implements Closeable {
mOutputPortDispatchers[i] = new MidiDispatcher();
}
mInputPortBusy = new boolean[mInputPortCount];
mOutputPortOpenCount = new int[numOutputPorts];
mGuard.open("close");
}
@@ -242,9 +284,28 @@ public final class MidiDeviceServer implements Closeable {
mDeviceInfo = deviceInfo;
}
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, mInputPortBusy,
mOutputPortOpenCount);
if (mCallback != null) {
mCallback.onDeviceStatusChanged(this, status);
}
try {
mMidiManager.setDeviceStatus(mDeviceStatusToken, 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++) {
@@ -263,6 +324,7 @@ public final class MidiDeviceServer implements Closeable {
} catch (RemoteException e) {
Log.e(TAG, "RemoteException in unregisterDeviceServer");
}
mIsClosed = true;
}
}

View File

@@ -57,6 +57,13 @@ abstract public class MidiDeviceService extends Service {
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 onCreate() {
mMidiManager = IMidiManager.Stub.asInterface(
@@ -75,7 +82,7 @@ abstract public class MidiDeviceService extends Service {
inputPortReceivers = new MidiReceiver[0];
}
server = new MidiDeviceServer(mMidiManager, inputPortReceivers,
deviceInfo.getOutputPortCount());
deviceInfo.getOutputPortCount(), mCallback);
server.setDeviceInfo(deviceInfo);
} catch (RemoteException e) {
Log.e(TAG, "RemoteException in IMidiManager.getServiceDeviceInfo");
@@ -114,6 +121,13 @@ abstract public class MidiDeviceService extends Service {
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) {
}
@Override
public IBinder onBind(Intent intent) {
if (SERVICE_INTERFACE.equals(intent.getAction()) && mServer != null) {

View File

@@ -0,0 +1,19 @@
/*
* 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;
parcelable MidiDeviceStatus;

View File

@@ -0,0 +1,141 @@
/*
* 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.
*
* CANDIDATE FOR PUBLIC API
* @hide
*/
public final class MidiDeviceStatus implements Parcelable {
private static final String TAG = "MidiDeviceStatus";
private final MidiDeviceInfo mDeviceInfo;
// true if input ports are busy
private final boolean mInputPortBusy[];
// open counts for output ports
private final int mOutputPortOpenCount[];
/**
* @hide
*/
public MidiDeviceStatus(MidiDeviceInfo deviceInfo, boolean inputPortBusy[],
int outputPortOpenCount[]) {
// MidiDeviceInfo is immutable so we can share references
mDeviceInfo = deviceInfo;
// make copies of the arrays
mInputPortBusy = new boolean[inputPortBusy.length];
System.arraycopy(inputPortBusy, 0, mInputPortBusy, 0, inputPortBusy.length);
mOutputPortOpenCount = new int[outputPortOpenCount.length];
System.arraycopy(outputPortOpenCount, 0, mOutputPortOpenCount, 0,
outputPortOpenCount.length);
}
/**
* Creates a MidiDeviceStatus with false for all input port busy values
* and zero for all output port open counts
* @hide
*/
public MidiDeviceStatus(MidiDeviceInfo deviceInfo) {
mDeviceInfo = deviceInfo;
mInputPortBusy = 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 busy.
*
* @param input port's port number
* @return input port busy status
*/
public boolean isInputPortBusy(int portNumber) {
return mInputPortBusy[portNumber];
}
/**
* Returns the open count for an output port.
*
* @param output port's port number
* @return output port open count
*/
public int getOutputPortOpenCount(int portNumber) {
return mOutputPortOpenCount[portNumber];
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder(mDeviceInfo.toString());
int inputPortCount = mDeviceInfo.getInputPortCount();
int outputPortCount = mDeviceInfo.getOutputPortCount();
builder.append(" mInputPortBusy=[");
for (int i = 0; i < inputPortCount; i++) {
builder.append(mInputPortBusy[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[] inputPortBusy = in.createBooleanArray();
int[] outputPortOpenCount = in.createIntArray();
return new MidiDeviceStatus(deviceInfo, inputPortBusy, 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(mInputPortBusy);
parcel.writeIntArray(mOutputPortOpenCount);
}
}

View File

@@ -55,11 +55,11 @@ public final class MidiDispatcher extends MidiReceiver {
};
/**
* Returns whether this dispatcher contains any receivers.
* @return true if the receiver list is not empty
* Returns the number of {@link MidiReceiver}s this dispatcher contains.
* @return the number of receivers
*/
public boolean hasReceivers() {
return mReceivers.size() > 0;
public int getReceiverCount() {
return mReceivers.size();
}
/**

View File

@@ -62,6 +62,7 @@ public final class MidiManager {
mHandler = handler;
}
@Override
public void onDeviceAdded(MidiDeviceInfo device) {
if (mHandler != null) {
final MidiDeviceInfo deviceF = device;
@@ -75,6 +76,7 @@ public final class MidiManager {
}
}
@Override
public void onDeviceRemoved(MidiDeviceInfo device) {
if (mHandler != null) {
final MidiDeviceInfo deviceF = device;
@@ -87,25 +89,49 @@ public final class MidiManager {
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
*/
abstract public static class DeviceCallback {
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
*/
abstract public void onDeviceAdded(MidiDeviceInfo 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
*/
abstract public void onDeviceRemoved(MidiDeviceInfo device);
public void onDeviceRemoved(MidiDeviceInfo device) {
}
/**
* Called to notify when the status of a MIDI device has changed
*
* @param device a {@link MidiDeviceStatus} for the changed device
*/
public void onDeviceStatusChanged(MidiDeviceStatus status) {
}
}
/**
@@ -251,10 +277,10 @@ public final class MidiManager {
/** @hide */
public MidiDeviceServer createDeviceServer(MidiReceiver[] inputPortReceivers,
int numOutputPorts, Bundle properties, int type) {
int numOutputPorts, Bundle properties, int type, MidiDeviceServer.Callback callback) {
try {
MidiDeviceServer server = new MidiDeviceServer(mService, inputPortReceivers,
numOutputPorts);
numOutputPorts, callback);
MidiDeviceInfo deviceInfo = mService.registerDeviceServer(server.getBinderInterface(),
inputPortReceivers.length, numOutputPorts, properties, type);
if (deviceInfo == null) {

View File

@@ -29,6 +29,7 @@ import android.media.midi.IMidiDeviceServer;
import android.media.midi.IMidiManager;
import android.media.midi.MidiDeviceInfo;
import android.media.midi.MidiDeviceService;
import android.media.midi.MidiDeviceStatus;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
@@ -147,6 +148,19 @@ public class MidiService extends IMidiManager.Stub {
}
}
public void deviceStatusChanged(Device device, MidiDeviceStatus status) {
// ignore private devices that our client cannot access
if (!device.isUidAllowed(mUid)) return;
try {
for (IMidiDeviceListener listener : mListeners) {
listener.onDeviceStatusChanged(status);
}
} catch (RemoteException e) {
Log.e(TAG, "remote exception", e);
}
}
public void binderDied() {
removeClient(mToken);
}
@@ -187,6 +201,8 @@ public class MidiService extends IMidiManager.Stub {
private final class Device implements IBinder.DeathRecipient {
private final IMidiDeviceServer mServer;
private final MidiDeviceInfo mDeviceInfo;
private MidiDeviceStatus mDeviceStatus;
private IBinder mDeviceStatusToken;
// ServiceInfo for the device's MidiDeviceServer implementation (virtual devices only)
private final ServiceInfo mServiceInfo;
// UID of device implementation
@@ -204,6 +220,33 @@ public class MidiService extends IMidiManager.Stub {
return mDeviceInfo;
}
public MidiDeviceStatus getDeviceStatus() {
return mDeviceStatus;
}
public void setDeviceStatus(IBinder token, MidiDeviceStatus status) {
mDeviceStatus = status;
if (mDeviceStatusToken == null && token != null) {
// register a death recipient so we can clear the status when the device dies
try {
token.linkToDeath(new IBinder.DeathRecipient() {
@Override
public void binderDied() {
// reset to default status and clear the token
mDeviceStatus = new MidiDeviceStatus(mDeviceInfo);
mDeviceStatusToken = null;
notifyDeviceStatusChanged(Device.this, mDeviceStatus);
}
}, 0);
mDeviceStatusToken = token;
} catch (RemoteException e) {
// reset to default status
mDeviceStatus = new MidiDeviceStatus(mDeviceInfo);
}
}
}
public IMidiDeviceServer getDeviceServer() {
return mServer;
}
@@ -216,6 +259,10 @@ public class MidiService extends IMidiManager.Stub {
return (mServiceInfo == null ? null : mServiceInfo.packageName);
}
public int getUid() {
return mUid;
}
public boolean isUidAllowed(int uid) {
return (!mDeviceInfo.isPrivate() || mUid == uid);
}
@@ -302,13 +349,14 @@ public class MidiService extends IMidiManager.Stub {
@Override
public MidiDeviceInfo registerDeviceServer(IMidiDeviceServer server, int numInputPorts,
int numOutputPorts, Bundle properties, int type) {
if (type != MidiDeviceInfo.TYPE_VIRTUAL && Binder.getCallingUid() != Process.SYSTEM_UID) {
int uid = Binder.getCallingUid();
if (type != MidiDeviceInfo.TYPE_VIRTUAL && uid != Process.SYSTEM_UID) {
throw new SecurityException("only system can create non-virtual devices");
}
synchronized (mDevicesByInfo) {
return addDeviceLocked(type, numInputPorts, numOutputPorts, properties,
server, null, false, -1);
server, null, false, uid);
}
}
@@ -337,6 +385,39 @@ public class MidiService extends IMidiManager.Stub {
}
}
@Override
public MidiDeviceStatus getDeviceStatus(MidiDeviceInfo deviceInfo) {
Device device = mDevicesByInfo.get(deviceInfo);
if (device == null) {
throw new IllegalArgumentException("no such device for " + deviceInfo);
}
return device.getDeviceStatus();
}
@Override
public void setDeviceStatus(IBinder token, MidiDeviceStatus status) {
MidiDeviceInfo deviceInfo = status.getDeviceInfo();
Device device = mDevicesByInfo.get(deviceInfo);
if (device == null) {
// Just return quietly here if device no longer exists
return;
}
if (Binder.getCallingUid() != device.getUid()) {
throw new SecurityException("setDeviceStatus() caller UID " + Binder.getCallingUid()
+ " does not match device's UID " + device.getUid());
}
device.setDeviceStatus(token, status);
notifyDeviceStatusChanged(device, status);
}
private void notifyDeviceStatusChanged(Device device, MidiDeviceStatus status) {
synchronized (mClients) {
for (Client c : mClients.values()) {
c.deviceStatusChanged(device, status);
}
}
}
// synchronize on mDevicesByInfo
private MidiDeviceInfo addDeviceLocked(int type, int numInputPorts, int numOutputPorts,
Bundle properties, IMidiDeviceServer server, ServiceInfo serviceInfo,
@@ -469,17 +550,15 @@ public class MidiService extends IMidiManager.Stub {
continue;
}
int uid = -1;
if (isPrivate) {
try {
ApplicationInfo appInfo = mPackageManager.getApplicationInfo(
serviceInfo.packageName, 0);
uid = appInfo.uid;
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "could not fetch ApplicationInfo for "
+ serviceInfo.packageName);
continue;
}
int uid;
try {
ApplicationInfo appInfo = mPackageManager.getApplicationInfo(
serviceInfo.packageName, 0);
uid = appInfo.uid;
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "could not fetch ApplicationInfo for "
+ serviceInfo.packageName);
continue;
}
synchronized (mDevicesByInfo) {

View File

@@ -121,7 +121,7 @@ public final class UsbMidiDevice implements Closeable {
int outputCount = mOutputStreams.length;
mServer = midiManager.createDeviceServer(mInputPortReceivers, outputCount,
properties, MidiDeviceInfo.TYPE_USB);
properties, MidiDeviceInfo.TYPE_USB, null);
if (mServer == null) {
return false;
}