Merge "Refactor CTSVerifier Loopback tests to use the common, Oboe-based, measurement." into rvc-dev
diff --git a/apps/CtsVerifier/jni/audio_loopback/NativeAudioAnalyzer.cpp b/apps/CtsVerifier/jni/audio_loopback/NativeAudioAnalyzer.cpp
index d8d6946..8a1c63f 100644
--- a/apps/CtsVerifier/jni/audio_loopback/NativeAudioAnalyzer.cpp
+++ b/apps/CtsVerifier/jni/audio_loopback/NativeAudioAnalyzer.cpp
@@ -202,6 +202,10 @@
     return mPulseLatencyAnalyzer.getMeasuredConfidence();
 }
 
+int NativeAudioAnalyzer::getSampleRate() {
+    return mOutputSampleRate;
+}
+
 aaudio_result_t NativeAudioAnalyzer::openAudio() {
     AAudioStreamBuilder *builder = nullptr;
 
@@ -234,13 +238,13 @@
     int32_t outputFramesPerBurst = AAudioStream_getFramesPerBurst(mOutputStream);
     (void) AAudioStream_setBufferSizeInFrames(mOutputStream, outputFramesPerBurst * kDefaultOutputSizeBursts);
 
-    int32_t outputSampleRate = AAudioStream_getSampleRate(mOutputStream);
+    mOutputSampleRate = AAudioStream_getSampleRate(mOutputStream);
     mActualOutputChannelCount = AAudioStream_getChannelCount(mOutputStream);
 
     // Create the INPUT stream -----------------------
     AAudioStreamBuilder_setDirection(builder, AAUDIO_DIRECTION_INPUT);
     AAudioStreamBuilder_setFormat(builder, AAUDIO_FORMAT_UNSPECIFIED);
-    AAudioStreamBuilder_setSampleRate(builder, outputSampleRate); // must match
+    AAudioStreamBuilder_setSampleRate(builder, mOutputSampleRate); // must match
     AAudioStreamBuilder_setChannelCount(builder, 1); // mono
     AAudioStreamBuilder_setDataCallback(builder, nullptr, nullptr);
     AAudioStreamBuilder_setErrorCallback(builder, nullptr, nullptr);
diff --git a/apps/CtsVerifier/jni/audio_loopback/NativeAudioAnalyzer.h b/apps/CtsVerifier/jni/audio_loopback/NativeAudioAnalyzer.h
index 0d9c64b..3367226 100644
--- a/apps/CtsVerifier/jni/audio_loopback/NativeAudioAnalyzer.h
+++ b/apps/CtsVerifier/jni/audio_loopback/NativeAudioAnalyzer.h
@@ -80,6 +80,11 @@
     double getLatencyMillis();
 
     /**
+     * @return the sample rate (in Hz) used for the measurement signal
+     */
+    int getSampleRate();
+
+    /**
      * The confidence is based on a normalized correlation.
      * It ranges from 0.0 to 1.0. Higher is better.
      *
@@ -96,6 +101,7 @@
     aaudio_format_t    mActualInputFormat = AAUDIO_FORMAT_INVALID;
     int16_t           *mInputShortData = nullptr;
     float             *mInputFloatData = nullptr;
+    int32_t            mOutputSampleRate = 0;
 
     aaudio_result_t    mInputError = AAUDIO_OK;
     aaudio_result_t    mOutputError = AAUDIO_OK;
diff --git a/apps/CtsVerifier/jni/audio_loopback/jni-bridge.cpp b/apps/CtsVerifier/jni/audio_loopback/jni-bridge.cpp
index a851cbe..58908bd 100644
--- a/apps/CtsVerifier/jni/audio_loopback/jni-bridge.cpp
+++ b/apps/CtsVerifier/jni/audio_loopback/jni-bridge.cpp
@@ -113,4 +113,13 @@
     return 0.0;
 }
 
+JNIEXPORT jint JNICALL Java_com_android_cts_verifier_audio_NativeAnalyzerThread_getSampleRate
+  (JNIEnv *env __unused, jobject obj __unused, jlong pAnalyzer) {
+    NativeAudioAnalyzer * analyzer = (NativeAudioAnalyzer *) pAnalyzer;
+    if (analyzer != nullptr) {
+        return analyzer->getSampleRate();
+    }
+    return 0;
+}
+
 }
diff --git a/apps/CtsVerifier/res/layout/audio_loopback_device_layout.xml b/apps/CtsVerifier/res/layout/audio_loopback_device_layout.xml
new file mode 100644
index 0000000..4d0ba8d
--- /dev/null
+++ b/apps/CtsVerifier/res/layout/audio_loopback_device_layout.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical">
+
+    <TextView
+        android:text="@string/audioLoopbackInputLbl"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textSize="18sp"/>
+
+    <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:paddingLeft="10dp"
+        android:paddingRight="10dp"
+        android:id="@+id/audioLoopbackInputLbl"
+        android:textSize="18sp"/>
+
+    <TextView
+        android:text="@string/audioLoopbackOutputLbl"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textSize="18sp"/>
+
+    <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:paddingLeft="10dp"
+        android:paddingRight="10dp"
+        android:id="@+id/audioLoopbackOutputLbl"
+        android:textSize="18sp"/>
+
+
+</LinearLayout>
\ No newline at end of file
diff --git a/apps/CtsVerifier/res/layout/audio_loopback_footer_layout.xml b/apps/CtsVerifier/res/layout/audio_loopback_footer_layout.xml
new file mode 100644
index 0000000..f59afeb
--- /dev/null
+++ b/apps/CtsVerifier/res/layout/audio_loopback_footer_layout.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical">
+
+    <ProgressBar
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:id="@+id/audio_loopback_progress_bar" />
+
+    <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/audio_loopback_results_text"
+        android:id="@+id/audio_loopback_results_text" />
+</LinearLayout>
diff --git a/apps/CtsVerifier/res/layout/audio_loopback_layout.xml b/apps/CtsVerifier/res/layout/audio_loopback_header_layout.xml
similarity index 96%
rename from apps/CtsVerifier/res/layout/audio_loopback_layout.xml
rename to apps/CtsVerifier/res/layout/audio_loopback_header_layout.xml
index 40732d0..a738826 100644
--- a/apps/CtsVerifier/res/layout/audio_loopback_layout.xml
+++ b/apps/CtsVerifier/res/layout/audio_loopback_header_layout.xml
@@ -1,3 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
 <LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
diff --git a/apps/CtsVerifier/res/layout/audio_loopback_latency_activity.xml b/apps/CtsVerifier/res/layout/audio_loopback_latency_activity.xml
index a9ad8a1..c8c7a69 100644
--- a/apps/CtsVerifier/res/layout/audio_loopback_latency_activity.xml
+++ b/apps/CtsVerifier/res/layout/audio_loopback_latency_activity.xml
@@ -30,7 +30,7 @@
             android:layout_height="wrap_content"
             android:orientation="vertical">
 
-            <include layout="@layout/audio_loopback_layout"/>
+            <include layout="@layout/audio_loopback_header_layout"/>
 
             <LinearLayout
                 android:layout_width="wrap_content"
@@ -38,53 +38,15 @@
                 android:orientation="vertical"
                 android:id="@+id/audio_loopback_headset_port">
 
-                <TextView
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:scrollbars="vertical"
-                    android:gravity="bottom"
-                    android:id="@+id/info_text"
-                    android:text="@string/audio_loopback_instructions" />
+                <include layout="@layout/audio_loopback_volume_layout" />
 
-                <Button
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:id="@+id/audio_loopback_plug_ready_btn"
-                    android:text="@string/audio_loopback_plug_ready_btn"
-                    android:nextFocusForward="@+id/audio_loopback_level_seekbar"
-                    android:nextFocusUp="@+id/audio_general_headset_yes"
-                    android:nextFocusDown="@+id/audio_loopback_level_seekbar"
-                    android:nextFocusLeft="@+id/audio_general_headset_yes"
-                    android:nextFocusRight="@+id/audio_loopback_level_seekbar" />
+                <include layout="@layout/audio_loopback_device_layout" />
 
                 <LinearLayout
                     android:orientation="vertical"
                     android:layout_width="match_parent"
                     android:layout_height="match_parent"
                     android:id="@+id/audio_loopback_layout">
-
-                    <TextView
-                        android:layout_width="wrap_content"
-                        android:layout_height="wrap_content"
-                        android:text="@string/audio_loopback_instructions2"
-                        android:id="@+id/audio_loopback_instructions2" />
-
-                    <SeekBar
-                        android:layout_width="match_parent"
-                        android:layout_height="wrap_content"
-                        android:id="@+id/audio_loopback_level_seekbar"
-                        android:nextFocusForward="@+id/audio_loopback_test_btn"
-                        android:nextFocusUp="@+id/audio_loopback_plug_ready_btn"
-                        android:nextFocusDown="@+id/audio_loopback_test_btn"
-                        android:nextFocusLeft="@+id/audio_loopback_plug_ready_btn"
-                        android:nextFocusRight="@+id/audio_loopback_test_btn" />
-
-                    <TextView
-                        android:layout_width="wrap_content"
-                        android:layout_height="wrap_content"
-                        android:text="@string/audio_loopback_level_text"
-                        android:id="@+id/audio_loopback_level_text" />
-
                     <Button
                         android:layout_width="wrap_content"
                         android:layout_height="wrap_content"
@@ -96,18 +58,10 @@
                         android:nextFocusLeft="@+id/audio_loopback_level_seekbar"
                         android:nextFocusRight="@+id/pass_button" />
 
-                    <ProgressBar
-                        android:layout_width="wrap_content"
-                        android:layout_height="wrap_content"
-                        android:id="@+id/audio_loopback_progress_bar" />
-
-                    <TextView
-                        android:layout_width="wrap_content"
-                        android:layout_height="wrap_content"
-                        android:text="@string/audio_loopback_results_text"
-                        android:id="@+id/audio_loopback_results_text" />
                 </LinearLayout>
 
+                <include layout="@layout/audio_loopback_footer_layout"/>
+
             </LinearLayout>
             <include layout="@layout/pass_fail_buttons" />
         </LinearLayout>
diff --git a/apps/CtsVerifier/res/layout/audio_loopback_volume_layout.xml b/apps/CtsVerifier/res/layout/audio_loopback_volume_layout.xml
new file mode 100644
index 0000000..b85985e
--- /dev/null
+++ b/apps/CtsVerifier/res/layout/audio_loopback_volume_layout.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:id="@+id/audio_loopback_layout">
+
+    <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/audio_loopback_instructions2"
+        android:id="@+id/audio_loopback_instructions2" />
+
+    <SeekBar
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:id="@+id/audio_loopback_level_seekbar"
+        android:nextFocusForward="@+id/audio_loopback_test_btn"
+        android:nextFocusUp="@+id/audio_loopback_plug_ready_btn"
+        android:nextFocusDown="@+id/audio_loopback_test_btn"
+        android:nextFocusLeft="@+id/audio_loopback_plug_ready_btn"
+        android:nextFocusRight="@+id/audio_loopback_test_btn" />
+
+    <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/audio_loopback_level_text"
+        android:id="@+id/audio_loopback_level_text" />
+
+</LinearLayout>
diff --git a/apps/CtsVerifier/res/layout/pro_audio.xml b/apps/CtsVerifier/res/layout/pro_audio.xml
index b07fbb9..e61ba01 100644
--- a/apps/CtsVerifier/res/layout/pro_audio.xml
+++ b/apps/CtsVerifier/res/layout/pro_audio.xml
@@ -9,7 +9,7 @@
     android:layout_width="match_parent"
     android:layout_height="wrap_content">
 
-    <include layout="@layout/audio_loopback_layout"/>
+    <include layout="@layout/audio_loopback_header_layout"/>
 
     <LinearLayout android:orientation="horizontal"
         android:layout_width="match_parent"
@@ -125,33 +125,9 @@
             android:textSize="18sp"/>
     </LinearLayout>
 
-    <TextView
-        android:text="@string/proAudioInputLbl"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:textSize="18sp"/>
+    <include layout="@layout/audio_loopback_device_layout" />
 
-    <TextView
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:paddingLeft="10dp"
-        android:paddingRight="10dp"
-        android:id="@+id/proAudioInputLbl"
-        android:textSize="18sp"/>
-
-    <TextView
-        android:text="@string/proAudioOutputLbl"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:textSize="18sp"/>
-
-    <TextView
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:paddingLeft="10dp"
-        android:paddingRight="10dp"
-        android:id="@+id/proAudioOutputLbl"
-        android:textSize="18sp"/>
+    <include layout="@layout/audio_loopback_volume_layout" />
 
     <Button
         android:text="@string/audio_proaudio_roundtrip"
@@ -159,41 +135,7 @@
         android:layout_height="wrap_content"
         android:id="@+id/proAudio_runRoundtripBtn"/>
 
-    <LinearLayout android:orientation="horizontal"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content">
-        <TextView
-            android:text="@string/proAudioRoundTripLbl"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:textSize="18sp"/>
-
-        <TextView
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:paddingLeft="10dp"
-            android:paddingRight="10dp"
-            android:id="@+id/proAudioRoundTripLbl"
-            android:textSize="18sp"/>
-    </LinearLayout>
-
-    <LinearLayout android:orientation="horizontal"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content">
-        <TextView
-            android:text="@string/proAudioConfidenceLbl"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:textSize="18sp"/>
-
-        <TextView
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:paddingLeft="10dp"
-            android:paddingRight="10dp"
-            android:id="@+id/proAudioConfidenceLbl"
-            android:textSize="18sp"/>
-    </LinearLayout>
+    <include layout="@layout/audio_loopback_footer_layout"/>
 
     <include layout="@layout/pass_fail_buttons"/>
 </LinearLayout>
diff --git a/apps/CtsVerifier/res/values/strings.xml b/apps/CtsVerifier/res/values/strings.xml
index 54e89b0c..b1e0388 100755
--- a/apps/CtsVerifier/res/values/strings.xml
+++ b/apps/CtsVerifier/res/values/strings.xml
@@ -4830,10 +4830,8 @@
 
     <string name="proAudioHasProAudiolbl">Has Pro Audio</string>
     <string name="proAudioHasLLAlbl">Has Low-Latency Audio</string>
-    <string name="proAudioInputLbl">Audio Input:</string>
-    <string name="proAudioOutputLbl">Audio Output:</string>
-    <string name="proAudioRoundTripLbl">Round Trip Latency:</string>
-    <string name="proAudioConfidenceLbl">Confidence:</string>
+    <string name="audioLoopbackInputLbl">Audio Input:</string>
+    <string name="audioLoopbackOutputLbl">Audio Output:</string>
 
     <string name="proAudioMidiHasMIDILbl">Has MIDI Support</string>
     <string name="proAudioMIDIInputLbl">MIDI Input:</string>
@@ -4843,8 +4841,6 @@
     <string name="proAudioHDMISupportLbl">HDMI Support:</string>
     <string name="proAudioHasHDMICheckBox">Has HDMI Support</string>
 
-    <string name="audio_proaudio_loopbackbtn">Start Loopback</string>
-    <string name="audio_proaudio_loopbackInfoBtn">Loopback Instructions</string>
     <string name="audio_proaudio_roundtrip">Round-Trip Test</string>
     <string name="audio_proaudio_NA">N/A</string>
     <string name="audio_proaudio_pending">pending...</string>
@@ -5341,11 +5337,12 @@
        (not a headset) and that peripheral\'s audio outputs are connected to the peripherals\'s
        audio inputs. Alternatively, for devices with an analog audio jack or USB-c Digital
        to Analog dongle, a <a href="https://source.android.com/devices/audio/latency/loopback">Loopback Plug</a>
-       can be used. Also, any if there is an input level
+       can be used. Also, if there is an input level
        control on the peripheral, it must be set to a non-zero value. When the test has
        verified support for a valid audio peripheral, press the \"Round-Trip Test\" button
-       to complete the test. Note that it may be necessary to run the latency test more than
-       once to get a sufficient confidence value.
+       to complete the test.
+       The latency measurement is generally accurate even when the volume is low. But you may
+       need to increase the volume to bring the confidence number above the threshold.
     </string>
     <string name="proaudio_hdmi_infotitle">HDMI Support</string>
     <string name="proaudio_hdmi_message">Please connect an HDMI peripheral to validate
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioLoopbackBaseActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioLoopbackBaseActivity.java
index 671b545..7f96464 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioLoopbackBaseActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioLoopbackBaseActivity.java
@@ -18,18 +18,17 @@
 
 import android.app.AlertDialog;
 
-import com.android.compatibility.common.util.ReportLog;
-import com.android.compatibility.common.util.ResultType;
-import com.android.compatibility.common.util.ResultUnit;
-import com.android.cts.verifier.R;
-import com.android.cts.verifier.PassFailButtons;
-
 import android.content.Context;
 
+import android.media.AudioDeviceCallback;
+import android.media.AudioDeviceInfo;
 import android.media.AudioManager;
 import android.media.AudioTrack;
+import android.media.MediaRecorder;
 
 import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
 
 import android.util.Log;
 
@@ -38,6 +37,16 @@
 import android.view.ViewGroup;
 
 import android.widget.Button;
+import android.widget.TextView;
+import android.widget.ProgressBar;
+import android.widget.SeekBar;
+
+import com.android.compatibility.common.util.ReportLog;
+import com.android.compatibility.common.util.ResultType;
+import com.android.compatibility.common.util.ResultUnit;
+
+import com.android.cts.verifier.PassFailButtons;
+import com.android.cts.verifier.R;
 
 /**
  * Base class for testing activitiees that require audio loopback hardware..
@@ -45,13 +54,45 @@
 public class AudioLoopbackBaseActivity extends PassFailButtons.Activity {
     private static final String TAG = "AudioLoopbackActivity";
 
+    protected AudioManager mAudioManager;
+
+    // UI
     OnBtnClickListener mBtnClickListener = new OnBtnClickListener();
 
     Button mLoopbackPortYesBtn;
     Button mLoopbackPortNoBtn;
 
+    TextView mInputDeviceTxt;
+    TextView mOutputDeviceTxt;
+
+    TextView mAudioLevelText;
+    SeekBar mAudioLevelSeekbar;
+
+    TextView mResultText;
+    ProgressBar mProgressBar;
+    int mMaxLevel;
+
+    // Peripheral(s)
+    boolean mIsPeripheralAttached;  // CDD ProAudio section C-1-3
+    AudioDeviceInfo mOutputDevInfo;
+    AudioDeviceInfo mInputDevInfo;
+
+    // Loopback Logic
+    NativeAnalyzerThread mNativeAnalyzerThread = null;
+
+    protected double mLatencyMillis = 0.0;
+    protected double mConfidence = 0.0;
+
+    protected static final double CONFIDENCE_THRESHOLD = 0.6;
+    protected static final double PROAUDIO_LATENCY_MS_LIMIT = 20.0;
+
+    // The audio stream callback threads should stop and close
+    // in less than a few hundred msec. This is a generous timeout value.
+    private static final int STOP_TEST_TIMEOUT_MSEC = 2 * 1000;
+
     //
     // Common UI Handling
+    //
     void enableLayout(int layoutId, boolean enable) {
         ViewGroup group = (ViewGroup)findViewById(layoutId);
         for (int i = 0; i < group.getChildCount(); i++) {
@@ -68,12 +109,120 @@
         mLoopbackPortNoBtn = (Button)findViewById(R.id.loopback_tests_no_btn);
         mLoopbackPortNoBtn.setOnClickListener(mBtnClickListener);
 
+        // Connected Device
+        mInputDeviceTxt = ((TextView)findViewById(R.id.audioLoopbackInputLbl));
+        mOutputDeviceTxt = ((TextView)findViewById(R.id.audioLoopbackOutputLbl));
+
         // Loopback Info
         findViewById(R.id.loopback_tests_info_btn).setOnClickListener(mBtnClickListener);
 
+        mAudioLevelText = (TextView)findViewById(R.id.audio_loopback_level_text);
+        mAudioLevelSeekbar = (SeekBar)findViewById(R.id.audio_loopback_level_seekbar);
+        mMaxLevel = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
+        mAudioLevelSeekbar.setMax(mMaxLevel);
+        mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, (int)(0.7 * mMaxLevel), 0);
+        refreshLevel();
+
+        mAudioLevelSeekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+            @Override
+            public void onStopTrackingTouch(SeekBar seekBar) {}
+
+            @Override
+            public void onStartTrackingTouch(SeekBar seekBar) {}
+
+            @Override
+            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+                mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC,
+                        progress, 0);
+                refreshLevel();
+                Log.i(TAG,"Changed stream volume to: " + progress);
+            }
+        });
+
+        mResultText = (TextView)findViewById(R.id.audio_loopback_results_text);
+        mProgressBar = (ProgressBar)findViewById(R.id.audio_loopback_progress_bar);
+        showWait(false);
+
         enableTestUI(false);
     }
 
+    //
+    // Peripheral Connection Logic
+    //
+    protected void scanPeripheralList(AudioDeviceInfo[] devices) {
+        // CDD Section C-1-3: USB port, host-mode support
+
+        // Can't just use the first record because then we will only get
+        // Source OR sink, not both even on devices that are both.
+        mOutputDevInfo = null;
+        mInputDevInfo = null;
+
+        // Any valid peripherals
+        // Do we leave in the Headset test to support a USB-Dongle?
+        for (AudioDeviceInfo devInfo : devices) {
+            if (devInfo.getType() == AudioDeviceInfo.TYPE_USB_DEVICE ||     // USB Peripheral
+                    devInfo.getType() == AudioDeviceInfo.TYPE_USB_HEADSET ||    // USB dongle+LBPlug
+                    devInfo.getType() == AudioDeviceInfo.TYPE_WIRED_HEADSET || // Loopback Plug?
+                    devInfo.getType() == AudioDeviceInfo.TYPE_AUX_LINE) { // Aux-cable loopback?
+                if (devInfo.isSink()) {
+                    mOutputDevInfo = devInfo;
+                }
+                if (devInfo.isSource()) {
+                    mInputDevInfo = devInfo;
+                }
+            }  else {
+                handleDeviceConnection(devInfo);
+            }
+        }
+
+        mIsPeripheralAttached = mOutputDevInfo != null || mInputDevInfo != null;
+        showConnectedAudioPeripheral();
+
+    }
+
+    protected void handleDeviceConnection(AudioDeviceInfo deviceInfo) {
+        // NOP
+    }
+
+    private class ConnectListener extends AudioDeviceCallback {
+        /*package*/ ConnectListener() {}
+
+        //
+        // AudioDevicesManager.OnDeviceConnectionListener
+        //
+        @Override
+        public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
+            scanPeripheralList(mAudioManager.getDevices(AudioManager.GET_DEVICES_ALL));
+        }
+
+        @Override
+        public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
+            scanPeripheralList(mAudioManager.getDevices(AudioManager.GET_DEVICES_ALL));
+        }
+    }
+
+    protected void showConnectedAudioPeripheral() {
+        mInputDeviceTxt.setText(
+                mInputDevInfo != null ? mInputDevInfo.getProductName().toString()
+                        : "");
+        mOutputDeviceTxt.setText(
+                mOutputDevInfo != null ? mOutputDevInfo.getProductName().toString()
+                        : "");
+    }
+
+    /**
+     * refresh Audio Level seekbar and text
+     */
+    private void refreshLevel() {
+        int currentLevel = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
+        mAudioLevelSeekbar.setProgress(currentLevel);
+
+        String levelText = String.format("%s: %d/%d",
+                getResources().getString(R.string.audio_loopback_level_text),
+                currentLevel, mMaxLevel);
+        mAudioLevelText.setText(levelText);
+    }
+
     private void showLoopbackInfoDialog() {
         new AlertDialog.Builder(this)
                 .setTitle(R.string.loopback_dlg_caption)
@@ -82,6 +231,18 @@
                 .show();
     }
 
+    //
+    // show active progress bar
+    //
+    protected void showWait(boolean show) {
+        if (show) {
+            mProgressBar.setVisibility(View.VISIBLE) ;
+        } else {
+            mProgressBar.setVisibility(View.INVISIBLE) ;
+        }
+    }
+
+
     private class OnBtnClickListener implements OnClickListener {
         @Override
         public void onClick(View v) {
@@ -108,10 +269,135 @@
         }
     }
 
+    //
+    // Common loging
+    //
+    protected void recordTestResults() {
+        ReportLog reportLog = getReportLog();
+        reportLog.addValue(
+                "Estimated Latency",
+                mLatencyMillis,
+                ResultType.LOWER_BETTER,
+                ResultUnit.MS);
+
+        reportLog.addValue(
+                "Confidence",
+                mConfidence,
+                ResultType.HIGHER_BETTER,
+                ResultUnit.NONE);
+
+        reportLog.addValue(
+                "Sample Rate",
+                mNativeAnalyzerThread.getSampleRate(),
+                ResultType.NEUTRAL,
+                ResultUnit.NONE);
+
+        reportLog.addValue(
+                "Is Peripheral Attached",
+                mIsPeripheralAttached,
+                ResultType.NEUTRAL,
+                ResultUnit.NONE);
+
+        if (mIsPeripheralAttached) {
+            reportLog.addValue(
+                    "Input Device",
+                    mInputDevInfo != null ? mInputDevInfo.getProductName().toString() : "None",
+                    ResultType.NEUTRAL,
+                    ResultUnit.NONE);
+
+            reportLog.addValue(
+                    "Ouput Device",
+                    mOutputDevInfo != null ? mOutputDevInfo.getProductName().toString() : "None",
+                    ResultType.NEUTRAL,
+                    ResultUnit.NONE);
+        }
+    }
+
+    //
+    //  test logic
+    //
+    protected void startAudioTest(Handler messageHandler) {
+        getPassButton().setEnabled(false);
+        mLatencyMillis = 0.0;
+        mConfidence = 0.0;
+
+        mNativeAnalyzerThread = new NativeAnalyzerThread();
+        if (mNativeAnalyzerThread != null) {
+            mNativeAnalyzerThread.setMessageHandler(messageHandler);
+            // This value matches AAUDIO_INPUT_PRESET_VOICE_RECOGNITION
+            mNativeAnalyzerThread.setInputPreset(MediaRecorder.AudioSource.VOICE_RECOGNITION);
+            mNativeAnalyzerThread.startTest();
+
+            try {
+                Thread.sleep(200);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    protected void handleTestCompletion() {
+        // Make sure the test thread is finished. It should already be done.
+        if (mNativeAnalyzerThread != null) {
+            try {
+                mNativeAnalyzerThread.stopTest(STOP_TEST_TIMEOUT_MSEC);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    /**
+     * handler for messages from audio thread
+     */
+    protected Handler mMessageHandler = new Handler() {
+        public void handleMessage(Message msg) {
+            super.handleMessage(msg);
+            switch(msg.what) {
+                case NativeAnalyzerThread.NATIVE_AUDIO_THREAD_MESSAGE_REC_STARTED:
+                    Log.v(TAG,"got message native rec started!!");
+                    showWait(true);
+                    mResultText.setText("Test Running...");
+                    break;
+                case NativeAnalyzerThread.NATIVE_AUDIO_THREAD_MESSAGE_OPEN_ERROR:
+                    Log.v(TAG,"got message native rec can't start!!");
+                    mResultText.setText("Test Error opening streams.");
+                    handleTestCompletion();
+                    break;
+                case NativeAnalyzerThread.NATIVE_AUDIO_THREAD_MESSAGE_REC_ERROR:
+                    Log.v(TAG,"got message native rec can't start!!");
+                    mResultText.setText("Test Error while recording.");
+                    handleTestCompletion();
+                    break;
+                case NativeAnalyzerThread.NATIVE_AUDIO_THREAD_MESSAGE_REC_COMPLETE_ERRORS:
+                    mResultText.setText("Test FAILED due to errors.");
+                    handleTestCompletion();
+                    break;
+                case NativeAnalyzerThread.NATIVE_AUDIO_THREAD_MESSAGE_REC_COMPLETE:
+                    if (mNativeAnalyzerThread != null) {
+                        mLatencyMillis = mNativeAnalyzerThread.getLatencyMillis();
+                        mConfidence = mNativeAnalyzerThread.getConfidence();
+                    }
+                    mResultText.setText(String.format(
+                            "Test Finished\nLatency:%.2f ms\nConfidence: %.2f",
+                            mLatencyMillis,
+                            mConfidence));
+                    handleTestCompletion();
+                    break;
+                default:
+                    break;
+            }
+        }
+    };
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
 
+        mAudioManager = (AudioManager)getSystemService(AUDIO_SERVICE);
+
+        mAudioManager.registerAudioDeviceCallback(new ConnectListener(), new Handler());
+
         connectLoopbackUI();
     }
 
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioLoopbackLatencyActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioLoopbackLatencyActivity.java
index acb2272..ded5c1e 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioLoopbackLatencyActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioLoopbackLatencyActivity.java
@@ -16,21 +16,9 @@
 
 package com.android.cts.verifier.audio;
 
-import com.android.cts.verifier.R;
-import com.android.compatibility.common.util.ReportLog;
-import com.android.compatibility.common.util.ResultType;
-import com.android.compatibility.common.util.ResultUnit;
 import android.content.Context;
 
-import android.media.AudioDeviceCallback;
-import android.media.AudioDeviceInfo;
-import android.media.AudioManager;
-import android.media.AudioTrack;
-import android.media.MediaRecorder;
-
 import android.os.Bundle;
-import android.os.Handler;
-import android.os.Message;
 
 import android.util.Log;
 
@@ -39,8 +27,12 @@
 
 import android.widget.Button;
 import android.widget.TextView;
-import android.widget.SeekBar;
-import android.widget.ProgressBar;
+
+import com.android.compatibility.common.util.ReportLog;
+import com.android.compatibility.common.util.ResultType;
+import com.android.compatibility.common.util.ResultUnit;
+
+import com.android.cts.verifier.R;
 
 /**
  * Tests Audio Device roundtrip latency by using a loopback plug.
@@ -48,39 +40,18 @@
 public class AudioLoopbackLatencyActivity extends AudioLoopbackBaseActivity {
     private static final String TAG = "AudioLoopbackLatencyActivity";
 
-    public static final int BYTES_PER_FRAME = 2;
+//    public static final int BYTES_PER_FRAME = 2;
 
-    NativeAnalyzerThread mNativeAnalyzerThread = null;
-
-    private int mSamplingRate = 44100;
     private int mMinBufferSizeInFrames = 0;
-    private static final double CONFIDENCE_THRESHOLD = 0.6;
 
     OnBtnClickListener mBtnClickListener = new OnBtnClickListener();
-    Context mContext;
-
-    TextView mAudioLevelText;
-    SeekBar mAudioLevelSeekbar;
 
     Button mTestButton;
 
-    TextView mResultText;
-    ProgressBar mProgressBar;
-
-    private double mLatencyMillis = 0.0;
-    private double mConfidence = 0.0;
-
-    int mMaxLevel;
     private class OnBtnClickListener implements OnClickListener {
         @Override
         public void onClick(View v) {
             switch (v.getId()) {
-                case R.id.audio_loopback_plug_ready_btn:
-                    Log.i(TAG, "audio loopback plug ready");
-                    //enable all the other views.
-                    enableLayout(R.id.audio_loopback_layout, true);
-                    break;
-
                 case R.id.audio_loopback_test_btn:
                     Log.i(TAG, "audio loopback test");
                     startAudioTest();
@@ -95,196 +66,52 @@
         setContentView(R.layout.audio_loopback_latency_activity);
         super.onCreate(savedInstanceState);
 
-        mContext = this;
-
-        mAudioLevelText = (TextView)findViewById(R.id.audio_loopback_level_text);
-        mAudioLevelSeekbar = (SeekBar)findViewById(R.id.audio_loopback_level_seekbar);
-        findViewById(R.id.audio_loopback_plug_ready_btn).setOnClickListener(mBtnClickListener);
         mTestButton =(Button)findViewById(R.id.audio_loopback_test_btn);
         mTestButton.setOnClickListener(mBtnClickListener);
-        mResultText = (TextView)findViewById(R.id.audio_loopback_results_text);
-        mProgressBar = (ProgressBar)findViewById(R.id.audio_loopback_progress_bar);
-        showWait(false);
-
-        enableLayout(R.id.audio_loopback_layout, false);         //disabled all content
-        AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
-        mMaxLevel = am.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
-        mAudioLevelSeekbar.setMax(mMaxLevel);
-        am.setStreamVolume(AudioManager.STREAM_MUSIC, (int)(0.7 * mMaxLevel), 0);
-        refreshLevel();
-
-        mAudioLevelSeekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
-            @Override
-            public void onStopTrackingTouch(SeekBar seekBar) {
-            }
-
-            @Override
-            public void onStartTrackingTouch(SeekBar seekBar) {
-            }
-
-            @Override
-            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
-
-                AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
-                am.setStreamVolume(AudioManager.STREAM_MUSIC,
-                        progress, 0);
-                refreshLevel();
-                Log.i(TAG,"Changed stream volume to: " + progress);
-            }
-        });
 
         setPassFailButtonClickListeners();
         getPassButton().setEnabled(false);
         setInfoResources(R.string.audio_loopback_latency_test, R.string.audio_loopback_info, -1);
     }
 
-    /**
-     * refresh Audio Level seekbar and text
-     */
-    private void refreshLevel() {
-        AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
-
-        int currentLevel = am.getStreamVolume(AudioManager.STREAM_MUSIC);
-        mAudioLevelSeekbar.setProgress(currentLevel);
-
-        String levelText = String.format("%s: %d/%d",
-                getResources().getString(R.string.audio_loopback_level_text),
-                currentLevel, mMaxLevel);
-        mAudioLevelText.setText(levelText);
-    }
-
-    /**
-     * show active progress bar
-     */
-    private void showWait(boolean show) {
-        if (show) {
-            mProgressBar.setVisibility(View.VISIBLE) ;
-        } else {
-            mProgressBar.setVisibility(View.INVISIBLE) ;
-        }
-    }
-
-    /**
-     *  Start the loopback audio test
-     */
-    private void startAudioTest() {
-        getPassButton().setEnabled(false);
+    protected void startAudioTest() {
         mTestButton.setEnabled(false);
-        mLatencyMillis = 0.0;
-        mConfidence = 0.0;
-
-        mNativeAnalyzerThread = new NativeAnalyzerThread();
-        if (mNativeAnalyzerThread != null) {
-            mNativeAnalyzerThread.setMessageHandler(mMessageHandler);
-            // This value matches AAUDIO_INPUT_PRESET_VOICE_RECOGNITION
-            mNativeAnalyzerThread.setInputPreset(MediaRecorder.AudioSource.VOICE_RECOGNITION);
-            mNativeAnalyzerThread.startTest();
-
-            try {
-                Thread.sleep(200);
-            } catch (InterruptedException e) {
-                e.printStackTrace();
-            }
-        }
+        super.startAudioTest(mMessageHandler);
     }
 
-    private void handleTestCompletion() {
-        recordTestResults();
+    protected void handleTestCompletion() {
+        super.handleTestCompletion();
+
         boolean resultValid = mConfidence >= CONFIDENCE_THRESHOLD
                 && mLatencyMillis > 1.0;
         getPassButton().setEnabled(resultValid);
 
-        // Make sure the test thread is finished. It should already be done.
-        if (mNativeAnalyzerThread != null) {
-            try {
-                mNativeAnalyzerThread.stopTest(2 * 1000);
-            } catch (InterruptedException e) {
-                e.printStackTrace();
-            }
-        }
+        recordTestResults();
+
         showWait(false);
         mTestButton.setEnabled(true);
     }
 
     /**
-     * handler for messages from audio thread
-     */
-    private Handler mMessageHandler = new Handler() {
-        public void handleMessage(Message msg) {
-            super.handleMessage(msg);
-            switch(msg.what) {
-                case NativeAnalyzerThread.NATIVE_AUDIO_THREAD_MESSAGE_REC_STARTED:
-                    Log.v(TAG,"got message native rec started!!");
-                    showWait(true);
-                    mResultText.setText("Test Running...");
-                    break;
-                case NativeAnalyzerThread.NATIVE_AUDIO_THREAD_MESSAGE_OPEN_ERROR:
-                    Log.v(TAG,"got message native rec can't start!!");
-                    mResultText.setText("Test Error opening streams.");
-                    handleTestCompletion();
-                    break;
-                case NativeAnalyzerThread.NATIVE_AUDIO_THREAD_MESSAGE_REC_ERROR:
-                    Log.v(TAG,"got message native rec can't start!!");
-                    mResultText.setText("Test Error while recording.");
-                    handleTestCompletion();
-                    break;
-                case NativeAnalyzerThread.NATIVE_AUDIO_THREAD_MESSAGE_REC_COMPLETE_ERRORS:
-                    mResultText.setText("Test FAILED due to errors.");
-                    handleTestCompletion();
-                    break;
-                case NativeAnalyzerThread.NATIVE_AUDIO_THREAD_MESSAGE_REC_COMPLETE:
-                    if (mNativeAnalyzerThread != null) {
-                        mLatencyMillis = mNativeAnalyzerThread.getLatencyMillis();
-                        mConfidence = mNativeAnalyzerThread.getConfidence();
-                    }
-                    mResultText.setText(String.format(
-                            "Test Finished\nLatency:%.2f ms\nConfidence: %.2f",
-                            mLatencyMillis,
-                            mConfidence));
-                    handleTestCompletion();
-                    break;
-                default:
-                    break;
-            }
-        }
-    };
-
-    /**
      * Store test results in log
      */
-    private void recordTestResults() {
+    protected void recordTestResults() {
+        super.recordTestResults();
 
-        getReportLog().addValue(
-                "Estimated Latency",
-                mLatencyMillis,
-                ResultType.LOWER_BETTER,
-                ResultUnit.MS);
-
-        getReportLog().addValue(
-                "Confidence",
-                mConfidence,
-                ResultType.HIGHER_BETTER,
-                ResultUnit.NONE);
-
+        ReportLog reportLog = getReportLog();
         int audioLevel = mAudioLevelSeekbar.getProgress();
-        getReportLog().addValue(
+        reportLog.addValue(
                 "Audio Level",
                 audioLevel,
                 ResultType.NEUTRAL,
                 ResultUnit.NONE);
 
-        getReportLog().addValue(
+        reportLog.addValue(
                 "Frames Buffer Size",
                 mMinBufferSizeInFrames,
                 ResultType.NEUTRAL,
                 ResultUnit.NONE);
 
-        getReportLog().addValue(
-                "Sampling Rate",
-                mSamplingRate,
-                ResultType.NEUTRAL,
-                ResultUnit.NONE);
-
         Log.v(TAG,"Results Recorded");
     }
 }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/NativeAnalyzerThread.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/NativeAnalyzerThread.java
index 42f22aa..2f74074 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/NativeAnalyzerThread.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/NativeAnalyzerThread.java
@@ -38,6 +38,8 @@
     private volatile boolean mEnabled = false;
     private volatile double mLatencyMillis = 0.0;
     private volatile double mConfidence = 0.0;
+    private volatile int mSampleRate = 0;
+
     private int mInputPreset = 0;
 
     static final int NATIVE_AUDIO_THREAD_MESSAGE_REC_STARTED = 892;
@@ -74,6 +76,7 @@
     private native int analyze(long audio_context);
     private native double getLatencyMillis(long audio_context);
     private native double getConfidence(long audio_context);
+    private native int getSampleRate(long audio_context);
 
     public double getLatencyMillis() {
         return mLatencyMillis;
@@ -83,6 +86,8 @@
         return mConfidence;
     }
 
+    public int getSampleRate() { return mSampleRate; }
+
     public synchronized void startTest() {
         if (mThread == null) {
             mEnabled = true;
@@ -111,6 +116,8 @@
     private Runnable mBackGroundTask = () -> {
         mLatencyMillis = 0.0;
         mConfidence = 0.0;
+        mSampleRate = 0;
+
         boolean analysisComplete = false;
 
         log(" Started capture test");
@@ -155,6 +162,7 @@
                     }
                     mLatencyMillis = getLatencyMillis(audioContext);
                     mConfidence = getConfidence(audioContext);
+                    mSampleRate = getSampleRate(audioContext);
                     break;
                 } else {
                     try {
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/ProAudioActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/ProAudioActivity.java
index c952e3b..c1714cc 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/ProAudioActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/ProAudioActivity.java
@@ -17,23 +17,26 @@
 package com.android.cts.verifier.audio;
 
 import android.app.AlertDialog;
-import android.content.ComponentName;
 import android.content.DialogInterface;
-import android.content.Intent;
 import android.content.pm.PackageManager;
-import android.media.AudioDeviceCallback;
 import android.media.AudioDeviceInfo;
 import android.media.AudioFormat;
-import android.media.AudioManager;
+
 import android.os.Bundle;
 import android.os.Handler;
+
 import android.util.Log;
+
 import android.view.View;
+
+import android.widget.Button;
 import android.widget.CheckBox;
 import android.widget.TextView;
-import android.widget.Toast;
 
-import com.android.cts.verifier.PassFailButtons;
+import com.android.compatibility.common.util.ReportLog;
+import com.android.compatibility.common.util.ResultType;
+import com.android.compatibility.common.util.ResultUnit;
+
 import com.android.cts.verifier.R;  // needed to access resource in CTSVerifier project namespace.
 
 public class ProAudioActivity
@@ -50,29 +53,15 @@
     private boolean mClaimsUSBPeripheralMode;  // CDD ProAudio section C-1-3
     private boolean mClaimsHDMI;               // CDD ProAudio section C-1-3
 
-    // Values
-    private static final double LATENCY_MS_LIMIT = 20.0; // CDD ProAudio section C-1-2
-    private double mRoundTripLatency;
-    private static final double CONFIDENCE_LIMIT = 0.75; // TBD
-    private double mRoundTripConfidence;
-
-    // Peripheral(s)
-    AudioManager mAudioManager;
-    private boolean mIsPeripheralAttached;  // CDD ProAudio section C-1-3
-    private AudioDeviceInfo mOutputDevInfo;
-    private AudioDeviceInfo mInputDevInfo;
-
-    private AudioDeviceInfo mHDMIDeviceInfo;
+    AudioDeviceInfo mHDMIDeviceInfo;
 
     // Widgets
-    TextView mInputDeviceTxt;
-    TextView mOutputDeviceTxt;
-    TextView mRoundTripLatencyTxt;
-    TextView mRoundTripConfidenceTxt;
     TextView mHDMISupportLbl;
 
     CheckBox mClaimsHDMICheckBox;
 
+    Button mRoundTripTestButton;
+
     // Borrowed from PassFailButtons.java
     private static final int INFO_DIALOG_ID = 1337;
     private static final String INFO_DIALOG_TITLE_ID = "infoDialogTitleId";
@@ -104,15 +93,6 @@
         return getPackageManager().hasSystemFeature(PackageManager.FEATURE_USB_ACCESSORY);
     }
 
-    private void showConnectedAudioPeripheral() {
-        mInputDeviceTxt.setText(
-                mInputDevInfo != null ? mInputDevInfo.getProductName().toString()
-                        : "");
-        mOutputDeviceTxt.setText(
-                mOutputDevInfo != null ? mOutputDevInfo.getProductName().toString()
-                        : "");
-    }
-
     // HDMI Stuff
     private boolean isHDMIValid() {
         if (mHDMIDeviceInfo == null) {
@@ -164,107 +144,37 @@
         return true;
     }
 
+    protected void handleDeviceConnection(AudioDeviceInfo devInfo) {
+        if (devInfo.isSink() && devInfo.getType() == AudioDeviceInfo.TYPE_HDMI) {
+            mHDMIDeviceInfo = devInfo;
+        }
+
+        if (mHDMIDeviceInfo != null) {
+            mClaimsHDMICheckBox.setChecked(true);
+            mHDMISupportLbl.setText(getResources().getString(
+                    isHDMIValid() ? R.string.pass_button_text : R.string.fail_button_text));
+        }
+        mHDMISupportLbl.setText(getResources().getString(R.string.audio_proaudio_NA));
+
+        calculatePass();
+    }
+
     private void calculatePass() {
         boolean hasPassed = !mClaimsProAudio ||
                 (mClaimsLowLatencyAudio && mClaimsMIDI &&
                 mClaimsUSBHostMode && mClaimsUSBPeripheralMode &&
                 (!mClaimsHDMI || isHDMIValid()) &&
                 mOutputDevInfo != null && mInputDevInfo != null &&
-                mRoundTripLatency != 0.0 && mRoundTripLatency <= LATENCY_MS_LIMIT &&
-                mRoundTripConfidence >= CONFIDENCE_LIMIT);
+                mConfidence >= CONFIDENCE_THRESHOLD && mLatencyMillis <= PROAUDIO_LATENCY_MS_LIMIT);
         getPassButton().setEnabled(hasPassed);
     }
 
-    //
-    // Loopback App Stuff
-    //
-    private final static String LOOPBACK_PACKAGE_NAME = "org.drrickorang.loopback";
-
-    // Test Intents
-    // From Loopback App LoobackActivity.java
-    private static final String INTENT_TEST_TYPE = "TestType";
-
-    // from Loopback App Constant.java
-    public static final int LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_LATENCY = 222;
-
-    public boolean isLoopbackAppInstalled() {
-        try {
-            getPackageManager().getPackageInfo(
-                    LOOPBACK_PACKAGE_NAME, PackageManager.GET_ACTIVITIES);
-            return true;
-        } catch (PackageManager.NameNotFoundException e) {
-            // This indicates that the specified app (Loopback in this case) is NOT installed
-            // fall through...
-        }
-        return false;
-    }
-
-    // arbitrary request code
-    private static final int LATENCY_RESULTS_REQUEST_CODE = 1;
-    private static final String KEY_CTSINVOCATION = "CTS-Test";
-    private static final String KEY_ROUND_TRIP_TIME = "RoundTripTime";
-    private static final String KEY_ROUND_TRIP_CONFIDENCE = "Confidence";
-
-    // We may need to iterate and average round-trip measurements
-    // So add this plumbing though NOT USED.
-    private static final String KEY_NUMITERATIONS = "NumIterations";
-    private static final int NUM_ROUNDTRIPITERATIONS = 3;
-
-    private void runRoundTripTest() {
-        if (!isLoopbackAppInstalled()) {
-            Toast.makeText(this, "Loopback App not installed", Toast.LENGTH_SHORT).show();
-            return;
-        }
-
-        if (!mIsPeripheralAttached) {
-            Toast.makeText(this, "Please connect a USB audio peripheral with loopback cables" +
-                    " before running the latency test.",
-                    Toast.LENGTH_SHORT).show();
-            return;
-        }
-
-        mRoundTripLatency = 0.0;
-        mRoundTripConfidence = 0.0;
-        Intent intent = new Intent(Intent.CATEGORY_LAUNCHER);
-        intent.setComponent(
-            new ComponentName(LOOPBACK_PACKAGE_NAME,LOOPBACK_PACKAGE_NAME + ".LoopbackActivity"));
-
-        intent.putExtra(KEY_CTSINVOCATION, "CTS-Verifier Invocation");
-        intent.putExtra(INTENT_TEST_TYPE, LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_LATENCY);
-        intent.putExtra(KEY_NUMITERATIONS, NUM_ROUNDTRIPITERATIONS);
-
-        startActivityForResult(intent, LATENCY_RESULTS_REQUEST_CODE);
-    }
-
-    @Override
-    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
-        // Check which request we're responding to
-        if (resultCode == RESULT_OK) {
-            Toast.makeText(this, "Round Trip Test Complete.", Toast.LENGTH_SHORT).show();
-            if (requestCode == LATENCY_RESULTS_REQUEST_CODE) {
-                Bundle extras = data != null ? data.getExtras() : null;
-                if (extras != null) {
-                    mRoundTripLatency =  extras.getDouble(KEY_ROUND_TRIP_TIME);
-                    mRoundTripLatencyTxt.setText(String.format("%.2f ms", mRoundTripLatency));
-                    mRoundTripConfidence = extras.getDouble(KEY_ROUND_TRIP_CONFIDENCE);
-                    mRoundTripConfidenceTxt.setText(String.format("%.2f", mRoundTripConfidence));
-                }
-            }
-            calculatePass();
-        } else {
-            Toast.makeText(this, "Round Trip Test Canceled.", Toast.LENGTH_SHORT).show();
-        }
-    }
-
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         setContentView(R.layout.pro_audio);
 
         super.onCreate(savedInstanceState);
 
-        mAudioManager = (AudioManager)getSystemService(AUDIO_SERVICE);
-        mAudioManager.registerAudioDeviceCallback(new ConnectListener(), new Handler());
-
         setPassFailButtonClickListeners();
         setInfoResources(R.string.proaudio_test, R.string.proaudio_info, -1);
 
@@ -291,14 +201,8 @@
         ((TextView)findViewById(
                 R.id.proAudioMidiHasUSBPeripheralLbl)).setText("" + mClaimsUSBPeripheralMode);
 
-        // Connected Device
-        mInputDeviceTxt = ((TextView)findViewById(R.id.proAudioInputLbl));
-        mOutputDeviceTxt = ((TextView)findViewById(R.id.proAudioOutputLbl));
-
-        // Round-trip Latency
-        mRoundTripLatencyTxt = (TextView)findViewById(R.id.proAudioRoundTripLbl);
-        mRoundTripConfidenceTxt = (TextView)findViewById(R.id.proAudioConfidenceLbl);
-        findViewById(R.id.proAudio_runRoundtripBtn).setOnClickListener(this);
+        mRoundTripTestButton = (Button)findViewById(R.id.proAudio_runRoundtripBtn);
+        mRoundTripTestButton.setOnClickListener(this);
 
         // HDMI
         mHDMISupportLbl = (TextView)findViewById(R.id.proAudioHDMISupportLbl);
@@ -308,68 +212,72 @@
         calculatePass();
     }
 
-    private void scanPeripheralList(AudioDeviceInfo[] devices) {
-        // CDD Section C-1-3: USB port, host-mode support
+    protected void startAudioTest() {
+        mRoundTripTestButton.setEnabled(false);
+        super.startAudioTest(mMessageHandler);
+    }
 
-        // Can't just use the first record because then we will only get
-        // Source OR sink, not both even on devices that are both.
-        mOutputDevInfo = null;
-        mInputDevInfo = null;
-
-        // Any valid peripherals
-        // Do we leave in the Headset test to support a USB-Dongle?
-        for (AudioDeviceInfo devInfo : devices) {
-            if (devInfo.getType() == AudioDeviceInfo.TYPE_USB_DEVICE ||     // USB Peripheral
-                devInfo.getType() == AudioDeviceInfo.TYPE_USB_HEADSET ||    // USB dongle+LBPlug
-                devInfo.getType() == AudioDeviceInfo.TYPE_WIRED_HEADSET || // Loopback Plug?
-                devInfo.getType() == AudioDeviceInfo.TYPE_AUX_LINE) { // Aux-cable loopback?
-                if (devInfo.isSink()) {
-                    mOutputDevInfo = devInfo;
-                }
-                if (devInfo.isSource()) {
-                    mInputDevInfo = devInfo;
-                }
-            } else if (devInfo.isSink() && devInfo.getType() == AudioDeviceInfo.TYPE_HDMI) {
-                mHDMIDeviceInfo = devInfo;
-            }
-        }
-
-        mIsPeripheralAttached = mOutputDevInfo != null || mInputDevInfo != null;
-        showConnectedAudioPeripheral();
-
-        if (mHDMIDeviceInfo != null) {
-            mClaimsHDMICheckBox.setChecked(true);
-            mHDMISupportLbl.setText(getResources().getString(
-                    isHDMIValid() ? R.string.pass_button_text : R.string.fail_button_text));
-        }
-        mHDMISupportLbl.setText(getResources().getString(R.string.audio_proaudio_NA));
+    protected void handleTestCompletion() {
+        super.handleTestCompletion();
 
         calculatePass();
+
+        recordTestResults();
+
+        showWait(false);
+        mRoundTripTestButton.setEnabled(true);
     }
 
-    private class ConnectListener extends AudioDeviceCallback {
-        /*package*/ ConnectListener() {}
+    /**
+     * Store test results in log
+     */
+    protected void recordTestResults() {
+        super.recordTestResults();
 
-        //
-        // AudioDevicesManager.OnDeviceConnectionListener
-        //
-        @Override
-        public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
-            scanPeripheralList(mAudioManager.getDevices(AudioManager.GET_DEVICES_ALL));
-        }
+        ReportLog reportLog = getReportLog();
+        reportLog.addValue(
+                "Claims Pro Audio",
+                mClaimsProAudio,
+                ResultType.NEUTRAL,
+                ResultUnit.NONE);
 
-        @Override
-        public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
-            scanPeripheralList(mAudioManager.getDevices(AudioManager.GET_DEVICES_ALL));
-        }
+        reportLog.addValue(
+                "Claims Low-Latency Audio",
+                mClaimsLowLatencyAudio,
+                ResultType.NEUTRAL,
+                ResultUnit.NONE);
+
+        reportLog.addValue(
+                "Claims MIDI",
+                mClaimsMIDI,
+                ResultType.NEUTRAL,
+                ResultUnit.NONE);
+
+        reportLog.addValue(
+                "Claims USB Host Mode",
+                mClaimsUSBHostMode,
+                ResultType.NEUTRAL,
+                ResultUnit.NONE);
+
+        reportLog.addValue(
+                "Claims USB Peripheral Mode",
+                mClaimsUSBPeripheralMode,
+                ResultType.NEUTRAL,
+                ResultUnit.NONE);
+
+        reportLog.addValue(
+                "Claims HDMI",
+                mClaimsHDMI,
+                ResultType.NEUTRAL,
+                ResultUnit.NONE);
     }
 
-    @Override
+        @Override
     public void onClick(View view) {
         switch (view.getId()) {
         case R.id.proAudio_runRoundtripBtn:
-            runRoundTripTest();
-            break;
+            startAudioTest();
+           break;
 
         case R.id.proAudioHasHDMICheckBox:
             if (mClaimsHDMICheckBox.isChecked()) {