content: /*
   * Copyright (C) 2017 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.descriptors;
  
  import android.hardware.usb.UsbDevice;
  import android.util.Log;
  
  import java.util.ArrayList;
  
  /**
   * @hide
   * Class for parsing a binary stream of USB Descriptors.
   */
  public final class UsbDescriptorParser {
      private static final String TAG = "UsbDescriptorParser";
      private static final boolean DEBUG = false;
  
      private final String mDeviceAddr;
  
      // Descriptor Objects
      private static final int DESCRIPTORS_ALLOC_SIZE = 128;
      private final ArrayList<UsbDescriptor> mDescriptors;
  
      private UsbDeviceDescriptor mDeviceDescriptor;
      private UsbConfigDescriptor mCurConfigDescriptor;
      private UsbInterfaceDescriptor mCurInterfaceDescriptor;
  
      // The AudioClass spec implemented by the AudioClass Interfaces
      // This may well be different than the overall USB Spec.
      // Obtained from the first AudioClass Header descriptor.
      private int mACInterfacesSpec = UsbDeviceDescriptor.USBSPEC_1_0;
  
      /**
       * Connect this parser to an existing set of already parsed descriptors.
       * This is useful for reporting.
       */
      public UsbDescriptorParser(String deviceAddr, ArrayList<UsbDescriptor> descriptors) {
          mDeviceAddr = deviceAddr;
          mDescriptors = descriptors;
          //TODO some error checking here....
          mDeviceDescriptor = (UsbDeviceDescriptor) descriptors.get(0);
      }
  
      /**
       * Connect this parser to an byte array containing unparsed (raw) device descriptors
       * to be parsed (and parse them). Useful for parsing a stored descriptor buffer.
       */
      public UsbDescriptorParser(String deviceAddr, byte[] rawDescriptors) {
          mDeviceAddr = deviceAddr;
          mDescriptors = new ArrayList<UsbDescriptor>(DESCRIPTORS_ALLOC_SIZE);
          parseDescriptors(rawDescriptors);
      }
  
      public String getDeviceAddr() {
          return mDeviceAddr;
      }
  
      /**
       * @return the USB Spec value associated with the Device descriptor for the
       * descriptors stream being parsed.
       *
       * @throws IllegalArgumentException
       */
      public int getUsbSpec() {
          if (mDeviceDescriptor != null) {
              return mDeviceDescriptor.getSpec();
          } else {
              throw new IllegalArgumentException();
          }
      }
  
      public void setACInterfaceSpec(int spec) {
          mACInterfacesSpec = spec;
      }
  
      public int getACInterfaceSpec() {
          return mACInterfacesSpec;
      }
  
      private class UsbDescriptorsStreamFormatException extends Exception {
          String mMessage;
          UsbDescriptorsStreamFormatException(String message) {
              mMessage = message;
          }
  
          public String toString() {
              return "Descriptor Stream Format Exception: " + mMessage;
          }
      }
  
      /**
       * The probability (as returned by getHeadsetProbability() at which we conclude
       * the peripheral is a headset.
       */
      private static final float IN_HEADSET_TRIGGER = 0.75f;
      private static final float OUT_HEADSET_TRIGGER = 0.75f;
  
      private UsbDescriptor allocDescriptor(ByteStream stream)
              throws UsbDescriptorsStreamFormatException {
          stream.resetReadCount();
  
          int length = stream.getUnsignedByte();
          byte type = stream.getByte();
  
          UsbDescriptor descriptor = null;
          switch (type) {
              /*
               * Standard
               */
              case UsbDescriptor.DESCRIPTORTYPE_DEVICE:
                  descriptor = mDeviceDescriptor = new UsbDeviceDescriptor(length, type);
                  break;
  
              case UsbDescriptor.DESCRIPTORTYPE_CONFIG:
                  descriptor = mCurConfigDescriptor = new UsbConfigDescriptor(length, type);
                  if (mDeviceDescriptor != null) {
                      mDeviceDescriptor.addConfigDescriptor(mCurConfigDescriptor);
                  } else {
                      Log.e(TAG, "Config Descriptor found with no associated Device Descriptor!");
                      throw new UsbDescriptorsStreamFormatException(
                              "Config Descriptor found with no associated Device Descriptor!");
                  }
                  break;
  
              case UsbDescriptor.DESCRIPTORTYPE_INTERFACE:
                  descriptor = mCurInterfaceDescriptor = new UsbInterfaceDescriptor(length, type);
                  if (mCurConfigDescriptor != null) {
                      mCurConfigDescriptor.addInterfaceDescriptor(mCurInterfaceDescriptor);
                  } else {
                      Log.e(TAG, "Interface Descriptor found with no associated Config Descriptor!");
                      throw new UsbDescriptorsStreamFormatException(
                              "Interface Descriptor found with no associated Config Descriptor!");
                  }
                  break;
  
              case UsbDescriptor.DESCRIPTORTYPE_ENDPOINT:
                  descriptor = new UsbEndpointDescriptor(length, type);
                  if (mCurInterfaceDescriptor != null) {
                      mCurInterfaceDescriptor.addEndpointDescriptor(
                              (UsbEndpointDescriptor) descriptor);
                  } else {
                      Log.e(TAG,
                              "Endpoint Descriptor found with no associated Interface Descriptor!");
                      throw new UsbDescriptorsStreamFormatException(
                              "Endpoint Descriptor found with no associated Interface Descriptor!");
                  }
                  break;
  
              /*
               * HID
               */
              case UsbDescriptor.DESCRIPTORTYPE_HID:
                  descriptor = new UsbHIDDescriptor(length, type);
                  break;
  
              /*
               * Other
               */
              case UsbDescriptor.DESCRIPTORTYPE_INTERFACEASSOC:
                  descriptor = new UsbInterfaceAssoc(length, type);
                  break;
  
              /*
               * Audio Class Specific
               */
              case UsbDescriptor.DESCRIPTORTYPE_AUDIO_INTERFACE:
                  descriptor = UsbACInterface.allocDescriptor(this, stream, length, type);
                  break;
  
              case UsbDescriptor.DESCRIPTORTYPE_AUDIO_ENDPOINT:
                  descriptor = UsbACEndpoint.allocDescriptor(this, length, type);
                  break;
  
              default:
                  break;
          }
  
          if (descriptor == null) {
              // Unknown Descriptor
              Log.i(TAG, "Unknown Descriptor len: " + length + " type:0x"
                      + Integer.toHexString(type));
              descriptor = new UsbUnknown(length, type);
          }
  
          return descriptor;
      }
  
      public UsbDeviceDescriptor getDeviceDescriptor() {
          return mDeviceDescriptor;
      }
  
      public UsbInterfaceDescriptor getCurInterface() {
          return mCurInterfaceDescriptor;
      }
  
      /**
       * @hide
       */
      public void parseDescriptors(byte[] descriptors) {
          if (DEBUG) {
              Log.d(TAG, "parseDescriptors() - start");
          }
  
          ByteStream stream = new ByteStream(descriptors);
          while (stream.available() > 0) {
              UsbDescriptor descriptor = null;
              try {
                  descriptor = allocDescriptor(stream);
              } catch (Exception ex) {
                  Log.e(TAG, "Exception allocating USB descriptor.", ex);
              }
  
              if (descriptor != null) {
                  // Parse
                  try {
                      descriptor.parseRawDescriptors(stream);
  
                      // Clean up
                      descriptor.postParse(stream);
                  } catch (Exception ex) {
                      Log.e(TAG, "Exception parsing USB descriptors.", ex);
  
                      // Clean up
                      descriptor.setStatus(UsbDescriptor.STATUS_PARSE_EXCEPTION);
                  } finally {
                      mDescriptors.add(descriptor);
                  }
              }
          }
          if (DEBUG) {
              Log.d(TAG, "parseDescriptors() - end " + mDescriptors.size() + " descriptors.");
          }
      }
  
      public byte[] getRawDescriptors() {
          return getRawDescriptors_native(mDeviceAddr);
      }
  
      private native byte[] getRawDescriptors_native(String deviceAddr);
  
      /**
       * @hide
       */
      public String getDescriptorString(int stringId) {
          return getDescriptorString_native(mDeviceAddr, stringId);
      }
  
      private native String getDescriptorString_native(String deviceAddr, int stringId);
  
      public int getParsingSpec() {
          return mDeviceDescriptor != null ? mDeviceDescriptor.getSpec() : 0;
      }
  
      public ArrayList<UsbDescriptor> getDescriptors() {
          return mDescriptors;
      }
  
      /**
       * @hide
       */
      public UsbDevice.Builder toAndroidUsbDevice() {
          if (mDeviceDescriptor == null) {
              Log.e(TAG, "toAndroidUsbDevice() ERROR - No Device Descriptor");
              return null;
          }
  
          UsbDevice.Builder device = mDeviceDescriptor.toAndroid(this);
          if (device == null) {
              Log.e(TAG, "toAndroidUsbDevice() ERROR Creating Device");
          }
          return device;
      }
  
      /**
       * @hide
       */
      public ArrayList<UsbDescriptor> getDescriptors(byte type) {
          ArrayList<UsbDescriptor> list = new ArrayList<UsbDescriptor>();
          for (UsbDescriptor descriptor : mDescriptors) {
              if (descriptor.getType() == type) {
                  list.add(descriptor);
              }
          }
          return list;
      }
  
      /**
       * @hide
       */
      public ArrayList<UsbDescriptor> getInterfaceDescriptorsForClass(int usbClass) {
          ArrayList<UsbDescriptor> list = new ArrayList<UsbDescriptor>();
          for (UsbDescriptor descriptor : mDescriptors) {
              // ensure that this isn't an unrecognized DESCRIPTORTYPE_INTERFACE
              if (descriptor.getType() == UsbDescriptor.DESCRIPTORTYPE_INTERFACE) {
                  if (descriptor instanceof UsbInterfaceDescriptor) {
                      UsbInterfaceDescriptor intrDesc = (UsbInterfaceDescriptor) descriptor;
                      if (intrDesc.getUsbClass() == usbClass) {
                          list.add(descriptor);
                      }
                  } else {
                      Log.w(TAG, "Unrecognized Interface l: " + descriptor.getLength()
                              + " t:0x" + Integer.toHexString(descriptor.getType()));
                  }
              }
          }
          return list;
      }
  
      /**
       * @hide
       */
      public ArrayList<UsbDescriptor> getACInterfaceDescriptors(byte subtype, int subclass) {
          ArrayList<UsbDescriptor> list = new ArrayList<UsbDescriptor>();
          for (UsbDescriptor descriptor : mDescriptors) {
              if (descriptor.getType() == UsbDescriptor.DESCRIPTORTYPE_AUDIO_INTERFACE) {
                  // ensure that this isn't an unrecognized DESCRIPTORTYPE_AUDIO_INTERFACE
                  if (descriptor instanceof UsbACInterface) {
                      UsbACInterface acDescriptor = (UsbACInterface) descriptor;
                      if (acDescriptor.getSubtype() == subtype
                              && acDescriptor.getSubclass() == subclass) {
                          list.add(descriptor);
                      }
                  } else {
                      Log.w(TAG, "Unrecognized Audio Interface l: " + descriptor.getLength()
                              + " t:0x" + Integer.toHexString(descriptor.getType()));
                  }
              }
          }
          return list;
      }
  
      /*
       * Attribute predicates
       */
      /**
       * @hide
       */
      public boolean hasInput() {
          if (DEBUG) {
              Log.d(TAG, "---- hasInput()");
          }
          ArrayList<UsbDescriptor> acDescriptors =
                  getACInterfaceDescriptors(UsbACInterface.ACI_INPUT_TERMINAL,
                  UsbACInterface.AUDIO_AUDIOCONTROL);
          boolean hasInput = false;
          for (UsbDescriptor descriptor : acDescriptors) {
              if (descriptor instanceof UsbACTerminal) {
                  UsbACTerminal inDescr = (UsbACTerminal) descriptor;
                  // Check for input and bi-directional terminal types
                  int type = inDescr.getTerminalType();
                  if (DEBUG) {
                      Log.d(TAG, "  type:0x" + Integer.toHexString(type));
                  }
                  int terminalCategory = type & ~0xFF;
                  if (terminalCategory != UsbTerminalTypes.TERMINAL_USB_UNDEFINED
                          && terminalCategory != UsbTerminalTypes.TERMINAL_OUT_UNDEFINED) {
                      // If not explicitly a USB connection or output, it could be an input.
                      hasInput = true;
                      break;
                  }
              } else {
                  Log.w(TAG, "Undefined Audio Input terminal l: " + descriptor.getLength()
                          + " t:0x" + Integer.toHexString(descriptor.getType()));
              }
          }
  
          if (DEBUG) {
              Log.d(TAG, "hasInput() = " + hasInput);
          }
          return hasInput;
      }
  
      /**
       * @hide
       */
      public boolean hasOutput() {
          if (DEBUG) {
              Log.d(TAG, "---- hasOutput()");
          }
          ArrayList<UsbDescriptor> acDescriptors =
                  getACInterfaceDescriptors(UsbACInterface.ACI_OUTPUT_TERMINAL,
                  UsbACInterface.AUDIO_AUDIOCONTROL);
          boolean hasOutput = false;
          for (UsbDescriptor descriptor : acDescriptors) {
              if (descriptor instanceof UsbACTerminal) {
                  UsbACTerminal outDescr = (UsbACTerminal) descriptor;
                  // Check for output and bi-directional terminal types
                  int type = outDescr.getTerminalType();
                  if (DEBUG) {
                      Log.d(TAG, "  type:0x" + Integer.toHexString(type));
                  }
                  int terminalCategory = type & ~0xFF;
                  if (terminalCategory != UsbTerminalTypes.TERMINAL_USB_UNDEFINED
                          && terminalCategory != UsbTerminalTypes.TERMINAL_IN_UNDEFINED) {
                      // If not explicitly a USB connection or input, it could be an output.
                      hasOutput = true;
                      break;
                  }
              } else {
                  Log.w(TAG, "Undefined Audio Input terminal l: " + descriptor.getLength()
                          + " t:0x" + Integer.toHexString(descriptor.getType()));
              }
          }
          if (DEBUG) {
              Log.d(TAG, "hasOutput() = " + hasOutput);
          }
          return hasOutput;
      }
  
      /**
       * @hide
       */
      public boolean hasMic() {
          boolean hasMic = false;
  
          ArrayList<UsbDescriptor> acDescriptors =
                  getACInterfaceDescriptors(UsbACInterface.ACI_INPUT_TERMINAL,
                  UsbACInterface.AUDIO_AUDIOCONTROL);
          for (UsbDescriptor descriptor : acDescriptors) {
              if (descriptor instanceof UsbACTerminal) {
                  UsbACTerminal inDescr = (UsbACTerminal) descriptor;
                  if (inDescr.getTerminalType() == UsbTerminalTypes.TERMINAL_IN_MIC
                          || inDescr.getTerminalType() == UsbTerminalTypes.TERMINAL_BIDIR_HEADSET
                          || inDescr.getTerminalType() == UsbTerminalTypes.TERMINAL_BIDIR_UNDEFINED
                          || inDescr.getTerminalType() == UsbTerminalTypes.TERMINAL_EXTERN_LINE) {
                      hasMic = true;
                      break;
                  }
              } else {
                  Log.w(TAG, "Undefined Audio Input terminal l: " + descriptor.getLength()
                          + " t:0x" + Integer.toHexString(descriptor.getType()));
              }
          }
          return hasMic;
      }
  
      /**
       * @hide
       */
      public boolean hasSpeaker() {
          boolean hasSpeaker = false;
  
          ArrayList<UsbDescriptor> acDescriptors =
                  getACInterfaceDescriptors(UsbACInterface.ACI_OUTPUT_TERMINAL,
                          UsbACInterface.AUDIO_AUDIOCONTROL);
          for (UsbDescriptor descriptor : acDescriptors) {
              if (descriptor instanceof UsbACTerminal) {
                  UsbACTerminal outDescr = (UsbACTerminal) descriptor;
                  if (outDescr.getTerminalType() == UsbTerminalTypes.TERMINAL_OUT_SPEAKER
                          || outDescr.getTerminalType() == UsbTerminalTypes.TERMINAL_OUT_HEADPHONES
                          || outDescr.getTerminalType() == UsbTerminalTypes.TERMINAL_BIDIR_HEADSET) {
                      hasSpeaker = true;
                      break;
                  }
              } else {
                  Log.w(TAG, "Undefined Audio Output terminal l: " + descriptor.getLength()
                          + " t:0x" + Integer.toHexString(descriptor.getType()));
              }
          }
  
          return hasSpeaker;
      }
  
      /**
       *@ hide
       */
      public boolean hasAudioInterface() {
          ArrayList<UsbDescriptor> descriptors =
                  getInterfaceDescriptorsForClass(UsbDescriptor.CLASSID_AUDIO);
          return !descriptors.isEmpty();
      }
  
      /**
       * @hide
       */
      public boolean hasHIDInterface() {
          ArrayList<UsbDescriptor> descriptors =
                  getInterfaceDescriptorsForClass(UsbDescriptor.CLASSID_HID);
          return !descriptors.isEmpty();
      }
  
      /**
       * @hide
       */
      public boolean hasStorageInterface() {
          ArrayList<UsbDescriptor> descriptors =
                  getInterfaceDescriptorsForClass(UsbDescriptor.CLASSID_STORAGE);
          return !descriptors.isEmpty();
      }
  
      /**
       * @hide
       */
      public boolean hasMIDIInterface() {
          ArrayList<UsbDescriptor> descriptors =
                  getInterfaceDescriptorsForClass(UsbDescriptor.CLASSID_AUDIO);
          for (UsbDescriptor descriptor : descriptors) {
              // enusure that this isn't an unrecognized interface descriptor
              if (descriptor instanceof UsbInterfaceDescriptor) {
                  UsbInterfaceDescriptor interfaceDescr = (UsbInterfaceDescriptor) descriptor;
                  if (interfaceDescr.getUsbSubclass() == UsbDescriptor.AUDIO_MIDISTREAMING) {
                      return true;
                  }
              } else {
                  Log.w(TAG, "Undefined Audio Class Interface l: " + descriptor.getLength()
                          + " t:0x" + Integer.toHexString(descriptor.getType()));
              }
          }
          return false;
      }
  
      /**
       * @hide
       */
      public float getInputHeadsetProbability() {
          if (hasMIDIInterface()) {
              return 0.0f;
          }
  
          float probability = 0.0f;
  
          // Look for a microphone
          boolean hasMic = hasMic();
  
          // Look for a "speaker"
          boolean hasSpeaker = hasSpeaker();
  
          if (hasMic && hasSpeaker) {
              probability += 0.75f;
          }
  
          if (hasMic && hasHIDInterface()) {
              probability += 0.25f;
          }
  
          return probability;
      }
  
      /**
       * getInputHeadsetProbability() reports a probability of a USB Input peripheral being a
       * headset. The probability range is between 0.0f (definitely NOT a headset) and
       * 1.0f (definitely IS a headset). A probability of 0.75f seems sufficient
       * to count on the peripheral being a headset.
       */
      public boolean isInputHeadset() {
          return getInputHeadsetProbability() >= IN_HEADSET_TRIGGER;
      }
  
      /**
       * @hide
       */
      public float getOutputHeadsetProbability() {
          if (hasMIDIInterface()) {
              return 0.0f;
          }
  
          float probability = 0.0f;
          ArrayList<UsbDescriptor> acDescriptors;
  
          // Look for a "speaker"
          boolean hasSpeaker = false;
          acDescriptors =
                  getACInterfaceDescriptors(UsbACInterface.ACI_OUTPUT_TERMINAL,
                          UsbACInterface.AUDIO_AUDIOCONTROL);
          for (UsbDescriptor descriptor : acDescriptors) {
              if (descriptor instanceof UsbACTerminal) {
                  UsbACTerminal outDescr = (UsbACTerminal) descriptor;
                  if (outDescr.getTerminalType() == UsbTerminalTypes.TERMINAL_OUT_SPEAKER
                          || outDescr.getTerminalType()
                              == UsbTerminalTypes.TERMINAL_OUT_HEADPHONES
                          || outDescr.getTerminalType() == UsbTerminalTypes.TERMINAL_BIDIR_HEADSET) {
                      hasSpeaker = true;
                      break;
                  }
              } else {
                  Log.w(TAG, "Undefined Audio Output terminal l: " + descriptor.getLength()
                          + " t:0x" + Integer.toHexString(descriptor.getType()));
              }
          }
  
          if (hasSpeaker) {
              probability += 0.75f;
          }
  
          if (hasSpeaker && hasHIDInterface()) {
              probability += 0.25f;
          }
  
          return probability;
      }
  
      /**
       * getOutputHeadsetProbability() reports a probability of a USB Output peripheral being a
       * headset. The probability range is between 0.0f (definitely NOT a headset) and
       * 1.0f (definitely IS a headset). A probability of 0.75f seems sufficient
       * to count on the peripheral being a headset.
       */
      public boolean isOutputHeadset() {
          return getOutputHeadsetProbability() >= OUT_HEADSET_TRIGGER;
      }
  
  }
  