| /* |
| * 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; |
| } |
| } |