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 com.android.server.usb;
  
  import android.content.Context;
  import android.content.pm.PackageManager;
  import android.content.res.Resources;
  import android.hardware.usb.UsbDevice;
  import android.media.IAudioService;
  import android.media.midi.MidiDeviceInfo;
  import android.os.Bundle;
  import android.os.ServiceManager;
  import android.provider.Settings;
  import android.service.usb.UsbAlsaManagerProto;
  import android.util.Slog;
  
  import com.android.internal.alsa.AlsaCardsParser;
  import com.android.internal.util.dump.DualDumpOutputStream;
  import com.android.server.usb.descriptors.UsbDescriptorParser;
  
  import libcore.io.IoUtils;
  
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.HashMap;
  import java.util.List;
  
  /**
   * UsbAlsaManager manages USB audio and MIDI devices.
   */
  public final class UsbAlsaManager {
      private static final String TAG = UsbAlsaManager.class.getSimpleName();
      private static final boolean DEBUG = false;
  
      // Flag to turn on/off multi-peripheral select mode
      // Set to true to have single-device-only mode
      private static final boolean mIsSingleMode = true;
  
      private static final String ALSA_DIRECTORY = "/dev/snd/";
  
      private final Context mContext;
      private IAudioService mAudioService;
      private final boolean mHasMidiFeature;
  
      private final AlsaCardsParser mCardsParser = new AlsaCardsParser();
  
      // this is needed to map USB devices to ALSA Audio Devices, especially to remove an
      // ALSA device when we are notified that its associated USB device has been removed.
      private final ArrayList<UsbAlsaDevice> mAlsaDevices = new ArrayList<UsbAlsaDevice>();
      private UsbAlsaDevice mSelectedDevice;
  
      //
      // Device Blacklist
      //
      // This exists due to problems with Sony game controllers which present as an audio device
      // even if no headset is connected and have no way to set the volume on the unit.
      // Handle this by simply declining to use them as an audio device.
      private static final int USB_VENDORID_SONY = 0x054C;
      private static final int USB_PRODUCTID_PS4CONTROLLER_ZCT1 = 0x05C4;
      private static final int USB_PRODUCTID_PS4CONTROLLER_ZCT2 = 0x09CC;
  
      private static final int USB_BLACKLIST_OUTPUT = 0x0001;
      private static final int USB_BLACKLIST_INPUT  = 0x0002;
  
      private static class BlackListEntry {
          final int mVendorId;
          final int mProductId;
          final int mFlags;
  
          BlackListEntry(int vendorId, int productId, int flags) {
              mVendorId = vendorId;
              mProductId = productId;
              mFlags = flags;
          }
      }
  
      static final List<BlackListEntry> sDeviceBlacklist = Arrays.asList(
              new BlackListEntry(USB_VENDORID_SONY,
                      USB_PRODUCTID_PS4CONTROLLER_ZCT1,
                      USB_BLACKLIST_OUTPUT),
              new BlackListEntry(USB_VENDORID_SONY,
                      USB_PRODUCTID_PS4CONTROLLER_ZCT2,
                      USB_BLACKLIST_OUTPUT));
  
      private static boolean isDeviceBlacklisted(int vendorId, int productId, int flags) {
          for (BlackListEntry entry : sDeviceBlacklist) {
              if (entry.mVendorId == vendorId && entry.mProductId == productId) {
                  // see if the type flag is set
                  return (entry.mFlags & flags) != 0;
              }
          }
  
          return false;
      }
  
      /**
       * List of connected MIDI devices
       */
      private final HashMap<String, UsbMidiDevice>
              mMidiDevices = new HashMap<String, UsbMidiDevice>();
  
      // UsbMidiDevice for USB peripheral mode (gadget) device
      private UsbMidiDevice mPeripheralMidiDevice = null;
  
      /* package */ UsbAlsaManager(Context context) {
          mContext = context;
          mHasMidiFeature = context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_MIDI);
      }
  
      public void systemReady() {
          mAudioService = IAudioService.Stub.asInterface(
                          ServiceManager.getService(Context.AUDIO_SERVICE));
      }
  
      /**
       * Select the AlsaDevice to be used for AudioService.
       * AlsaDevice.start() notifies AudioService of it's connected state.
       *
       * @param alsaDevice The selected UsbAlsaDevice for system USB audio.
       */
      private synchronized void selectAlsaDevice(UsbAlsaDevice alsaDevice) {
          if (DEBUG) {
              Slog.d(TAG, "selectAlsaDevice() " + alsaDevice);
          }
  
          // This must be where an existing USB audio device is deselected.... (I think)
          if (mIsSingleMode && mSelectedDevice != null) {
              deselectAlsaDevice();
          }
  
          // FIXME Does not yet handle the case where the setting is changed
          // after device connection.  Ideally we should handle the settings change
          // in SettingsObserver. Here we should log that a USB device is connected
          // and disconnected with its address (card , device) and force the
          // connection or disconnection when the setting changes.
          int isDisabled = Settings.Secure.getInt(mContext.getContentResolver(),
                  Settings.Secure.USB_AUDIO_AUTOMATIC_ROUTING_DISABLED, 0);
          if (isDisabled != 0) {
              return;
          }
  
          mSelectedDevice = alsaDevice;
          alsaDevice.start();
          if (DEBUG) {
              Slog.d(TAG, "selectAlsaDevice() - done.");
          }
      }
  
      private synchronized void deselectAlsaDevice() {
          if (DEBUG) {
              Slog.d(TAG, "deselectAlsaDevice() mSelectedDevice " + mSelectedDevice);
          }
          if (mSelectedDevice != null) {
              mSelectedDevice.stop();
              mSelectedDevice = null;
          }
      }
  
      private int getAlsaDeviceListIndexFor(String deviceAddress) {
          for (int index = 0; index < mAlsaDevices.size(); index++) {
              if (mAlsaDevices.get(index).getDeviceAddress().equals(deviceAddress)) {
                  return index;
              }
          }
          return -1;
      }
  
      private UsbAlsaDevice removeAlsaDeviceFromList(String deviceAddress) {
          int index = getAlsaDeviceListIndexFor(deviceAddress);
          if (index > -1) {
              return mAlsaDevices.remove(index);
          } else {
              return null;
          }
      }
  
      /* package */ UsbAlsaDevice selectDefaultDevice() {
          if (DEBUG) {
              Slog.d(TAG, "selectDefaultDevice()");
          }
  
          if (mAlsaDevices.size() > 0) {
              UsbAlsaDevice alsaDevice = mAlsaDevices.get(0);
              if (DEBUG) {
                  Slog.d(TAG, "  alsaDevice:" + alsaDevice);
              }
              if (alsaDevice != null) {
                  selectAlsaDevice(alsaDevice);
              }
              return alsaDevice;
          } else {
              return null;
          }
      }
  
      /* package */ void usbDeviceAdded(String deviceAddress, UsbDevice usbDevice,
              UsbDescriptorParser parser) {
          if (DEBUG) {
              Slog.d(TAG, "usbDeviceAdded(): " + usbDevice.getManufacturerName()
                      + " nm:" + usbDevice.getProductName());
          }
  
          // Scan the Alsa File Space
          mCardsParser.scan();
  
          // Find the ALSA spec for this device address
          AlsaCardsParser.AlsaCardRecord cardRec =
                  mCardsParser.findCardNumFor(deviceAddress);
          if (cardRec == null) {
              return;
          }
  
          // Add it to the devices list
          boolean hasInput = parser.hasInput()
                  && !isDeviceBlacklisted(usbDevice.getVendorId(), usbDevice.getProductId(),
                          USB_BLACKLIST_INPUT);
          boolean hasOutput = parser.hasOutput()
                  && !isDeviceBlacklisted(usbDevice.getVendorId(), usbDevice.getProductId(),
                          USB_BLACKLIST_OUTPUT);
          if (DEBUG) {
              Slog.d(TAG, "hasInput: " + hasInput + " hasOutput:" + hasOutput);
          }
          if (hasInput || hasOutput) {
              boolean isInputHeadset = parser.isInputHeadset();
              boolean isOutputHeadset = parser.isOutputHeadset();
  
              if (mAudioService == null) {
                  Slog.e(TAG, "no AudioService");
                  return;
              }
  
              UsbAlsaDevice alsaDevice =
                      new UsbAlsaDevice(mAudioService, cardRec.getCardNum(), 0 /*device*/,
                                        deviceAddress, hasOutput, hasInput,
                                        isInputHeadset, isOutputHeadset);
              if (alsaDevice != null) {
                  alsaDevice.setDeviceNameAndDescription(
                            cardRec.getCardName(), cardRec.getCardDescription());
                  mAlsaDevices.add(0, alsaDevice);
                  selectAlsaDevice(alsaDevice);
              }
          }
  
          // look for MIDI devices
          boolean hasMidi = parser.hasMIDIInterface();
          if (DEBUG) {
              Slog.d(TAG, "hasMidi: " + hasMidi + " mHasMidiFeature:" + mHasMidiFeature);
          }
          if (hasMidi && mHasMidiFeature) {
              int device = 0;
              Bundle properties = new Bundle();
              String manufacturer = usbDevice.getManufacturerName();
              String product = usbDevice.getProductName();
              String version = usbDevice.getVersion();
              String name;
              if (manufacturer == null || manufacturer.isEmpty()) {
                  name = product;
              } else if (product == null || product.isEmpty()) {
                  name = manufacturer;
              } else {
                  name = manufacturer + " " + product;
              }
              properties.putString(MidiDeviceInfo.PROPERTY_NAME, name);
              properties.putString(MidiDeviceInfo.PROPERTY_MANUFACTURER, manufacturer);
              properties.putString(MidiDeviceInfo.PROPERTY_PRODUCT, product);
              properties.putString(MidiDeviceInfo.PROPERTY_VERSION, version);
              properties.putString(MidiDeviceInfo.PROPERTY_SERIAL_NUMBER,
                      usbDevice.getSerialNumber());
              properties.putInt(MidiDeviceInfo.PROPERTY_ALSA_CARD, cardRec.getCardNum());
              properties.putInt(MidiDeviceInfo.PROPERTY_ALSA_DEVICE, 0 /*deviceNum*/);
              properties.putParcelable(MidiDeviceInfo.PROPERTY_USB_DEVICE, usbDevice);
  
              UsbMidiDevice usbMidiDevice = UsbMidiDevice.create(mContext, properties,
                      cardRec.getCardNum(), 0 /*device*/);
              if (usbMidiDevice != null) {
                  mMidiDevices.put(deviceAddress, usbMidiDevice);
              }
          }
  
          logDevices("deviceAdded()");
  
          if (DEBUG) {
              Slog.d(TAG, "deviceAdded() - done");
          }
      }
  
      /* package */ synchronized void usbDeviceRemoved(String deviceAddress/*UsbDevice usbDevice*/) {
          if (DEBUG) {
              Slog.d(TAG, "deviceRemoved(" + deviceAddress + ")");
          }
  
          // Audio
          UsbAlsaDevice alsaDevice = removeAlsaDeviceFromList(deviceAddress);
          Slog.i(TAG, "USB Audio Device Removed: " + alsaDevice);
          if (alsaDevice != null && alsaDevice == mSelectedDevice) {
              deselectAlsaDevice();
              selectDefaultDevice(); // if there any external devices left, select one of them
          }
  
          // MIDI
          UsbMidiDevice usbMidiDevice = mMidiDevices.remove(deviceAddress);
          if (usbMidiDevice != null) {
              Slog.i(TAG, "USB MIDI Device Removed: " + usbMidiDevice);
              IoUtils.closeQuietly(usbMidiDevice);
          }
  
          logDevices("usbDeviceRemoved()");
  
      }
  
     /* package */ void setPeripheralMidiState(boolean enabled, int card, int device) {
          if (!mHasMidiFeature) {
              return;
          }
  
          if (enabled && mPeripheralMidiDevice == null) {
              Bundle properties = new Bundle();
              Resources r = mContext.getResources();
              properties.putString(MidiDeviceInfo.PROPERTY_NAME, r.getString(
                      com.android.internal.R.string.usb_midi_peripheral_name));
              properties.putString(MidiDeviceInfo.PROPERTY_MANUFACTURER, r.getString(
                      com.android.internal.R.string.usb_midi_peripheral_manufacturer_name));
              properties.putString(MidiDeviceInfo.PROPERTY_PRODUCT, r.getString(
                      com.android.internal.R.string.usb_midi_peripheral_product_name));
              properties.putInt(MidiDeviceInfo.PROPERTY_ALSA_CARD, card);
              properties.putInt(MidiDeviceInfo.PROPERTY_ALSA_DEVICE, device);
              mPeripheralMidiDevice = UsbMidiDevice.create(mContext, properties, card, device);
          } else if (!enabled && mPeripheralMidiDevice != null) {
              IoUtils.closeQuietly(mPeripheralMidiDevice);
              mPeripheralMidiDevice = null;
          }
     }
  
      //
      // Devices List
      //
  /*
      //import java.util.ArrayList;
      public ArrayList<UsbAudioDevice> getConnectedDevices() {
          ArrayList<UsbAudioDevice> devices = new ArrayList<UsbAudioDevice>(mAudioDevices.size());
          for (HashMap.Entry<UsbDevice,UsbAudioDevice> entry : mAudioDevices.entrySet()) {
              devices.add(entry.getValue());
          }
          return devices;
      }
  */
  
      /**
       * Dump the USB alsa state.
       */
      // invoked with "adb shell dumpsys usb"
      public void dump(DualDumpOutputStream dump, String idName, long id) {
          long token = dump.start(idName, id);
  
          dump.write("cards_parser", UsbAlsaManagerProto.CARDS_PARSER, mCardsParser.getScanStatus());
  
          for (UsbAlsaDevice usbAlsaDevice : mAlsaDevices) {
              usbAlsaDevice.dump(dump, "alsa_devices", UsbAlsaManagerProto.ALSA_DEVICES);
          }
  
          for (String deviceAddr : mMidiDevices.keySet()) {
              // A UsbMidiDevice does not have a handle to the UsbDevice anymore
              mMidiDevices.get(deviceAddr).dump(deviceAddr, dump, "midi_devices",
                      UsbAlsaManagerProto.MIDI_DEVICES);
          }
  
          dump.end(token);
      }
  
      public void logDevicesList(String title) {
          if (DEBUG) {
              Slog.i(TAG, title + "----------------");
              for (UsbAlsaDevice alsaDevice : mAlsaDevices) {
                  Slog.i(TAG, "  -->");
                  Slog.i(TAG, "" + alsaDevice);
                  Slog.i(TAG, "  <--");
              }
              Slog.i(TAG, "----------------");
          }
      }
  
      // This logs a more terse (and more readable) version of the devices list
      public void logDevices(String title) {
          if (DEBUG) {
              Slog.i(TAG, title + "----------------");
              for (UsbAlsaDevice alsaDevice : mAlsaDevices) {
                  Slog.i(TAG, alsaDevice.toShortString());
              }
              Slog.i(TAG, "----------------");
          }
      }
  }
  