Listen to USB device de/attach event. (#1961)

* Listen to USB device de/attach event.

With listening to USB device de/attach event, disconnect
test can well sync on the peripherals plug state.
diff --git a/apps/OboeTester/app/build.gradle b/apps/OboeTester/app/build.gradle
index d930cd8..5f90160 100644
--- a/apps/OboeTester/app/build.gradle
+++ b/apps/OboeTester/app/build.gradle
@@ -6,8 +6,8 @@
         applicationId = "com.mobileer.oboetester"
         minSdkVersion 23
         targetSdkVersion 34
-        versionCode 80
-        versionName "2.5.9"
+        versionCode 81
+        versionName "2.5.10"
         testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
         externalNativeBuild {
             cmake {
diff --git a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestDisconnectActivity.java b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestDisconnectActivity.java
index a314d1a..9e4d880 100644
--- a/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestDisconnectActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/mobileer/oboetester/TestDisconnectActivity.java
@@ -20,6 +20,11 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.hardware.usb.UsbConstants;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbEndpoint;
+import android.hardware.usb.UsbInterface;
+import android.hardware.usb.UsbManager;
 import android.os.Bundle;
 import android.util.Log;
 import android.view.View;
@@ -49,6 +54,7 @@
     private volatile boolean mTestFailed;
     private volatile boolean mSkipTest;
     private volatile int mPlugCount;
+    private volatile int mUsbDeviceAttachedCount;
     private volatile int mPlugState;
     private volatile int mPlugMicrophone;
     private BroadcastReceiver mPluginReceiver = new PluginBroadcastReceiver();
@@ -64,13 +70,34 @@
     public class PluginBroadcastReceiver extends BroadcastReceiver {
         @Override
         public void onReceive(Context context, Intent intent) {
-            mPlugMicrophone = intent.getIntExtra("microphone", -1);
-            mPlugState = intent.getIntExtra("state", -1);
-            mPlugCount++;
+            switch (intent.getAction()) {
+                case Intent.ACTION_HEADSET_PLUG: {
+                    mPlugMicrophone = intent.getIntExtra("microphone", -1);
+                    mPlugState = intent.getIntExtra("state", -1);
+                    mPlugCount++;
+                } break;
+                case UsbManager.ACTION_USB_DEVICE_ATTACHED:
+                case UsbManager.ACTION_USB_DEVICE_DETACHED: {
+                    UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
+                    final boolean hasAudioPlayback =
+                            containsAudioStreamingInterface(device, UsbConstants.USB_DIR_OUT);
+                    final boolean hasAudioCapture =
+                            containsAudioStreamingInterface(device, UsbConstants.USB_DIR_IN);
+                    if (hasAudioPlayback || hasAudioCapture) {
+                        mPlugState =
+                                intent.getAction() == UsbManager.ACTION_USB_DEVICE_ATTACHED ? 1 : 0;
+                        mUsbDeviceAttachedCount++;
+                        mPlugMicrophone = hasAudioCapture ? 1 : 0;
+                    }
+                } break;
+                default:
+                    break;
+            }
             runOnUiThread(new Runnable() {
                 @Override
                 public void run() {
                     String message = "HEADSET_PLUG #" + mPlugCount
+                            + ", USB_DEVICE_DE/ATTACHED #" + mUsbDeviceAttachedCount
                             + ", mic = " + mPlugMicrophone
                             + ", state = " + mPlugState;
                     mPlugTextView.setText(message);
@@ -78,6 +105,33 @@
                 }
             });
         }
+
+        private static final int AUDIO_STREAMING_SUB_CLASS = 2;
+
+        /**
+         * Figure out if an UsbDevice contains audio input/output streaming interface or not.
+         *
+         * @param device the given UsbDevice
+         * @param direction the direction of the audio streaming interface
+         * @return true if the UsbDevice contains the audio input/output streaming interface.
+         */
+        private boolean containsAudioStreamingInterface(UsbDevice device, int direction) {
+            final int interfaceCount = device.getInterfaceCount();
+            for (int i = 0; i < interfaceCount; ++i) {
+                UsbInterface usbInterface = device.getInterface(i);
+                if (usbInterface.getInterfaceClass() != UsbConstants.USB_CLASS_AUDIO
+                        && usbInterface.getInterfaceSubclass() != AUDIO_STREAMING_SUB_CLASS) {
+                    continue;
+                }
+                final int endpointCount = usbInterface.getEndpointCount();
+                for (int j = 0; j < endpointCount; ++j) {
+                    if (usbInterface.getEndpoint(j).getDirection() == direction) {
+                        return true;
+                    }
+                }
+            }
+            return false;
+        }
     }
 
     @Override
@@ -152,6 +206,8 @@
     public void onResume() {
         super.onResume();
         IntentFilter filter = new IntentFilter(Intent.ACTION_HEADSET_PLUG);
+        filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
+        filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
         this.registerReceiver(mPluginReceiver, filter);
     }
 
diff --git a/apps/OboeTester/app/src/main/res/layout/activity_test_disconnect.xml b/apps/OboeTester/app/src/main/res/layout/activity_test_disconnect.xml
index c09bda0..345e820 100644
--- a/apps/OboeTester/app/src/main/res/layout/activity_test_disconnect.xml
+++ b/apps/OboeTester/app/src/main/res/layout/activity_test_disconnect.xml
@@ -24,7 +24,7 @@
         android:id="@+id/text_plug_events"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:lines="1"
+        android:lines="2"
         android:text="plug #"
         android:textSize="18sp"
         android:textStyle="bold"