blob: 51d552066bc60d2886a663c7a32b5f10fdeb560e [file] [log] [blame]
/*
* 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;
}
}