Merge "Implement mean and mean absolute deviation over 5 latency tests." into sc-dev
diff --git a/apps/CtsVerifier/jni/audio_loopback/NativeAudioAnalyzer.cpp b/apps/CtsVerifier/jni/audio_loopback/NativeAudioAnalyzer.cpp
index 8a1c63f..bde5c6e 100644
--- a/apps/CtsVerifier/jni/audio_loopback/NativeAudioAnalyzer.cpp
+++ b/apps/CtsVerifier/jni/audio_loopback/NativeAudioAnalyzer.cpp
@@ -202,6 +202,10 @@
     return mPulseLatencyAnalyzer.getMeasuredConfidence();
 }
 
+bool NativeAudioAnalyzer::isLowLatencyStream() {
+    return mIsLowLatencyStream;
+}
+
 int NativeAudioAnalyzer::getSampleRate() {
     return mOutputSampleRate;
 }
@@ -235,6 +239,10 @@
         return result;
     }
 
+    // Did we get a low-latency stream?
+    mIsLowLatencyStream =
+        AAudioStream_getPerformanceMode(mOutputStream) == AAUDIO_PERFORMANCE_MODE_LOW_LATENCY;
+
     int32_t outputFramesPerBurst = AAudioStream_getFramesPerBurst(mOutputStream);
     (void) AAudioStream_setBufferSizeInFrames(mOutputStream, outputFramesPerBurst * kDefaultOutputSizeBursts);
 
diff --git a/apps/CtsVerifier/jni/audio_loopback/NativeAudioAnalyzer.h b/apps/CtsVerifier/jni/audio_loopback/NativeAudioAnalyzer.h
index 3367226..080fe4c 100644
--- a/apps/CtsVerifier/jni/audio_loopback/NativeAudioAnalyzer.h
+++ b/apps/CtsVerifier/jni/audio_loopback/NativeAudioAnalyzer.h
@@ -92,6 +92,11 @@
      */
     double getConfidence();
 
+    /**
+     * Returns true if the stream was successfully opened in low-latency mode.
+     */
+    bool isLowLatencyStream();
+
     aaudio_result_t getError() {
         return mInputError ? mInputError : mOutputError;
     }
@@ -131,6 +136,7 @@
     int32_t            mFramesReadTotal = 0;
     int32_t            mFramesWrittenTotal = 0;
     bool               mIsDone = false;
+    bool               mIsLowLatencyStream = false;
 
     static constexpr int kLogPeriodMillis         = 1000;
     static constexpr int kNumInputChannels        = 1;
diff --git a/apps/CtsVerifier/jni/audio_loopback/jni-bridge.cpp b/apps/CtsVerifier/jni/audio_loopback/jni-bridge.cpp
index 58908bd..6cc3be9 100644
--- a/apps/CtsVerifier/jni/audio_loopback/jni-bridge.cpp
+++ b/apps/CtsVerifier/jni/audio_loopback/jni-bridge.cpp
@@ -77,6 +77,15 @@
     return false;
 }
 
+JNIEXPORT jboolean JNICALL Java_com_android_cts_verifier_audio_NativeAnalyzerThread_isLowlatency
+  (JNIEnv *env __unused, jobject obj __unused, jlong pAnalyzer) {
+    NativeAudioAnalyzer * analyzer = (NativeAudioAnalyzer *) pAnalyzer;
+    if (analyzer != nullptr) {
+        return analyzer->isLowLatencyStream();
+    }
+    return false;
+}
+
 JNIEXPORT jint JNICALL Java_com_android_cts_verifier_audio_NativeAnalyzerThread_getError
   (JNIEnv *env __unused, jobject obj __unused, jlong pAnalyzer) {
     NativeAudioAnalyzer * analyzer = (NativeAudioAnalyzer *) pAnalyzer;
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 b61ea25..92ba0e1 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioLoopbackBaseActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioLoopbackBaseActivity.java
@@ -52,7 +52,7 @@
  * Base class for testing activitiees that require audio loopback hardware..
  */
 public class AudioLoopbackBaseActivity extends PassFailButtons.Activity {
-    private static final String TAG = "AudioLoopbackActivity";
+    private static final String TAG = "AudioLoopbackBaseActivity";
 
     protected AudioManager mAudioManager;
 
@@ -80,8 +80,15 @@
     // Loopback Logic
     NativeAnalyzerThread mNativeAnalyzerThread = null;
 
-    protected double mLatencyMillis = 0.0;
-    protected double mConfidence = 0.0;
+    protected static final int NUM_TEST_PHASES = 5;
+    protected int mTestPhase = 0;
+
+    protected double[] mLatencyMillis = new double[NUM_TEST_PHASES];
+    protected double[] mConfidence = new double[NUM_TEST_PHASES];
+
+    protected double mMeanLatencyMillis;
+    protected double mMeanAbsoluteDeviation;
+    protected double mMeanConfidence;
 
     protected static final double CONFIDENCE_THRESHOLD = 0.6;
     protected static final double PROAUDIO_LATENCY_MS_LIMIT = 20.0;
@@ -276,13 +283,13 @@
         ReportLog reportLog = getReportLog();
         reportLog.addValue(
                 "Estimated Latency",
-                mLatencyMillis,
+                mMeanLatencyMillis,
                 ResultType.LOWER_BETTER,
                 ResultUnit.MS);
 
         reportLog.addValue(
                 "Confidence",
-                mConfidence,
+                mMeanConfidence,
                 ResultType.HIGHER_BETTER,
                 ResultUnit.NONE);
 
@@ -293,6 +300,12 @@
                 ResultUnit.NONE);
 
         reportLog.addValue(
+                "Low Latency Stream",
+                mNativeAnalyzerThread.isLowLatencyStream(),
+                ResultType.NEUTRAL,
+                ResultUnit.NONE);
+
+        reportLog.addValue(
                 "Is Peripheral Attached",
                 mIsPeripheralAttached,
                 ResultType.NEUTRAL,
@@ -316,18 +329,43 @@
     //
     //  test logic
     //
+    private double calculateMeanAbsoluteDeviation(double mean, double[] values) {
+        double sum = 0.0;
+        for (double value : values) {
+            sum += Math.abs(value - mean);
+        }
+        return sum / values.length;
+    }
+
+    private double calculateMean(double[] values) {
+        double sum = 0.0;
+        for (double value : values) {
+            sum += value;
+        }
+        return sum / values.length;
+    }
+
     protected void startAudioTest(Handler messageHandler) {
         getPassButton().setEnabled(false);
-        mLatencyMillis = 0.0;
-        mConfidence = 0.0;
+
+        mTestPhase = 0;
+        java.util.Arrays.fill(mLatencyMillis, 0.0);
+        java.util.Arrays.fill(mConfidence, 0.0);
 
         mNativeAnalyzerThread = new NativeAnalyzerThread(this);
         if (mNativeAnalyzerThread != null) {
             mNativeAnalyzerThread.setMessageHandler(messageHandler);
             // This value matches AAUDIO_INPUT_PRESET_VOICE_RECOGNITION
             mNativeAnalyzerThread.setInputPreset(MediaRecorder.AudioSource.VOICE_RECOGNITION);
+            startTestPhase();
+        }
+    }
+
+    private void startTestPhase() {
+        if (mNativeAnalyzerThread != null) {
             mNativeAnalyzerThread.startTest();
 
+            // what is this for?
             try {
                 Thread.sleep(200);
             } catch (InterruptedException e) {
@@ -337,6 +375,21 @@
     }
 
     protected void handleTestCompletion() {
+        Log.i(TAG, "handleTestCompletion() ...");
+
+        mMeanLatencyMillis = calculateMean(mLatencyMillis);
+        mMeanAbsoluteDeviation = calculateMeanAbsoluteDeviation(mMeanLatencyMillis, mLatencyMillis);
+        mMeanConfidence = calculateMean(mConfidence);
+
+        String result = String.format(
+            "Test Finished\nMean Latency:%.2f ms\nMean Absolute Deviation: %.2f\n" +
+                " Confidence: %.2f\n" +
+                " Low Latency Path: %s",
+            mMeanLatencyMillis,
+            mMeanAbsoluteDeviation,
+            mMeanConfidence,
+            (mNativeAnalyzerThread.isLowLatencyStream() ? "yes" : "no"));
+
         // Make sure the test thread is finished. It should already be done.
         if (mNativeAnalyzerThread != null) {
             try {
@@ -345,6 +398,40 @@
                 e.printStackTrace();
             }
         }
+        Log.i(TAG, result);
+
+        mResultText.setText(result);
+
+        Log.i(TAG, "... Done");
+    }
+
+    protected void handleTestPhaseCompletion() {
+        if (mNativeAnalyzerThread != null && mTestPhase < NUM_TEST_PHASES) {
+            mLatencyMillis[mTestPhase] = mNativeAnalyzerThread.getLatencyMillis();
+            mConfidence[mTestPhase] = mNativeAnalyzerThread.getConfidence();
+
+            String result = String.format(
+                    "Test %d Finished\nLatency: %.2f ms\nConfidence: %.2f\n",
+                    mTestPhase,
+                    mLatencyMillis[mTestPhase],
+                    mConfidence[mTestPhase]);
+            Log.i(TAG, result);
+
+            mResultText.setText(result);
+            try {
+                mNativeAnalyzerThread.stopTest(STOP_TEST_TIMEOUT_MSEC);
+                // Thread.sleep(/*STOP_TEST_TIMEOUT_MSEC*/500);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+
+            mTestPhase++;
+            if (mTestPhase >= NUM_TEST_PHASES) {
+                handleTestCompletion();
+            } else {
+                startTestPhase();
+            }
+        }
     }
 
     /**
@@ -357,7 +444,9 @@
                 case NativeAnalyzerThread.NATIVE_AUDIO_THREAD_MESSAGE_REC_STARTED:
                     Log.v(TAG,"got message native rec started!!");
                     showWait(true);
-                    mResultText.setText("Test Running...");
+                    mResultText.setText(String.format("Test Running (phase: %d, low-latency: %s)...",
+                            (mTestPhase + 1),
+                            (mNativeAnalyzerThread.isLowLatencyStream() ? "yes" : "no")));
                     break;
                 case NativeAnalyzerThread.NATIVE_AUDIO_THREAD_MESSAGE_OPEN_ERROR:
                     Log.v(TAG,"got message native rec can't start!!");
@@ -373,16 +462,15 @@
                     mResultText.setText("Test FAILED due to errors.");
                     handleTestCompletion();
                     break;
+                case NativeAnalyzerThread.NATIVE_AUDIO_THREAD_MESSAGE_ANALYZING:
+                    Log.i(TAG, "NATIVE_AUDIO_THREAD_MESSAGE_ANALYZING");
+                    mResultText.setText(String.format("Analyzing (phase: %d, low-latency: %s)...",
+                            mTestPhase + 1,
+                            (mNativeAnalyzerThread.isLowLatencyStream() ? "yes" : "no")));
+                    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();
+                    Log.i(TAG, "NATIVE_AUDIO_THREAD_MESSAGE_REC_COMPLETE");
+                    handleTestPhaseCompletion();
                     break;
                 default:
                     break;
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 ded5c1e..d492a4e 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioLoopbackLatencyActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioLoopbackLatencyActivity.java
@@ -82,14 +82,17 @@
     protected void handleTestCompletion() {
         super.handleTestCompletion();
 
-        boolean resultValid = mConfidence >= CONFIDENCE_THRESHOLD
-                && mLatencyMillis > 1.0;
+        Log.i(TAG, "handleTestCompletion() ...");
+        boolean resultValid = mMeanConfidence >= CONFIDENCE_THRESHOLD
+                && mMeanLatencyMillis > 1.0;
         getPassButton().setEnabled(resultValid);
 
         recordTestResults();
 
         showWait(false);
         mTestButton.setEnabled(true);
+
+        Log.i(TAG, "... done");
     }
 
     /**
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 12fc9b2..d18f924 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/NativeAnalyzerThread.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/NativeAnalyzerThread.java
@@ -47,6 +47,7 @@
     private volatile double mLatencyMillis = 0.0;
     private volatile double mConfidence = 0.0;
     private volatile int mSampleRate = 0;
+    private volatile boolean mIsLowLatencyStream = false;
 
     private int mInputPreset = 0;
 
@@ -55,6 +56,7 @@
     static final int NATIVE_AUDIO_THREAD_MESSAGE_REC_ERROR = 894;
     static final int NATIVE_AUDIO_THREAD_MESSAGE_REC_COMPLETE = 895;
     static final int NATIVE_AUDIO_THREAD_MESSAGE_REC_COMPLETE_ERRORS = 896;
+    static final int NATIVE_AUDIO_THREAD_MESSAGE_ANALYZING = 897;
 
     public NativeAnalyzerThread(Context context) {
         mContext = context;
@@ -89,6 +91,7 @@
     private native int analyze(long audio_context);
     private native double getLatencyMillis(long audio_context);
     private native double getConfidence(long audio_context);
+    private native boolean isLowlatency(long audio_context);
 
     private native int getSampleRate(long audio_context);
 
@@ -102,6 +105,8 @@
 
     public int getSampleRate() { return mSampleRate; }
 
+    public boolean isLowLatencyStream() { return mIsLowLatencyStream; }
+
     public synchronized void startTest() {
         if (mThread == null) {
             mEnabled = true;
@@ -149,6 +154,7 @@
                 sendMessage(NATIVE_AUDIO_THREAD_MESSAGE_REC_ERROR);
                 mEnabled = false;
             }
+            mIsLowLatencyStream = isLowlatency(audioContext);
 
             final long timeoutMillis = mSecondsToRun * 1000;
             final long startedAtMillis = System.currentTimeMillis();
@@ -164,6 +170,7 @@
 
                     // Analyze the recording and measure latency.
                     mThread.setPriority(Thread.MAX_PRIORITY);
+                    sendMessage(NATIVE_AUDIO_THREAD_MESSAGE_ANALYZING);
                     result = analyze(audioContext);
                     if (result < 0) {
                         break;
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 b95e9e3..95090f1 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/ProAudioActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/ProAudioActivity.java
@@ -164,12 +164,17 @@
     }
 
     private boolean calculatePass() {
-        boolean hasPassed = !mClaimsProAudio ||
+        boolean flagsCorrect = !mClaimsProAudio ||
                 (mClaimsLowLatencyAudio && mClaimsMIDI &&
-                mClaimsUSBHostMode && mClaimsUSBPeripheralMode &&
-                (!mClaimsHDMI || isHDMIValid()) &&
-                mOutputDevInfo != null && mInputDevInfo != null &&
-                mConfidence >= CONFIDENCE_THRESHOLD && mLatencyMillis <= PROAUDIO_LATENCY_MS_LIMIT);
+                 mClaimsUSBHostMode && mClaimsUSBPeripheralMode);
+        boolean hdmiValid = !mClaimsHDMI || isHDMIValid();
+        boolean deviceValid = mOutputDevInfo != null && mInputDevInfo != null;
+
+        boolean hasPassed = flagsCorrect &&
+                hdmiValid &&
+                deviceValid &&
+                mMeanConfidence >= CONFIDENCE_THRESHOLD &&
+                mMeanLatencyMillis <= PROAUDIO_LATENCY_MS_LIMIT;
 
         getPassButton().setEnabled(hasPassed);
         return hasPassed;
@@ -181,9 +186,9 @@
         Resources strings = getResources();
         if (hasPassed) {
             mTestStatusLbl.setText(strings.getString(R.string.audio_proaudio_pass));
-        } else if (mClaimsProAudio && mLatencyMillis > PROAUDIO_LATENCY_MS_LIMIT) {
+        } else if (mClaimsProAudio && mMeanLatencyMillis > PROAUDIO_LATENCY_MS_LIMIT) {
             mTestStatusLbl.setText(strings.getString(R.string.audio_proaudio_latencytoohigh));
-        } else if (mClaimsProAudio && mConfidence < CONFIDENCE_THRESHOLD) {
+        } else if (mClaimsProAudio && mMeanConfidence < CONFIDENCE_THRESHOLD) {
             mTestStatusLbl.setText(strings.getString(R.string.audio_proaudio_confidencetoolow));
         } else if (!mClaimsMIDI) {
             mTestStatusLbl.setText(strings.getString(R.string.audio_proaudio_midinotreported));