Add support for USB audio docks.

Associate Android device type DEVICE_OUT_DGTL_DOCK_HEADSET with a UAC
peripheral with terminal type 0x602 to allow specific audio policy for
docking stations with speaker and USB connectivity.

Bug: 197584191
Test: make
Change-Id: I090e86b2e3e7666f3978ef904a922b040bcb44e4
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index 0961fcb3..2dd6bf5 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -1360,6 +1360,9 @@
             case AudioSystem.DEVICE_OUT_USB_HEADSET:
                 connType = AudioRoutesInfo.MAIN_USB;
                 break;
+            case AudioSystem.DEVICE_OUT_DGTL_DOCK_HEADSET:
+                connType = AudioRoutesInfo.MAIN_DOCK_SPEAKERS;
+                break;
         }
 
         synchronized (mCurAudioRoutes) {
diff --git a/services/usb/java/com/android/server/usb/UsbAlsaDevice.java b/services/usb/java/com/android/server/usb/UsbAlsaDevice.java
index 9d4db00..85b1de5 100644
--- a/services/usb/java/com/android/server/usb/UsbAlsaDevice.java
+++ b/services/usb/java/com/android/server/usb/UsbAlsaDevice.java
@@ -41,6 +41,7 @@
 
     private final boolean mIsInputHeadset;
     private final boolean mIsOutputHeadset;
+    private final boolean mIsDock;
 
     private boolean mSelected = false;
     private int mOutputState;
@@ -53,7 +54,7 @@
 
     public UsbAlsaDevice(IAudioService audioService, int card, int device, String deviceAddress,
             boolean hasOutput, boolean hasInput,
-            boolean isInputHeadset, boolean isOutputHeadset) {
+            boolean isInputHeadset, boolean isOutputHeadset, boolean isDock) {
         mAudioService = audioService;
         mCardNum = card;
         mDeviceNum = device;
@@ -62,31 +63,32 @@
         mHasInput = hasInput;
         mIsInputHeadset = isInputHeadset;
         mIsOutputHeadset = isOutputHeadset;
+        mIsDock = isDock;
     }
 
     /**
-     * @returns the ALSA card number associated with this peripheral.
+     * @return the ALSA card number associated with this peripheral.
      */
     public int getCardNum() {
         return mCardNum;
     }
 
     /**
-     * @returns the ALSA device number associated with this peripheral.
+     * @return the ALSA device number associated with this peripheral.
      */
     public int getDeviceNum() {
         return mDeviceNum;
     }
 
     /**
-     * @returns the USB device device address associated with this peripheral.
+     * @return the USB device device address associated with this peripheral.
      */
     public String getDeviceAddress() {
         return mDeviceAddress;
     }
 
     /**
-     * @returns the ALSA card/device address string.
+     * @return the ALSA card/device address string.
      */
     public String getAlsaCardDeviceString() {
         if (mCardNum < 0 || mDeviceNum < 0) {
@@ -98,35 +100,42 @@
     }
 
     /**
-     * @returns true if the device supports output.
+     * @return true if the device supports output.
      */
     public boolean hasOutput() {
         return mHasOutput;
     }
 
     /**
-     * @returns true if the device supports input (recording).
+     * @return true if the device supports input (recording).
      */
     public boolean hasInput() {
         return mHasInput;
     }
 
     /**
-     * @returns true if the device is a headset for purposes of input.
+     * @return true if the device is a headset for purposes of input.
      */
     public boolean isInputHeadset() {
         return mIsInputHeadset;
     }
 
     /**
-     * @returns true if the device is a headset for purposes of output.
+     * @return true if the device is a headset for purposes of output.
      */
     public boolean isOutputHeadset() {
         return mIsOutputHeadset;
     }
 
     /**
-     * @returns true if input jack is detected or jack detection is not supported.
+     * @return true if the device is a USB dock.
+     */
+    public boolean isDock() {
+        return mIsDock;
+    }
+
+    /**
+     * @return true if input jack is detected or jack detection is not supported.
      */
     private synchronized boolean isInputJackConnected() {
         if (mJackDetector == null) {
@@ -136,7 +145,7 @@
     }
 
     /**
-     * @returns true if input jack is detected or jack detection is not supported.
+     * @return true if input jack is detected or jack detection is not supported.
      */
     private synchronized boolean isOutputJackConnected() {
         if (mJackDetector == null) {
@@ -190,9 +199,10 @@
         try {
             // Output Device
             if (mHasOutput) {
-                int device = mIsOutputHeadset
-                        ? AudioSystem.DEVICE_OUT_USB_HEADSET
-                        : AudioSystem.DEVICE_OUT_USB_DEVICE;
+                int device = mIsDock ? AudioSystem.DEVICE_OUT_DGTL_DOCK_HEADSET
+                        : (mIsOutputHeadset
+                            ? AudioSystem.DEVICE_OUT_USB_HEADSET
+                            : AudioSystem.DEVICE_OUT_USB_DEVICE);
                 if (DEBUG) {
                     Slog.d(TAG, "pre-call device:0x" + Integer.toHexString(device)
                             + " addr:" + alsaCardDeviceString
@@ -231,7 +241,7 @@
 
     /**
      * @Override
-     * @returns a string representation of the object.
+     * @return a string representation of the object.
      */
     public synchronized String toString() {
         return "UsbAlsaDevice: [card: " + mCardNum
@@ -273,7 +283,7 @@
 
     /**
      * @Override
-     * @returns true if the objects are equivalent.
+     * @return true if the objects are equivalent.
      */
     public boolean equals(Object obj) {
         if (!(obj instanceof UsbAlsaDevice)) {
@@ -285,12 +295,13 @@
                 && mHasOutput == other.mHasOutput
                 && mHasInput == other.mHasInput
                 && mIsInputHeadset == other.mIsInputHeadset
-                && mIsOutputHeadset == other.mIsOutputHeadset);
+                && mIsOutputHeadset == other.mIsOutputHeadset
+                && mIsDock == other.mIsDock);
     }
 
     /**
      * @Override
-     * @returns a hash code generated from the object contents.
+     * @return a hash code generated from the object contents.
      */
     public int hashCode() {
         final int prime = 31;
@@ -301,6 +312,7 @@
         result = prime * result + (mHasInput ? 0 : 1);
         result = prime * result + (mIsInputHeadset ? 0 : 1);
         result = prime * result + (mIsOutputHeadset ? 0 : 1);
+        result = prime * result + (mIsDock ? 0 : 1);
 
         return result;
     }
diff --git a/services/usb/java/com/android/server/usb/UsbAlsaManager.java b/services/usb/java/com/android/server/usb/UsbAlsaManager.java
index 0aa62c5..22b110f 100644
--- a/services/usb/java/com/android/server/usb/UsbAlsaManager.java
+++ b/services/usb/java/com/android/server/usb/UsbAlsaManager.java
@@ -237,6 +237,7 @@
         if (hasInput || hasOutput) {
             boolean isInputHeadset = parser.isInputHeadset();
             boolean isOutputHeadset = parser.isOutputHeadset();
+            boolean isDock = parser.isDock();
 
             if (mAudioService == null) {
                 Slog.e(TAG, "no AudioService");
@@ -246,7 +247,7 @@
             UsbAlsaDevice alsaDevice =
                     new UsbAlsaDevice(mAudioService, cardRec.getCardNum(), 0 /*device*/,
                                       deviceAddress, hasOutput, hasInput,
-                                      isInputHeadset, isOutputHeadset);
+                                      isInputHeadset, isOutputHeadset, isDock);
             if (alsaDevice != null) {
                 alsaDevice.setDeviceNameAndDescription(
                           cardRec.getCardName(), cardRec.getCardDescription());
diff --git a/services/usb/java/com/android/server/usb/UsbHostManager.java b/services/usb/java/com/android/server/usb/UsbHostManager.java
index f33001c..5befe52 100644
--- a/services/usb/java/com/android/server/usb/UsbHostManager.java
+++ b/services/usb/java/com/android/server/usb/UsbHostManager.java
@@ -155,7 +155,7 @@
                 pw.println("manfacturer:0x" + Integer.toHexString(deviceDescriptor.getVendorID())
                         + " product:" + Integer.toHexString(deviceDescriptor.getProductID()));
                 pw.println("isHeadset[in: " + parser.isInputHeadset()
-                        + " , out: " + parser.isOutputHeadset() + "]");
+                        + " , out: " + parser.isOutputHeadset() + "], isDock: " + parser.isDock());
             } else {
                 pw.println(formatTime() + " Disconnect " + mDeviceAddress);
             }
@@ -169,9 +169,8 @@
                 UsbDescriptorsTree descriptorTree = new UsbDescriptorsTree();
                 descriptorTree.parse(parser);
                 descriptorTree.report(new TextReportCanvas(parser, stringBuilder));
-
                 stringBuilder.append("isHeadset[in: " + parser.isInputHeadset()
-                        + " , out: " + parser.isOutputHeadset() + "]");
+                        + " , out: " + parser.isOutputHeadset() + "], isDock: " + parser.isDock());
                 pw.println(stringBuilder.toString());
             } else {
                 pw.println(formatTime() + " Disconnect " + mDeviceAddress);
@@ -188,9 +187,8 @@
                     descriptor.report(canvas);
                 }
                 pw.println(stringBuilder.toString());
-
                 pw.println("isHeadset[in: " + parser.isInputHeadset()
-                        + " , out: " + parser.isOutputHeadset() + "]");
+                        + " , out: " + parser.isOutputHeadset() + "], isDock: " + parser.isDock());
             } else {
                 pw.println(formatTime() + " Disconnect " + mDeviceAddress);
             }
diff --git a/services/usb/java/com/android/server/usb/descriptors/UsbDescriptorParser.java b/services/usb/java/com/android/server/usb/descriptors/UsbDescriptorParser.java
index 7250a07..38ce6cb 100644
--- a/services/usb/java/com/android/server/usb/descriptors/UsbDescriptorParser.java
+++ b/services/usb/java/com/android/server/usb/descriptors/UsbDescriptorParser.java
@@ -785,4 +785,35 @@
         return getOutputHeadsetProbability() >= OUT_HEADSET_TRIGGER;
     }
 
+    /**
+     * isDock() indicates if the connected USB output peripheral is a docking station with
+     * audio output.
+     * A valid audio dock must declare only one audio output control terminal of type
+     * TERMINAL_EXTERN_DIGITAL.
+     */
+    public boolean isDock() {
+        if (hasMIDIInterface() || hasHIDInterface()) {
+            return false;
+        }
+
+        ArrayList<UsbDescriptor> acDescriptors =
+                getACInterfaceDescriptors(UsbACInterface.ACI_OUTPUT_TERMINAL,
+                        UsbACInterface.AUDIO_AUDIOCONTROL);
+
+        if (acDescriptors.size() != 1) {
+            return false;
+        }
+
+        if (acDescriptors.get(0) instanceof UsbACTerminal) {
+            UsbACTerminal outDescr = (UsbACTerminal) acDescriptors.get(0);
+            if (outDescr.getTerminalType() == UsbTerminalTypes.TERMINAL_EXTERN_DIGITAL) {
+                return true;
+            }
+        } else {
+            Log.w(TAG, "Undefined Audio Output terminal l: " + acDescriptors.get(0).getLength()
+                    + " t:0x" + Integer.toHexString(acDescriptors.get(0).getType()));
+        }
+        return false;
+    }
+
 }