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 an
   * limitations under the License.
   */
  
  package com.android.server.usb;
  
  import android.annotation.NonNull;
  import android.content.Context;
  import android.media.midi.MidiDeviceInfo;
  import android.media.midi.MidiDeviceServer;
  import android.media.midi.MidiDeviceStatus;
  import android.media.midi.MidiManager;
  import android.media.midi.MidiReceiver;
  import android.os.Bundle;
  import android.service.usb.UsbMidiDeviceProto;
  import android.system.ErrnoException;
  import android.system.Os;
  import android.system.OsConstants;
  import android.system.StructPollfd;
  import android.util.Log;
  
  import com.android.internal.midi.MidiEventScheduler;
  import com.android.internal.midi.MidiEventScheduler.MidiEvent;
  import com.android.internal.util.dump.DualDumpOutputStream;
  
  import libcore.io.IoUtils;
  
  import java.io.Closeable;
  import java.io.FileDescriptor;
  import java.io.FileInputStream;
  import java.io.FileOutputStream;
  import java.io.IOException;
  
  public final class UsbMidiDevice implements Closeable {
      private static final String TAG = "UsbMidiDevice";
  
      private final int mAlsaCard;
      private final int mAlsaDevice;
      private final int mSubdeviceCount;
      private final InputReceiverProxy[] mInputPortReceivers;
  
      private MidiDeviceServer mServer;
  
      // event schedulers for each input port of the physical device
      private MidiEventScheduler[] mEventSchedulers;
  
      private static final int BUFFER_SIZE = 512;
  
      private FileDescriptor[] mFileDescriptors;
  
      // for polling multiple FileDescriptors for MIDI events
      private StructPollfd[] mPollFDs;
      // streams for reading from ALSA driver
      private FileInputStream[] mInputStreams;
      // streams for writing to ALSA driver
      private FileOutputStream[] mOutputStreams;
  
      private final Object mLock = new Object();
      private boolean mIsOpen;
  
      // pipe file descriptor for signalling input thread to exit
      // only accessed from JNI code
      private int mPipeFD = -1;
  
      private final MidiDeviceServer.Callback mCallback = new MidiDeviceServer.Callback() {
  
          @Override
          public void onDeviceStatusChanged(MidiDeviceServer server, MidiDeviceStatus status) {
              MidiDeviceInfo deviceInfo = status.getDeviceInfo();
              int inputPorts = deviceInfo.getInputPortCount();
              int outputPorts = deviceInfo.getOutputPortCount();
              boolean hasOpenPorts = false;
  
              for (int i = 0; i < inputPorts; i++) {
                  if (status.isInputPortOpen(i)) {
                      hasOpenPorts = true;
                      break;
                  }
              }
  
              if (!hasOpenPorts) {
                  for (int i = 0; i < outputPorts; i++) {
                      if (status.getOutputPortOpenCount(i) > 0) {
                          hasOpenPorts = true;
                          break;
                      }
                  }
              }
  
              synchronized (mLock) {
                  if (hasOpenPorts && !mIsOpen) {
                      openLocked();
                  } else if (!hasOpenPorts && mIsOpen) {
                      closeLocked();
                  }
              }
          }
  
          @Override
          public void onClose() {
          }
      };
  
      // This class acts as a proxy for our MidiEventScheduler receivers, which do not exist
      // until the device has active clients
      private final class InputReceiverProxy extends MidiReceiver {
          private MidiReceiver mReceiver;
  
          @Override
          public void onSend(byte[] msg, int offset, int count, long timestamp) throws IOException {
              MidiReceiver receiver = mReceiver;
              if (receiver != null) {
                  receiver.send(msg, offset, count, timestamp);
              }
          }
  
          public void setReceiver(MidiReceiver receiver) {
              mReceiver = receiver;
          }
  
          @Override
          public void onFlush() throws IOException {
              MidiReceiver receiver = mReceiver;
              if (receiver != null) {
                  receiver.flush();
              }
          }
      }
  
      public static UsbMidiDevice create(Context context, Bundle properties, int card, int device) {
          // FIXME - support devices with different number of input and output ports
          int subDeviceCount = nativeGetSubdeviceCount(card, device);
          if (subDeviceCount <= 0) {
              Log.e(TAG, "nativeGetSubdeviceCount failed");
              return null;
          }
  
          UsbMidiDevice midiDevice = new UsbMidiDevice(card, device, subDeviceCount);
          if (!midiDevice.register(context, properties)) {
              IoUtils.closeQuietly(midiDevice);
              Log.e(TAG, "createDeviceServer failed");
              return null;
          }
          return midiDevice;
      }
  
      private UsbMidiDevice(int card, int device, int subdeviceCount) {
          mAlsaCard = card;
          mAlsaDevice = device;
          mSubdeviceCount = subdeviceCount;
  
          // FIXME - support devices with different number of input and output ports
          int inputPortCount = subdeviceCount;
          mInputPortReceivers = new InputReceiverProxy[inputPortCount];
          for (int port = 0; port < inputPortCount; port++) {
              mInputPortReceivers[port] = new InputReceiverProxy();
          }
      }
  
      private boolean openLocked() {
          // FIXME - support devices with different number of input and output ports
          FileDescriptor[] fileDescriptors = nativeOpen(mAlsaCard, mAlsaDevice, mSubdeviceCount);
          if (fileDescriptors == null) {
              Log.e(TAG, "nativeOpen failed");
              return false;
          }
  
          mFileDescriptors = fileDescriptors;
          int inputStreamCount = fileDescriptors.length;
          // last file descriptor returned from nativeOpen() is only used for unblocking Os.poll()
          // in our input thread
          int outputStreamCount = fileDescriptors.length - 1;
  
          mPollFDs = new StructPollfd[inputStreamCount];
          mInputStreams = new FileInputStream[inputStreamCount];
          for (int i = 0; i < inputStreamCount; i++) {
              FileDescriptor fd = fileDescriptors[i];
              StructPollfd pollfd = new StructPollfd();
              pollfd.fd = fd;
              pollfd.events = (short)OsConstants.POLLIN;
              mPollFDs[i] = pollfd;
              mInputStreams[i] = new FileInputStream(fd);
          }
  
          mOutputStreams = new FileOutputStream[outputStreamCount];
          mEventSchedulers = new MidiEventScheduler[outputStreamCount];
          for (int i = 0; i < outputStreamCount; i++) {
              mOutputStreams[i] = new FileOutputStream(fileDescriptors[i]);
  
              MidiEventScheduler scheduler = new MidiEventScheduler();
              mEventSchedulers[i] = scheduler;
              mInputPortReceivers[i].setReceiver(scheduler.getReceiver());
          }
  
          final MidiReceiver[] outputReceivers = mServer.getOutputPortReceivers();
  
          // Create input thread which will read from all output ports of the physical device
          new Thread("UsbMidiDevice input thread") {
              @Override
              public void run() {
                  byte[] buffer = new byte[BUFFER_SIZE];
                  try {
                      while (true) {
                          // Record time of event immediately after waking.
                          long timestamp = System.nanoTime();
                          synchronized (mLock) {
                              if (!mIsOpen) break;
  
                              // look for a readable FileDescriptor
                              for (int index = 0; index < mPollFDs.length; index++) {
                                  StructPollfd pfd = mPollFDs[index];
                                  if ((pfd.revents & (OsConstants.POLLERR
                                                              | OsConstants.POLLHUP)) != 0) {
                                      break;
                                  } else if ((pfd.revents & OsConstants.POLLIN) != 0) {
                                      // clear readable flag
                                      pfd.revents = 0;
  
                                      if (index == mInputStreams.length - 1) {
                                          // last file descriptor is used only for unblocking Os.poll()
                                          break;
                                      }
  
                                      int count = mInputStreams[index].read(buffer);
                                      outputReceivers[index].send(buffer, 0, count, timestamp);
                                  }
                              }
                          }
  
                          // wait until we have a readable port or we are signalled to close
                          Os.poll(mPollFDs, -1 /* infinite timeout */);
                       }
                  } catch (IOException e) {
                      Log.d(TAG, "reader thread exiting");
                  } catch (ErrnoException e) {
                      Log.d(TAG, "reader thread exiting");
                  }
                  Log.d(TAG, "input thread exit");
              }
          }.start();
  
          // Create output thread for each input port of the physical device
          for (int port = 0; port < outputStreamCount; port++) {
              final MidiEventScheduler eventSchedulerF = mEventSchedulers[port];
              final FileOutputStream outputStreamF = mOutputStreams[port];
              final int portF = port;
  
              new Thread("UsbMidiDevice output thread " + port) {
                  @Override
                  public void run() {
                      while (true) {
                          MidiEvent event;
                          try {
                              event = (MidiEvent)eventSchedulerF.waitNextEvent();
                          } catch (InterruptedException e) {
                              // try again
                              continue;
                          }
                          if (event == null) {
                              break;
                          }
                          try {
                              outputStreamF.write(event.data, 0, event.count);
                          } catch (IOException e) {
                              Log.e(TAG, "write failed for port " + portF);
                          }
                          eventSchedulerF.addEventToPool(event);
                      }
                      Log.d(TAG, "output thread exit");
                  }
              }.start();
          }
  
          mIsOpen = true;
          return true;
      }
  
      private boolean register(Context context, Bundle properties) {
          MidiManager midiManager = (MidiManager)context.getSystemService(Context.MIDI_SERVICE);
          if (midiManager == null) {
              Log.e(TAG, "No MidiManager in UsbMidiDevice.create()");
              return false;
          }
  
          mServer = midiManager.createDeviceServer(mInputPortReceivers, mSubdeviceCount,
                  null, null, properties, MidiDeviceInfo.TYPE_USB, mCallback);
          if (mServer == null) {
              return false;
          }
  
          return true;
      }
  
      @Override
      public void close() throws IOException {
          synchronized (mLock) {
              if (mIsOpen) {
                  closeLocked();
              }
          }
  
          if (mServer != null) {
              IoUtils.closeQuietly(mServer);
          }
      }
  
      private void closeLocked() {
          for (int i = 0; i < mEventSchedulers.length; i++) {
              mInputPortReceivers[i].setReceiver(null);
              mEventSchedulers[i].close();
          }
          mEventSchedulers = null;
  
          for (int i = 0; i < mInputStreams.length; i++) {
              IoUtils.closeQuietly(mInputStreams[i]);
          }
          mInputStreams = null;
  
          for (int i = 0; i < mOutputStreams.length; i++) {
              IoUtils.closeQuietly(mOutputStreams[i]);
          }
          mOutputStreams = null;
  
          // nativeClose will close the file descriptors and signal the input thread to exit
          nativeClose(mFileDescriptors);
          mFileDescriptors = null;
  
          mIsOpen = false;
      }
  
      /**
       * Write a description of the device to a dump stream.
       */
      public void dump(String deviceAddr, @NonNull DualDumpOutputStream dump, @NonNull String idName,
              long id) {
          long token = dump.start(idName, id);
  
          dump.write("device_address", UsbMidiDeviceProto.DEVICE_ADDRESS, deviceAddr);
          dump.write("card", UsbMidiDeviceProto.CARD, mAlsaCard);
          dump.write("device", UsbMidiDeviceProto.DEVICE, mAlsaDevice);
  
          dump.end(token);
      }
  
      private static native int nativeGetSubdeviceCount(int card, int device);
      private native FileDescriptor[] nativeOpen(int card, int device, int subdeviceCount);
      private native void nativeClose(FileDescriptor[] fileDescriptors);
  }
  