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