Merge "Ensure resource are closed"
diff --git a/src/com/android/media/tests/AdbScreenrecordTest.java b/src/com/android/media/tests/AdbScreenrecordTest.java
index eb50f50..34ecbde 100644
--- a/src/com/android/media/tests/AdbScreenrecordTest.java
+++ b/src/com/android/media/tests/AdbScreenrecordTest.java
@@ -131,10 +131,13 @@
 
             // "resultDictionary" can be used to post results to dashboards like BlackBox
             resultsDictionary = runTest(resultsDictionary, TEST_TIMEOUT_MS);
-        } finally {
             final String metricsStr = Arrays.toString(resultsDictionary.entrySet().toArray());
             CLog.i("Uploading metrics values:\n" + metricsStr);
             mTestRunHelper.endTest(resultsDictionary);
+        } catch (TestFailureException e) {
+            CLog.i("TestRunHelper.reportFailure triggered");
+        } finally {
+            deleteFileFromDevice(getAbsoluteFilename());
         }
     }
 
@@ -153,9 +156,10 @@
      * </ul>
      *
      * @throws DeviceNotAvailableException
+     * @throws TestFailureException
      */
     private Map<String, String> runTest(Map<String, String> results, final long timeout)
-            throws DeviceNotAvailableException {
+            throws DeviceNotAvailableException, TestFailureException {
         final CollectingOutputReceiver receiver = new CollectingOutputReceiver();
         final String cmd = generateAdbScreenRecordCommand();
         final String deviceFileName = getAbsoluteFilename();
@@ -169,15 +173,10 @@
         CLog.i("Wait for recorded file: " + deviceFileName);
         if (!waitForFile(getDevice(), timeout, deviceFileName)) {
             mTestRunHelper.reportFailure("Recorded test file not found");
-            // Since we don't have a file, no need to delete it; we can return here
-            return results;
         }
 
         CLog.i("Get number of recorded frames and recorded length from adb output");
-        if (!extractVideoDataFromAdbOutput(adbOutput, results)) {
-            deleteFileFromDevice(deviceFileName);
-            return results;
-        }
+        extractVideoDataFromAdbOutput(adbOutput, results);
 
         CLog.i("Get duration and bitrate info from video file using '" + AVPROBE_STR + "'");
         try {
@@ -234,6 +233,10 @@
      * @throws DeviceNotAvailableException
      */
     private void deleteFileFromDevice(String deviceFileName) throws DeviceNotAvailableException {
+        if (deviceFileName == null || deviceFileName.isEmpty()) {
+            return;
+        }
+
         CLog.i("Delete file from device: " + deviceFileName);
         getDevice().executeShellCommand("rm -f " + deviceFileName);
     }
@@ -243,15 +246,15 @@
      *
      * @throws DeviceNotAvailableException
      * @throws ParseException
+     * @throws TestFailureException
      */
-    private boolean extractDurationAndBitrateFromVideoFileUsingAvprobe(
+    private void extractDurationAndBitrateFromVideoFileUsingAvprobe(
             String deviceFileName, Map<String, String> results)
-            throws DeviceNotAvailableException, ParseException {
+            throws DeviceNotAvailableException, ParseException, TestFailureException {
         CLog.i("Check if the recorded file has some data in it: " + deviceFileName);
         IFileEntry video = getDevice().getFileEntry(deviceFileName);
         if (video == null || video.getFileEntry().getSizeValue() < 1) {
             mTestRunHelper.reportFailure("Video Entry info failed");
-            return false;
         }
 
         final File recordedVideo = getDevice().pullFile(deviceFileName);
@@ -271,14 +274,12 @@
 
         if (result.getStatus() != CommandStatus.SUCCESS) {
             mTestRunHelper.reportFailure(AVPROBE_STR + " command failed");
-            return false;
         }
 
         String data = result.getStderr();
         CLog.i("data: " + data);
         if (data == null || data.isEmpty()) {
             mTestRunHelper.reportFailure(AVPROBE_STR + " output data is empty");
-            return false;
         }
 
         Matcher m = Pattern.compile(REGEX_IS_VIDEO_OK).matcher(data);
@@ -286,7 +287,6 @@
             final String errMsg =
                     "Video verification failed; no matching verification pattern found";
             mTestRunHelper.reportFailure(errMsg);
-            return false;
         }
 
         String duration = m.group(1);
@@ -296,11 +296,12 @@
 
         results.put(RESULT_KEY_VERIFIED_DURATION, Long.toString(durationInMilliseconds / 1000));
         results.put(RESULT_KEY_VERIFIED_BITRATE, Long.toString(bitrateInKilobits));
-        return true;
     }
 
-    /** Extracts recorded number of frames and recorded video length from adb output */
-    private boolean extractVideoDataFromAdbOutput(String adbOutput, Map<String, String> results) {
+    /** Extracts recorded number of frames and recorded video length from adb output
+     * @throws TestFailureException */
+    private boolean extractVideoDataFromAdbOutput(String adbOutput, Map<String, String> results)
+            throws TestFailureException {
         final String regEx = "recorded (\\d+) frames in (\\d+) second";
         Matcher m = Pattern.compile(regEx).matcher(adbOutput);
         if (!m.find()) {
@@ -387,8 +388,9 @@
         throw new RuntimeException(err);
     }
 
-    /** Verifies that passed in test parameters are legitimate */
-    private boolean verifyTestParameters() {
+    /** Verifies that passed in test parameters are legitimate
+     * @throws TestFailureException */
+    private boolean verifyTestParameters() throws TestFailureException {
         if (mRecordTimeInSeconds != -1 && mRecordTimeInSeconds < 1) {
             final String error =
                     String.format(ERR_OPTION_MALFORMED, OPTION_TIME_LIMIT, mRecordTimeInSeconds);
diff --git a/src/com/android/media/tests/AudioLevelUtility.java b/src/com/android/media/tests/AudioLevelUtility.java
index 3aee1fb..7898d55 100644
--- a/src/com/android/media/tests/AudioLevelUtility.java
+++ b/src/com/android/media/tests/AudioLevelUtility.java
@@ -25,7 +25,7 @@
 /** Class to provide audio level utility functions for a test device */
 public class AudioLevelUtility {
 
-    public static int extractDeviceAudioLevelFromAdbShell(ITestDevice device)
+    public static int extractDeviceHeadsetLevelFromAdbShell(ITestDevice device)
             throws DeviceNotAvailableException {
 
         final String ADB_SHELL_DUMPSYS_AUDIO = "dumpsys audio";
diff --git a/src/com/android/media/tests/AudioLoopbackImageAnalyzer.java b/src/com/android/media/tests/AudioLoopbackImageAnalyzer.java
index 3b6d767..75df13b 100644
--- a/src/com/android/media/tests/AudioLoopbackImageAnalyzer.java
+++ b/src/com/android/media/tests/AudioLoopbackImageAnalyzer.java
@@ -33,13 +33,13 @@
 public class AudioLoopbackImageAnalyzer {
 
     // General
-    private static final int HORIZONTAL_THRESHOLD = 10;
     private static final int VERTICAL_THRESHOLD = 0;
     private static final int PRIMARY_WAVE_COLOR = 0xFF1E4A99;
     private static final int SECONDARY_WAVE_COLOR = 0xFF1D4998;
     private static final int[] TARGET_COLORS_TABLET =
             new int[] {PRIMARY_WAVE_COLOR, SECONDARY_WAVE_COLOR};
-    private static final int[] TARGET_COLORS_PHONE = new int[] {PRIMARY_WAVE_COLOR};
+    private static final int[] TARGET_COLORS_PHONE =
+            new int[] {PRIMARY_WAVE_COLOR, SECONDARY_WAVE_COLOR};
 
     private static final float EXPERIMENTAL_WAVE_MAX_TABLET = 69.0f; // In percent of image height
     private static final float EXPERIMENTAL_WAVE_MAX_PHONE = 32.0f; // In percent of image height
@@ -58,7 +58,7 @@
     // Required numbers of column for a response
     private static final int MIN_NUMBER_OF_COLUMNS = 4;
     // The difference between two amplitude columns should not be more than this
-    private static final float MAX_ALLOWED_COLUMN_DECREASE = 0.50f;
+    private static final float MAX_ALLOWED_COLUMN_DECREASE = 0.42f;
     // Only check MAX_ALLOWED_COLUMN_DECREASE up to this number
     private static final float MIN_NUMBER_OF_DECREASING_COLUMNS = 8;
 
@@ -95,21 +95,24 @@
         final int[] targetColors;
         final int amplitudeCenterMaxDiff;
         final float maxDuration;
-        final int minNrOfZeroesBetweenAmplitudes;
+        final int minNrOfZeroesBetweenAmplitudes = 5;
+        final int horizontalStart; //ignore anything left of this bound
+        int horizontalThreshold = 10;
 
         if (width >= TABLET_SCREEN_MIN_WIDTH && height >= TABLET_SCREEN_MIN_HEIGHT) {
             CLog.i("Apply TABLET config values");
             waveMax = EXPERIMENTAL_WAVE_MAX_TABLET;
             amplitudeCenterMaxDiff = 40;
-            minNrOfZeroesBetweenAmplitudes = 8;
-            maxDuration = 3 * SECTION_WIDTH_IN_PERCENT;
+            maxDuration = 5 * SECTION_WIDTH_IN_PERCENT;
             targetColors = TARGET_COLORS_TABLET;
+            horizontalStart = Math.round(1.7f * SECTION_WIDTH_IN_PERCENT * width / 100.0f);
+            horizontalThreshold = 40;
         } else {
             waveMax = EXPERIMENTAL_WAVE_MAX_PHONE;
             amplitudeCenterMaxDiff = 20;
-            minNrOfZeroesBetweenAmplitudes = 5;
             maxDuration = 2.5f * SECTION_WIDTH_IN_PERCENT;
             targetColors = TARGET_COLORS_PHONE;
+            horizontalStart = Math.round(1 * SECTION_WIDTH_IN_PERCENT * width / 100.0f);
         }
 
         // Amplitude
@@ -122,8 +125,8 @@
         final int[] horizontal = new int[width];
 
         projectPixelsToXAxis(img, targetColors, horizontal, width, height);
-        filter(horizontal, HORIZONTAL_THRESHOLD);
-        final Pair<Integer, Integer> durationBounds = getBounds(horizontal);
+        filter(horizontal, horizontalThreshold);
+        final Pair<Integer, Integer> durationBounds = getBounds(horizontal, horizontalStart, -1);
         if (!boundsWithinRange(durationBounds, 0, width)) {
             final String fmt = "%1$s Upper/Lower bound along horizontal axis not found";
             final String err = String.format(fmt, FN_TAG);
@@ -133,7 +136,7 @@
 
         projectPixelsToYAxis(img, targetColors, vertical, height, durationBounds);
         filter(vertical, VERTICAL_THRESHOLD);
-        final Pair<Integer, Integer> amplitudeBounds = getBounds(vertical);
+        final Pair<Integer, Integer> amplitudeBounds = getBounds(vertical, -1, -1);
         if (!boundsWithinRange(durationBounds, 0, height)) {
             final String fmt = "%1$s: Upper/Lower bound along vertical axis not found";
             final String err = String.format(fmt, FN_TAG);
@@ -216,34 +219,42 @@
         }
 
         final ArrayList<Amplitude> amplitudes = new ArrayList<Amplitude>();
-        Amplitude currentAmplitude = null;
+        Amplitude currentAmplitude = new Amplitude();
+        amplitudes.add(currentAmplitude);
         int zeroCounter = 0;
 
+        // Create amplitude objects that track largest amplitude within a "group" in the array.
+        // Example array:
+        //      [ 202, 530, 420, 12, 0, 0, 0, 0, 0, 0, 0, 236, 423, 262, 0, 0, 0, 0, 0, 0, 0, 0 ]
+        // We would get two amplitude objects with amplitude 530 and 423. Each amplitude object
+        // will also get the number of zeroes to the next amplitude, i.e. 7 and 8 respectively.
         for (int i = durationLeft; i < durationRight; i++) {
             final int v = horizontal[i];
             if (v == 0) {
+                // Count how many consecutive zeroes we have
                 zeroCounter++;
-            } else {
-                CLog.i("index=" + i + ", v=" + v);
+                continue;
+            }
 
-                if (zeroCounter > minNrOfZeroesBetweenAmplitudes) {
-                    // Found a new amplitude; update old amplitude
-                    // with the "gap" count - i.e. nr of zeroes between the amplitudes
-                    if (currentAmplitude != null) {
-                        currentAmplitude.zeroCounter = zeroCounter;
-                    }
+            CLog.i("index=" + i + ", v=" + v);
 
-                    // Create new Amplitude object
-                    currentAmplitude = new Amplitude();
-                    amplitudes.add(currentAmplitude);
+            if (zeroCounter > minNrOfZeroesBetweenAmplitudes) {
+                // Found a new amplitude; update old amplitude
+                // with the "gap" count - i.e. nr of zeroes between the amplitudes
+                if (currentAmplitude != null) {
+                    currentAmplitude.zeroCounter = zeroCounter;
                 }
 
-                // Reset counter
-                zeroCounter = 0;
+                // Create new Amplitude object
+                currentAmplitude = new Amplitude();
+                amplitudes.add(currentAmplitude);
+            }
 
-                if (currentAmplitude != null && v > currentAmplitude.maxHeight) {
-                    currentAmplitude.maxHeight = horizontal[i];
-                }
+            // Reset counter
+            zeroCounter = 0;
+
+            if (currentAmplitude != null && v > currentAmplitude.maxHeight) {
+                currentAmplitude.maxHeight = v;
             }
         }
 
@@ -415,10 +426,18 @@
         }
     }
 
-    private static Pair<Integer, Integer> getBounds(int[] array) {
+    private static Pair<Integer, Integer> getBounds(int[] array, int lowerBound, int upperBound) {
         // Determine min, max
+        if (lowerBound == -1) {
+            lowerBound = 0;
+        }
+
+        if (upperBound == -1) {
+            upperBound = array.length - 1;
+        }
+
         int min = -1;
-        for (int i = 0; i < array.length; i++) {
+        for (int i = lowerBound; i <= upperBound; i++) {
             if (array[i] > 0) {
                 min = i;
                 break;
@@ -426,7 +445,7 @@
         }
 
         int max = -1;
-        for (int i = array.length - 1; i >= 0; i--) {
+        for (int i = upperBound; i >= lowerBound; i--) {
             if (array[i] > 0) {
                 max = i;
                 break;
diff --git a/src/com/android/media/tests/AudioLoopbackTest.java b/src/com/android/media/tests/AudioLoopbackTest.java
index e005408..8835b65 100644
--- a/src/com/android/media/tests/AudioLoopbackTest.java
+++ b/src/com/android/media/tests/AudioLoopbackTest.java
@@ -19,6 +19,7 @@
 
 import com.android.ddmlib.NullOutputReceiver;
 import com.android.ddmlib.testrunner.TestIdentifier;
+import com.android.media.tests.AudioLoopbackImageAnalyzer.Result;
 import com.android.media.tests.AudioLoopbackTestHelper.LogFileType;
 import com.android.media.tests.AudioLoopbackTestHelper.ResultData;
 import com.android.tradefed.config.Option;
@@ -156,6 +157,8 @@
     private static final String KEY_RESULT_PERIOD_CONFIDENCE = "period_confidence";
     private static final String KEY_RESULT_SAMPLING_BLOCK_SIZE = "block_size";
 
+    private static final String REDUCED_GLITCHES_TEST_DURATION = "600"; // 10 min
+
     private static final LogFileType[] LATENCY_TEST_LOGS = {
         LogFileType.RESULT,
         LogFileType.GRAPH,
@@ -322,6 +325,7 @@
 
         mTestRunHelper.startTest(1);
 
+        Map<String, String> metrics = null;
         try {
             if (!verifyTestParameters()) {
                 return;
@@ -330,28 +334,160 @@
             // Stop logcat logging so we can record one logcat log per iteration
             getDevice().stopLogcat();
 
-            // Run test iterations
-            for (int i = 0; i < mIterations; i++) {
-                CLog.i("---- Iteration " + i + " of " + (mIterations - 1) + " -----");
-
-                final ResultData d = new ResultData();
-                d.setIteration(i);
-                Map<String, String> resultsDictionary = null;
-                resultsDictionary = runTest(d, getSingleTestTimeoutValue());
-
-                mLoopbackTestHelper.addTestData(d, resultsDictionary);
+            switch (getTestType()) {
+                case GLITCH:
+                    runGlitchesTest(mTestRunHelper, mLoopbackTestHelper);
+                    break;
+                case LATENCY:
+                case LATENCY_STRESS:
+                    // Run test iterations
+                    runLatencyTest(mLoopbackTestHelper, mIterations);
+                    break;
+                default:
+                    break;
             }
 
             mLoopbackTestHelper.processTestData();
-        } finally {
-            Map<String, String> metrics = uploadLogsReturnMetrics(listener);
+            metrics = uploadLogsReturnMetrics(listener);
             CLog.i("Uploading metrics values:\n" + Arrays.toString(metrics.entrySet().toArray()));
             mTestRunHelper.endTest(metrics);
+        } catch (TestFailureException e) {
+            CLog.i("TestRunHelper.reportFailure triggered");
+        } finally {
+            CLog.i("Test ended - cleanup");
             deleteAllTempFiles();
             getDevice().startLogcat();
         }
     }
 
+    private void runLatencyTest(AudioLoopbackTestHelper loopbackTestHelper, int iterations)
+            throws DeviceNotAvailableException, TestFailureException {
+        for (int i = 0; i < iterations; i++) {
+            CLog.i("---- Iteration " + i + " of " + (iterations - 1) + " -----");
+
+            final ResultData d = new ResultData();
+            d.setIteration(i);
+            Map<String, String> resultsDictionary = null;
+            resultsDictionary = runTest(d, getSingleTestTimeoutValue());
+            loopbackTestHelper.addTestData(d, resultsDictionary, true);
+        }
+    }
+
+    /**
+     * Glitches test, strategy:
+     * <p>
+     *
+     * <ul>
+     *   <li>1. Calibrate Audio level
+     *   <li>2. Run Audio Latency test until seeing good waveform
+     *   <li>3. Run small Glitches test, 5-10 seconds
+     *   <li>4. If numbers look good, run long Glitches test, else run reduced Glitches test
+     * </ul>
+     *
+     * @param testRunHelper
+     * @param loopbackTestHelper
+     * @throws DeviceNotAvailableException
+     * @throws TestFailureException
+     */
+    private void runGlitchesTest(TestRunHelper testRunHelper,
+            AudioLoopbackTestHelper loopbackTestHelper)
+                    throws DeviceNotAvailableException, TestFailureException {
+        final int MAX_RETRIES = 3;
+        int nrOfSuccessfulTests;
+        int counter = 0;
+        AudioLoopbackTestHelper tempTestHelper = null;
+        boolean runningReducedGlitchesTest = false;
+
+        // Step 1: Calibrate Audio level
+        // Step 2: Run Audio Latency test until seeing good waveform
+        final int LOOPBACK_ITERATIONS = 4;
+        final String originalTestType = mTestType;
+        final String originalBufferTestDuration = mBufferTestDuration;
+        mTestType = TESTTYPE_LATENCY_STR;
+        do {
+            nrOfSuccessfulTests = 0;
+            tempTestHelper = new AudioLoopbackTestHelper(LOOPBACK_ITERATIONS);
+            runLatencyTest(tempTestHelper, LOOPBACK_ITERATIONS);
+            nrOfSuccessfulTests = tempTestHelper.processTestData();
+            counter++;
+        } while (nrOfSuccessfulTests <= 0 && counter <= MAX_RETRIES);
+
+        if (nrOfSuccessfulTests <= 0) {
+            testRunHelper.reportFailure("Glitch Setup failed: Latency test");
+        }
+
+        // Retrieve audio level from successful test
+        int audioLevel = -1;
+        List<ResultData> results = tempTestHelper.getAllTestData();
+        for (ResultData rd : results) {
+            // Check if test passed
+            if (rd.getImageAnalyzerResult() == Result.PASS && rd.getConfidence() == 1.0) {
+                audioLevel = rd.getAudioLevel();
+                break;
+            }
+        }
+
+        if (audioLevel < 6) {
+            testRunHelper.reportFailure("Glitch Setup failed: Audio level not valid");
+        }
+
+        CLog.i("Audio Glitch: Audio level is " + audioLevel);
+
+        // Step 3: Run small Glitches test, 5-10 seconds
+        mTestType = originalTestType;
+        mBufferTestDuration = "10";
+        mAudioLevel = Integer.toString(audioLevel);
+
+        counter = 0;
+        int glitches = -1;
+        do {
+            tempTestHelper = new AudioLoopbackTestHelper(1);
+            runLatencyTest(tempTestHelper, 1);
+            Map<String, String> resultsDictionary =
+                    tempTestHelper.getResultDictionaryForIteration(0);
+            final String nrOfGlitches =
+                    resultsDictionary.get(getMetricsKey(KEY_RESULT_NUMBER_OF_GLITCHES));
+            glitches = Integer.parseInt(nrOfGlitches);
+            CLog.i("10 s glitch test produced " + glitches + " glitches");
+            counter++;
+        } while (glitches > 10 || glitches < 0 && counter <= MAX_RETRIES);
+
+        // Step 4: If numbers look good, run long Glitches test
+        if (glitches > 10 || glitches < 0) {
+            // Reduce test time and set some values to 0 once test completes
+            runningReducedGlitchesTest = true;
+            mBufferTestDuration = REDUCED_GLITCHES_TEST_DURATION;
+        } else {
+            mBufferTestDuration = originalBufferTestDuration;
+        }
+
+        final ResultData d = new ResultData();
+        d.setIteration(0);
+        Map<String, String> resultsDictionary = null;
+        resultsDictionary = runTest(d, getSingleTestTimeoutValue());
+        if (runningReducedGlitchesTest) {
+            // Special treatment, we want to upload values, but also indicate that pre-test
+            // conditions failed. We will set the glitches count and zero out the rest.
+            String[] testValuesToChangeArray = new String[] {
+                KEY_RESULT_RECORDER_BENCHMARK,
+                KEY_RESULT_RECORDER_OUTLIER,
+                KEY_RESULT_PLAYER_BENCHMARK,
+                KEY_RESULT_PLAYER_OUTLIER,
+                KEY_RESULT_RECORDER_BUFFER_CALLBACK,
+                KEY_RESULT_PLAYER_BUFFER_CALLBACK
+            };
+
+            for (String key : testValuesToChangeArray) {
+                final String metricsKey = getMetricsKey(key);
+                if (resultsDictionary.containsKey(metricsKey)) {
+                    resultsDictionary.put(metricsKey, "0");
+                }
+            }
+        }
+
+        loopbackTestHelper.addTestData(d, resultsDictionary, false);
+    }
+
     private void initializeTest(ITestInvocationListener listener)
             throws UnsupportedOperationException, DeviceNotAvailableException {
 
@@ -370,7 +506,7 @@
     }
 
     private Map<String, String> runTest(ResultData data, final long timeout)
-            throws DeviceNotAvailableException {
+            throws DeviceNotAvailableException, TestFailureException {
 
         // start measurement and wait for result file
         final NullOutputReceiver receiver = new NullOutputReceiver();
@@ -435,11 +571,11 @@
 
             // Trust but verify, so get Audio Level from ADB and compare to value from app
             final int adbAudioLevel =
-                    AudioLevelUtility.extractDeviceAudioLevelFromAdbShell(getDevice());
-            if (data.getAudioLevel() != adbAudioLevel) {
+                    AudioLevelUtility.extractDeviceHeadsetLevelFromAdbShell(getDevice());
+            if (adbAudioLevel > -1 && data.getAudioLevel() != adbAudioLevel) {
                 final String errMsg =
                         String.format(
-                                "App Audio Level (%1$d)differs from ADB level (%2$d)",
+                                "App Audio Level (%1$d) differs from ADB level (%2$d)",
                                 data.getAudioLevel(), adbAudioLevel);
                 mTestRunHelper.reportFailure(errMsg);
             }
@@ -462,7 +598,7 @@
     }
 
     private Map<String, String> uploadLogsReturnMetrics(ITestInvocationListener listener)
-            throws DeviceNotAvailableException {
+            throws DeviceNotAvailableException, TestFailureException {
 
         // "resultDictionary" is used to post results to dashboards like BlackBox
         // "results" contains test logs to be uploaded; i.e. to Sponge
@@ -546,7 +682,7 @@
         return TestType.NONE;
     }
 
-    private boolean verifyTestParameters() {
+    private boolean verifyTestParameters() throws TestFailureException {
         if (getTestType() != TestType.NONE) {
             return true;
         }
diff --git a/src/com/android/media/tests/AudioLoopbackTestHelper.java b/src/com/android/media/tests/AudioLoopbackTestHelper.java
index 4e9c2b0..83e4a30 100644
--- a/src/com/android/media/tests/AudioLoopbackTestHelper.java
+++ b/src/com/android/media/tests/AudioLoopbackTestHelper.java
@@ -100,10 +100,10 @@
         private String mFailureReason = null;
 
         // Optional
-        private Float mPeriodConfidence = Float.valueOf(0.0f);
-        private Float mRms = Float.valueOf(0.0f);
-        private Float mRmsAverage = Float.valueOf(0.0f);
-        private Integer mBblockSize = Integer.valueOf(0);
+        private Float mPeriodConfidence = null;
+        private Float mRms = null;
+        private Float mRmsAverage = null;
+        private Integer mBblockSize = null;
 
         public float getLatency() {
             return mLatencyMs.floatValue();
@@ -113,6 +113,10 @@
             this.mLatencyMs = Float.valueOf(latencyMs);
         }
 
+        public boolean hasLatency() {
+            return mLatencyMs != null;
+        }
+
         public float getConfidence() {
             return mLatencyConfidence.floatValue();
         }
@@ -121,6 +125,10 @@
             this.mLatencyConfidence = Float.valueOf(latencyConfidence);
         }
 
+        public boolean hasConfidence() {
+            return mLatencyConfidence != null;
+        }
+
         public float getPeriodConfidence() {
             return mPeriodConfidence.floatValue();
         }
@@ -129,6 +137,10 @@
             this.mPeriodConfidence = Float.valueOf(periodConfidence);
         }
 
+        public boolean hasPeriodConfidence() {
+            return mPeriodConfidence != null;
+        }
+
         public float getRMS() {
             return mRms.floatValue();
         }
@@ -137,6 +149,10 @@
             this.mRms = Float.valueOf(rms);
         }
 
+        public boolean hasRMS() {
+            return mRms != null;
+        }
+
         public float getRMSAverage() {
             return mRmsAverage.floatValue();
         }
@@ -145,6 +161,10 @@
             this.mRmsAverage = Float.valueOf(rmsAverage);
         }
 
+        public boolean hasRMSAverage() {
+            return mRmsAverage != null;
+        }
+
         public int getAudioLevel() {
             return mAudioLevel.intValue();
         }
@@ -153,6 +173,10 @@
             this.mAudioLevel = Integer.valueOf(audioLevel);
         }
 
+        public boolean hasAudioLevel() {
+            return mAudioLevel != null;
+        }
+
         public int getBlockSize() {
             return mBblockSize.intValue();
         }
@@ -161,6 +185,10 @@
             this.mBblockSize = Integer.valueOf(blockSize);
         }
 
+        public boolean hasBlockSize() {
+            return mBblockSize != null;
+        }
+
         public int getIteration() {
             return mIteration.intValue();
         }
@@ -215,8 +243,8 @@
         public boolean hasBadResults() {
             return hasTimedOut()
                     || hasNoTestResults()
-                    || hasNoLatencyResult()
-                    || hasNoLatencyConfidence()
+                    || !hasLatency()
+                    || !hasConfidence()
                     || mImageAnalyzerResult == Result.FAIL;
         }
 
@@ -228,16 +256,8 @@
             return mLogs.containsKey(log);
         }
 
-        public boolean hasNoLatencyResult() {
-            return mLatencyMs == null;
-        }
-
-        public boolean hasNoLatencyConfidence() {
-            return mLatencyConfidence == null;
-        }
-
         public boolean hasNoTestResults() {
-            return hasNoLatencyConfidence() && hasNoLatencyResult();
+            return !hasConfidence() && !hasLatency();
         }
 
         public static Comparator<ResultData> latencyComparator =
@@ -287,15 +307,20 @@
         mAllResults = new ArrayList<ResultData>(iterations);
     }
 
-    public void addTestData(ResultData data, Map<String, String> resultDictionary) {
+    public void addTestData(ResultData data,
+            Map<String,
+            String> resultDictionary,
+            boolean useImageAnalyzer) {
         mResultDictionaries.add(data.getIteration(), resultDictionary);
         mAllResults.add(data);
 
-        // Analyze captured screenshot to see if wave form is within reason
-        final String screenshot = data.getLogFile(LogFileType.GRAPH);
-        final Pair<Result, String> result = AudioLoopbackImageAnalyzer.analyzeImage(screenshot);
-        data.setImageAnalyzerResult(result.first);
-        data.setFailureReason(result.second);
+        if (useImageAnalyzer && data.hasLogFile(LogFileType.GRAPH)) {
+            // Analyze captured screenshot to see if wave form is within reason
+            final String screenshot = data.getLogFile(LogFileType.GRAPH);
+            final Pair<Result, String> result = AudioLoopbackImageAnalyzer.analyzeImage(screenshot);
+            data.setImageAnalyzerResult(result.first);
+            data.setFailureReason(result.second);
+        }
     }
 
     public final List<ResultData> getAllTestData() {
@@ -524,13 +549,13 @@
             sb.append(buildId).append(SEPARATOR);
             sb.append(serialNumber).append(SEPARATOR);
             sb.append(data.getIteration()).append(SEPARATOR);
-            sb.append(data.getLatency()).append(SEPARATOR);
-            sb.append(data.getConfidence()).append(SEPARATOR);
-            sb.append(data.getPeriodConfidence()).append(SEPARATOR);
-            sb.append(data.getBlockSize()).append(SEPARATOR);
-            sb.append(data.getAudioLevel()).append(SEPARATOR);
-            sb.append(data.getRMS()).append(SEPARATOR);
-            sb.append(data.getRMSAverage()).append(SEPARATOR);
+            sb.append(data.hasLatency() ? data.getLatency() : "").append(SEPARATOR);
+            sb.append(data.hasConfidence() ? data.getConfidence() : "").append(SEPARATOR);
+            sb.append(data.hasPeriodConfidence() ? data.getPeriodConfidence() : "").append(SEPARATOR);
+            sb.append(data.hasBlockSize() ? data.getBlockSize() : "").append(SEPARATOR);
+            sb.append(data.hasAudioLevel() ? data.getAudioLevel() : "").append(SEPARATOR);
+            sb.append(data.hasRMS() ? data.getRMS() : "").append(SEPARATOR);
+            sb.append(data.hasRMSAverage() ? data.getRMSAverage() : "").append(SEPARATOR);
             sb.append(data.getImageAnalyzerResult().name()).append(SEPARATOR);
             sb.append(data.getFailureReason());
 
@@ -561,13 +586,13 @@
                 continue;
             }
 
-            if (data.hasNoLatencyResult()) {
+            if (!data.hasLatency()) {
                 testWithoutLatencyResultCount++;
                 testNoData++;
                 continue;
             }
 
-            if (data.hasNoLatencyConfidence()) {
+            if (!data.hasConfidence()) {
                 testWithoutConfidenceResultCount++;
                 testNoData++;
                 continue;
diff --git a/src/com/android/media/tests/TestFailureException.java b/src/com/android/media/tests/TestFailureException.java
new file mode 100644
index 0000000..c33c195
--- /dev/null
+++ b/src/com/android/media/tests/TestFailureException.java
@@ -0,0 +1,25 @@
+/*
+ * 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.media.tests;
+
+/** Exception used to indicate test failure. */
+public class TestFailureException extends RuntimeException {
+   static final long serialVersionUID = 1L;
+
+    public TestFailureException() {
+        super();
+    }
+}
diff --git a/src/com/android/media/tests/TestRunHelper.java b/src/com/android/media/tests/TestRunHelper.java
index f752cf3..f619772 100644
--- a/src/com/android/media/tests/TestRunHelper.java
+++ b/src/com/android/media/tests/TestRunHelper.java
@@ -39,18 +39,19 @@
         return mTestStopTime - mTestStartTime;
     }
 
-    public void reportFailure(String errMsg) {
+    public void reportFailure(String errMsg) throws TestFailureException {
         CLog.e(errMsg);
+        mListener.testRunFailed(errMsg);
         mListener.testFailed(mTestId, errMsg);
         mListener.testEnded(mTestId, new HashMap<String, String>());
-        mListener.testRunFailed(errMsg);
+        throw new TestFailureException();
     }
 
     /** @param resultDictionary */
     public void endTest(Map<String, String> resultDictionary) {
         mTestStopTime = System.currentTimeMillis();
-        mListener.testEnded(mTestId, resultDictionary);
         mListener.testRunEnded(getTotalTestTime(), resultDictionary);
+        mListener.testEnded(mTestId, resultDictionary);
     }
 
     public void startTest(int numberOfTests) {