am 0e8d9cf1: Merge "Make tests more reliable by cleaning up worker threads on cancelation. b/17888343" into lmp-dev

* commit '0e8d9cf18bcc2317785bf74d2e2bacb107f7d5e1':
  Make tests more reliable by cleaning up worker threads on cancelation. b/17888343
diff --git a/apps/CtsVerifier/res/values/strings.xml b/apps/CtsVerifier/res/values/strings.xml
index 1fbded5..3b74e52 100644
--- a/apps/CtsVerifier/res/values/strings.xml
+++ b/apps/CtsVerifier/res/values/strings.xml
@@ -469,7 +469,7 @@
     <string name="snsr_test_pass">PASS</string>
     <string name="snsr_test_skipped">SKIPPED</string>
     <string name="snsr_test_fail">FAIL</string>
-    <string name="snsr_executing_test">\nExecuting test case \'%1$s\'..\n</string>
+    <string name="snsr_execution_time">Test execution time %1$s sec</string>
 
     <!-- Strings to interact with users in Sensor Tests -->
     <string name="snsr_test_play_sound">A sound will be played once the verification is complete...</string>
@@ -479,7 +479,6 @@
     <string name="snsr_keep_device_rotating_clockwise">Once the test begins, you will have to keep rotating the device clockwise.</string>
     <string name="snsr_wait_for_user">Press \'Next\' to continue.</string>
     <string name="snsr_wait_to_begin">Press \'Next\' to begin.</string>
-    <string name="snsr_wait_to_complete">Press \'Next\' to complete.</string>
     <string name="snsr_on_complete_return">After completing the task, go back to this test.</string>
     <string name="snsr_movement_expected">Movement was expected during the test. Found=%1$b.</string>
     <string name="snsr_sensor_feature_deactivation">Additionally, turn off any other features installed in the device, that register for sensors. Once you are done, you can continue the test.</string>
@@ -491,7 +490,6 @@
     <string name="snsr_setting_auto_rotate_screen_mode">Auto-rotate screen</string>
     <string name="snsr_setting_keep_screen_on">Stay awake</string>
     <string name="snsr_setting_location_mode">Location</string>
-    <string name="snsr_setting_auto_screen_off_mode">Display Sleep</string>
     <string name="snsr_pass_on_error">Pass Anyway</string>
     <string name="snsr_run_automated_tests">The screen will be turned off to execute the tests,
         when tests complete, the device will vibrate and the screen will be turned back on.</string>
@@ -526,7 +524,6 @@
         facing the ceiling. Read the instructions for each scenario, before you perform the
         test.</string>
     <string name="snsr_gyro_device_static">Leave the device static.</string>
-    <string name="snsr_gyro_rotate_clockwise">Rotate the device clockwise.</string>
     <string name="snsr_gyro_rotate_device">Once you begin the test, you will need to rotate the
         device 360deg (one time) in the direction show by the animation, then place it back on the
         flat surface.</string>
@@ -550,20 +547,15 @@
     <string name="snsr_mag_measurement">-&gt; (%1$.2f, %2$.2f, %3$.2f) : %4$.2f uT</string>
 
     <!-- Sensor Value Accuracy -->
-    <string name="snsr_val_acc_test">Sensor Value Accuracy Tests</string>
     <string name="snsr_rot_vec_test">Rotation Vector Accuracy Test</string>
-    <string name="snsr_collected_events_length">Sensor(%2$s). Collected events expected to be greater than zero. Found=%1$d.</string>
     <string name="snsr_event_length">Sensor(%3$s). Event values expected to have size=%1$d. Found=%2$d.</string>
-    <string name="snsr_event_length_positive">Sensor(%2$s). Event values expected to have size > 0. Found=%1$d</string>
     <string name="snsr_event_value">Sensor(%3$s). Event value[0] expected to be of value=%1$f. Found=%2$f.</string>
     <string name="snsr_event_time">Sensor(%5$s). Event timestamp expected to be synchronized with SystemClock.elapsedRealtimeNanos(). Event received at=%1$d. Event timestamp=%2$d. Delta=%3$d. Threshold=%4$d.</string>
-    <string name="snsr_event_time_positive">Sensor(%2$s). Event timestamp expected to positive (> 0). Found=%1$d.</string>
 
     <!-- Sensor Batching -->
     <string name="snsr_batch_test">Sensor Batching Tests</string>
     <string name="snsr_batching_walking_needed">Once the test begins, you will have to take the
         device in your hand and walk.</string>
-    <string name="snsr_batching_fifo_count">FifoReservedEventCount=%1$d. Expected to be at most FifoMaxEventCount=%2$d.</string>
 
     <!-- Sensor Synchronization -->
     <string name="snsr_synch_test">Sensor Synchronization Test</string>
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/BatchingTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/BatchingTestActivity.java
index 4ba38a9..6f0a7aa 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/BatchingTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/BatchingTestActivity.java
@@ -19,15 +19,12 @@
 import com.android.cts.verifier.R;
 import com.android.cts.verifier.sensors.base.SensorCtsVerifierTestActivity;
 
-import android.content.Context;
 import android.hardware.Sensor;
 import android.hardware.SensorManager;
 import android.hardware.cts.helpers.TestSensorEnvironment;
 import android.hardware.cts.helpers.sensoroperations.TestSensorFlushOperation;
 import android.hardware.cts.helpers.sensoroperations.TestSensorOperation;
 import android.hardware.cts.helpers.sensoroperations.VerifiableSensorOperation;
-import android.os.Bundle;
-import android.os.PowerManager;
 
 import java.util.concurrent.TimeUnit;
 
@@ -51,27 +48,9 @@
     // such events to generate
     private static final int REPORT_LATENCY_25_SEC = 25;
 
-    private PowerManager.WakeLock mWakeLock;
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-    }
-
-    @Override
-    protected void activitySetUp() throws InterruptedException {
-        PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
-        mWakeLock = powerManager.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, "BatchingTests");
-        mWakeLock.acquire();
-    }
-
-    @Override
-    protected void activityCleanUp() throws InterruptedException {
-        mWakeLock.release();
-    }
-
     // TODO: refactor to discover all available sensors of each type and dynamically generate test
     // cases for all of them
+    @SuppressWarnings("unused")
     public String testStepCounter_batching() throws Throwable {
         return runBatchTest(
                 Sensor.TYPE_STEP_COUNTER,
@@ -79,6 +58,7 @@
                 R.string.snsr_batching_walking_needed);
     }
 
+    @SuppressWarnings("unused")
     public String testStepCounter_flush() throws Throwable {
         return runFlushTest(
                 Sensor.TYPE_STEP_COUNTER,
@@ -86,6 +66,7 @@
                 R.string.snsr_batching_walking_needed);
     }
 
+    @SuppressWarnings("unused")
     public String testStepDetector_batching() throws Throwable {
         return  runBatchTest(
                 Sensor.TYPE_STEP_DETECTOR,
@@ -93,6 +74,7 @@
                 R.string.snsr_batching_walking_needed);
     }
 
+    @SuppressWarnings("unused")
     public String testStepDetector_flush() throws Throwable {
         return  runFlushTest(
                 Sensor.TYPE_STEP_DETECTOR,
@@ -100,6 +82,7 @@
                 R.string.snsr_batching_walking_needed);
     }
 
+    @SuppressWarnings("unused")
     public String testProximity_batching() throws Throwable {
         return runBatchTest(
                 Sensor.TYPE_PROXIMITY,
@@ -107,6 +90,7 @@
                 R.string.snsr_interaction_needed);
     }
 
+    @SuppressWarnings("unused")
     public String testProximity_flush() throws Throwable {
         return runFlushTest(
                 Sensor.TYPE_PROXIMITY,
@@ -114,6 +98,7 @@
                 R.string.snsr_interaction_needed);
     }
 
+    @SuppressWarnings("unused")
     public String testLight_batching() throws Throwable {
         return runBatchTest(
                 Sensor.TYPE_LIGHT,
@@ -121,6 +106,7 @@
                 R.string.snsr_interaction_needed);
     }
 
+    @SuppressWarnings("unused")
     public String testLight_flush() throws Throwable {
         return runFlushTest(
                 Sensor.TYPE_LIGHT,
@@ -164,7 +150,7 @@
         return executeTest(operation);
     }
 
-    private String executeTest(VerifiableSensorOperation operation) {
+    private String executeTest(VerifiableSensorOperation operation) throws InterruptedException {
         operation.addDefaultVerifications();
         operation.setLogEvents(true);
         operation.execute();
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/GyroscopeMeasurementTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/GyroscopeMeasurementTestActivity.java
index ac63780..4b2a7f4 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/GyroscopeMeasurementTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/GyroscopeMeasurementTestActivity.java
@@ -51,7 +51,7 @@
     }
 
     @Override
-    protected void activitySetUp() {
+    protected void activitySetUp() throws InterruptedException {
         getTestLogger().logInstructions(R.string.snsr_gyro_device_placement);
         waitForUserToContinue();
         initializeGlSurfaceView(mRenderer);
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/MagneticFieldMeasurementTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/MagneticFieldMeasurementTestActivity.java
index bd18a95..553147b 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/MagneticFieldMeasurementTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/MagneticFieldMeasurementTestActivity.java
@@ -46,7 +46,7 @@
     }
 
     @Override
-    public void activitySetUp() {
+    public void activitySetUp() throws InterruptedException {
         calibrateMagnetometer();
     }
 
@@ -165,7 +165,7 @@
     /**
      * A routine to help operators calibrate the magnetometer.
      */
-    private void calibrateMagnetometer() {
+    private void calibrateMagnetometer() throws InterruptedException {
         SensorEventListener2 listener = new SensorEventListener2() {
             @Override
             public void onSensorChanged(SensorEvent event) {
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/RotationVectorTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/RotationVectorTestActivity.java
index cd94128..6b804dd 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/RotationVectorTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/RotationVectorTestActivity.java
@@ -87,7 +87,7 @@
         // TODO: take reference value automatically when device is 'still'
         clearText();
         appendText(R.string.snsr_rotation_vector_set_reference);
-        waitForUser();
+        waitForUserToContinue();
 
         clearText();
         for (int i = 0; i < MAX_SENSORS_AVAILABLE; ++i) {
@@ -104,7 +104,7 @@
         // TODO: take final value automatically when device becomes 'still' at the end
         clearText();
         appendText(R.string.snsr_rotation_vector_set_final);
-        waitForUser();
+        waitForUserToContinue();
 
         clearText();
         closeGlSurfaceView();
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/SensorPowerTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/SensorPowerTestActivity.java
index cfc4db0..8370d3e 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/SensorPowerTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/SensorPowerTestActivity.java
@@ -34,11 +34,10 @@
         super(SensorPowerTestActivity.class);
     }
 
-
     @Override
-    public void waitForUserAcknowledgement(final String message) {
+    public void waitForUserAcknowledgement(final String message) throws InterruptedException {
         appendText(message);
-        waitForUser();
+        waitForUserToContinue();
     }
 
     @Override
@@ -63,7 +62,7 @@
     }
 
     @Override
-    protected void activitySetUp() {
+    protected void activitySetUp() throws InterruptedException {
         mScreenManipulator = new SensorTestScreenManipulator(getApplicationContext());
         mScreenManipulator.initialize(this);
     }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/SensorSynchronizationTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/SensorSynchronizationTestActivity.java
index 1dd5984..683430c 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/SensorSynchronizationTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/SensorSynchronizationTestActivity.java
@@ -7,7 +7,6 @@
 
 import android.annotation.TargetApi;
 import android.content.Context;
-import android.graphics.Color;
 import android.hardware.Sensor;
 import android.hardware.SensorEvent;
 import android.hardware.SensorEventListener;
@@ -137,9 +136,9 @@
     public String testCrossSensorSynchronization() throws Throwable {
         appendText("This test provides a rough indication of cross-sensor timestamp synchronization.");
         appendText("Hold device still in hand and click 'Next'");
-        waitForUser();
+        waitForUserToBegin();
         clearText();
-        appendText("Quickly twist device upside-down and back", Color.GREEN);
+        appendText("Quickly twist device upside-down and back");
 
         startDataCollection();
         Thread.sleep(DATA_COLLECTION_TIME_IN_MS);
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/SignificantMotionTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/SignificantMotionTestActivity.java
index a84a045..faba445 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/SignificantMotionTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/SignificantMotionTestActivity.java
@@ -59,6 +59,8 @@
     /**
      * Test cases.
      */
+
+    @SuppressWarnings("unused")
     public String testTrigger() throws Throwable {
         return runTest(
                 R.string.snsr_significant_motion_test_trigger,
@@ -67,6 +69,7 @@
                 false /* vibrate */);
     }
 
+    @SuppressWarnings("unused")
     public String testNotTriggerAfterCancel() throws Throwable {
         return runTest(
                 R.string.snsr_significant_motion_test_cancel,
@@ -78,6 +81,7 @@
     /**
      * Verifies that Significant Motion is not trigger by the vibrator motion.
      */
+    @SuppressWarnings("unused")
     public String testVibratorDoesNotTrigger() throws Throwable {
      return runTest(
              R.string.snsr_significant_motion_test_vibration,
@@ -90,6 +94,7 @@
      * Verifies that the natural motion of keeping the device in hand does not change the location.
      * It ensures that Significant Motion will not trigger in that scenario.
      */
+    @SuppressWarnings("unused")
     public String testInHandDoesNotTrigger() throws Throwable {
         return runTest(
                 R.string.snsr_significant_motion_test_in_hand,
@@ -98,6 +103,7 @@
                 false /* vibrate */);
     }
 
+    @SuppressWarnings("unused")
     public String testSittingDoesNotTrigger() throws Throwable {
         return runTest(
                 R.string.snsr_significant_motion_test_sitting,
@@ -106,6 +112,7 @@
                 false /* vibrate */);
     }
 
+    @SuppressWarnings("unused")
     public String testTriggerDeactivation() throws Throwable {
         SensorTestLogger logger = getTestLogger();
         logger.logInstructions(R.string.snsr_significant_motion_test_deactivation);
@@ -167,6 +174,7 @@
             }
         } finally {
             mSensorManager.cancelTriggerSensor(verifier, mSensorSignificantMotion);
+            playSound();
         }
         return result;
     }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/StepCounterTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/StepCounterTestActivity.java
index 76d12d9..98368c6 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/StepCounterTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/StepCounterTestActivity.java
@@ -33,7 +33,6 @@
 import android.os.SystemClock;
 import android.view.MotionEvent;
 import android.view.View;
-import android.widget.ScrollView;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -86,8 +85,7 @@
                     "Sensors Step Counter/Detector are not supported.");
         }
 
-        ScrollView scrollView = (ScrollView) findViewById(R.id.log_scroll_view);
-        scrollView.setOnTouchListener(new View.OnTouchListener() {
+        setLogScrollViewListener(new View.OnTouchListener() {
             @Override
             public boolean onTouch(View v, MotionEvent event) {
                 // during movement of the device, the ScrollView will detect user taps as attempts
@@ -95,14 +93,27 @@
                 // to overcome the fact that a ScrollView cannot be disabled from scrolling, we
                 // listen for ACTION_UP events instead of click events in the child layout
                 long elapsedTime = SystemClock.elapsedRealtimeNanos();
-                if (event.getAction() == MotionEvent.ACTION_UP) {
+                if (event.getAction() != MotionEvent.ACTION_UP) {
+                    return false;
+                }
+
+                try {
                     logUserReportedStep(elapsedTime);
+                } catch (InterruptedException e) {
+                    // we cannot propagate the exception in the main thread, so we just catch and
+                    // restore the status, we don't need to log as we are terminating anyways
+                    Thread.currentThread().interrupt();
                 }
                 return false;
             }
         });
     }
 
+    @Override
+    protected void activityCleanUp() {
+        setLogScrollViewListener(null /* listener */);
+    }
+
     public String testWalking() throws Throwable {
         return runTest(
                 R.string.snsr_step_counter_test_walking,
@@ -323,11 +334,10 @@
         // TODO: with delayed assertions check events of other types are tracked
     }
 
-    private void logUserReportedStep(long timestamp) {
+    private void logUserReportedStep(long timestamp) throws InterruptedException {
         if (!mCheckForMotion) {
             return;
         }
-
         playSound();
         mTimestampsUserReported.add(timestamp);
         getTestLogger().logMessage(R.string.snsr_step_reported, timestamp);
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/base/BaseSensorTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/base/BaseSensorTestActivity.java
index d1c06cb..0bf9636 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/base/BaseSensorTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/base/BaseSensorTestActivity.java
@@ -28,13 +28,14 @@
 import android.content.Context;
 import android.content.Intent;
 import android.hardware.cts.helpers.ActivityResultMultiplexedLatch;
-import android.hardware.cts.helpers.SensorTestStateNotSupportedException;
 import android.media.MediaPlayer;
 import android.opengl.GLSurfaceView;
 import android.os.Bundle;
+import android.os.SystemClock;
 import android.os.Vibrator;
 import android.provider.Settings;
 import android.text.TextUtils;
+import android.text.format.DateUtils;
 import android.util.Log;
 import android.view.View;
 import android.widget.Button;
@@ -42,8 +43,11 @@
 import android.widget.ScrollView;
 import android.widget.TextView;
 
-import java.security.InvalidParameterException;
-import java.util.concurrent.Semaphore;
+import java.util.ArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
 
 /**
  * A base Activity that is used to build different methods to execute tests inside CtsVerifier.
@@ -76,10 +80,11 @@
     private final int mLayoutId;
     private final SensorFeaturesDeactivator mSensorFeaturesDeactivator;
 
-    private final Semaphore mSemaphore = new Semaphore(0);
+    private final ExecutorService mExecutorService = Executors.newSingleThreadExecutor();
     private final SensorTestLogger mTestLogger = new SensorTestLogger();
     private final ActivityResultMultiplexedLatch mActivityResultMultiplexedLatch =
             new ActivityResultMultiplexedLatch();
+    private final ArrayList<CountDownLatch> mWaitForUserLatches = new ArrayList<CountDownLatch>();
 
     private ScrollView mLogScrollView;
     private LinearLayout mLogLayout;
@@ -128,7 +133,13 @@
         mGLSurfaceView = (GLSurfaceView) findViewById(R.id.gl_surface_view);
 
         updateNextButton(false /*enabled*/);
-        new Thread(this).start();
+        mExecutorService.execute(this);
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        mExecutorService.shutdownNow();
     }
 
     @Override
@@ -149,7 +160,12 @@
 
     @Override
     public void onClick(View target) {
-        mSemaphore.release();
+        synchronized (mWaitForUserLatches) {
+            for (CountDownLatch latch : mWaitForUserLatches) {
+                latch.countDown();
+            }
+            mWaitForUserLatches.clear();
+        }
     }
 
     @Override
@@ -167,40 +183,36 @@
      */
     @Override
     public void run() {
+        long startTimeNs = SystemClock.elapsedRealtimeNanos();
         String testName = getTestClassName();
 
-        // guarantee the proper clean up of tests based on the operations that successfully ran
-        SensorTestDetails testDetails = deactivateSensorFeatures();
-        if (testDetails.getResultCode() == SensorTestDetails.ResultCode.PASS) {
-            // sensor features
-            testDetails = executeActivitySetUp();
-            if (testDetails.getResultCode() == SensorTestDetails.ResultCode.PASS) {
-                // activity set up
-                // TODO: implement execution filters:
-                //      - execute all tests and report results officially
-                //      - execute single test or failed tests only
-                testDetails = executeTests();
-                try {
-                    activityCleanUp();
-                } catch (Throwable e) {
-                    testDetails = new SensorTestDetails(
-                            testName,
-                            SensorTestDetails.ResultCode.FAIL,
-                            "[ActivityCleanUp] " + e.getMessage());
-                }
-                // end activity set up
-            }
-            try {
-                mSensorFeaturesDeactivator.requestToRestoreFeatures();
-            } catch (Throwable e) {
-                testDetails = new SensorTestDetails(
-                        testName,
-                        SensorTestDetails.ResultCode.FAIL,
-                        "[RestoreSensorFeatures] " + e.getMessage());
-            }
-            // end sensor features
+        SensorTestDetails testDetails;
+        try {
+            mSensorFeaturesDeactivator.requestDeactivationOfFeatures();
+            testDetails = new SensorTestDetails(testName, SensorTestDetails.ResultCode.PASS);
+        } catch (Throwable e) {
+            testDetails = new SensorTestDetails(testName, "DeactivateSensorFeatures", e);
         }
+
+        SensorTestDetails.ResultCode resultCode = testDetails.getResultCode();
+        if (resultCode == SensorTestDetails.ResultCode.SKIPPED) {
+            // this is an invalid state at this point of the test setup
+            throw new IllegalStateException("Deactivation of features cannot skip the test.");
+        }
+        if (resultCode == SensorTestDetails.ResultCode.PASS) {
+            testDetails = executeActivityTests(testName);
+        }
+
+        // we consider all remaining states at this point, because we could have been half way
+        // deactivating features
+        try {
+            mSensorFeaturesDeactivator.requestToRestoreFeatures();
+        } catch (Throwable e) {
+            testDetails = new SensorTestDetails(testName, "RestoreSensorFeatures", e);
+        }
+
         mTestLogger.logTestDetails(testDetails);
+        mTestLogger.logExecutionTime(startTimeNs);
 
         // because we cannot enforce test failures in several devices, set the test UI so the
         // operator can report the result of the test
@@ -210,6 +222,9 @@
     /**
      * A general set up routine. It executes only once before the first test case.
      *
+     * NOTE: implementers must be aware of the interrupted status of the worker thread, and let
+     * {@link InterruptedException} propagate.
+     *
      * @throws Throwable An exception that denotes the failure of set up. No tests will be executed.
      */
     protected void activitySetUp() throws Throwable {}
@@ -218,6 +233,11 @@
      * A general clean up routine. It executes upon successful execution of {@link #activitySetUp()}
      * and after all the test cases.
      *
+     * NOTE: implementers must be aware of the interrupted status of the worker thread, and handle
+     * it in two cases:
+     * - let {@link InterruptedException} propagate
+     * - if it is invoked with the interrupted status, prevent from showing any UI
+
      * @throws Throwable An exception that will be logged and ignored, for ease of implementation
      *                   by subclasses.
      */
@@ -229,7 +249,7 @@
      *
      * @return A {@link SensorTestDetails} object containing information about the executed tests.
      */
-    protected abstract SensorTestDetails executeTests();
+    protected abstract SensorTestDetails executeTests() throws InterruptedException;
 
     @Override
     public SensorTestLogger getTestLogger() {
@@ -237,11 +257,6 @@
     }
 
     @Deprecated
-    protected void appendText(String text, int textColor) {
-        appendText(text);
-    }
-
-    @Deprecated
     protected void appendText(int resId) {
         mTestLogger.logInstructions(resId);
     }
@@ -268,21 +283,22 @@
      *
      * @param waitMessageResId The action requested to the operator.
      */
-    protected void waitForUser(int waitMessageResId) {
+    protected void waitForUser(int waitMessageResId) throws InterruptedException {
+        CountDownLatch latch = new CountDownLatch(1);
+        synchronized (mWaitForUserLatches) {
+            mWaitForUserLatches.add(latch);
+        }
+
         mTestLogger.logInstructions(waitMessageResId);
         updateNextButton(true);
-        try {
-            mSemaphore.acquire();
-        } catch (InterruptedException e)  {
-            Log.e(LOG_TAG, "Error on waitForUser", e);
-        }
+        latch.await();
         updateNextButton(false);
     }
 
     /**
      * Waits for the operator to acknowledge to begin execution.
      */
-    protected void waitForUserToBegin() {
+    protected void waitForUserToBegin() throws InterruptedException {
         waitForUser(R.string.snsr_wait_to_begin);
     }
 
@@ -290,20 +306,15 @@
      * {@inheritDoc}
      */
     @Override
-    public void waitForUserToContinue() {
+    public void waitForUserToContinue() throws InterruptedException {
         waitForUser(R.string.snsr_wait_for_user);
     }
 
-    @Deprecated
-    protected void waitForUser() {
-        waitForUserToContinue();
-    }
-
     /**
      * {@inheritDoc}
      */
     @Override
-    public int executeActivity(String action) {
+    public int executeActivity(String action) throws InterruptedException {
         return executeActivity(new Intent(action));
     }
 
@@ -311,7 +322,7 @@
      * {@inheritDoc}
      */
     @Override
-    public int executeActivity(Intent intent) {
+    public int executeActivity(Intent intent) throws InterruptedException {
         ActivityResultMultiplexedLatch.Latch latch = mActivityResultMultiplexedLatch.bindThread();
         startActivityForResult(intent, latch.getRequestCode());
         return latch.await();
@@ -352,18 +363,15 @@
     /**
      * Plays a (default) sound as a notification for the operator.
      */
-    protected void playSound() {
+    protected void playSound() throws InterruptedException {
         MediaPlayer player = MediaPlayer.create(this, Settings.System.DEFAULT_NOTIFICATION_URI);
         if (player == null) {
             Log.e(LOG_TAG, "MediaPlayer unavailable.");
             return;
         }
-
         player.start();
         try {
             Thread.sleep(500);
-        } catch(InterruptedException e) {
-            Log.d(LOG_TAG, "Error on playSound", e);
         } finally {
             player.stop();
         }
@@ -411,10 +419,16 @@
         return mTestClass.getName();
     }
 
+    protected void setLogScrollViewListener(View.OnTouchListener listener) {
+        mLogScrollView.setOnTouchListener(listener);
+    }
+
     private void setTestResult(SensorTestDetails testDetails) {
+        // the name here, must be the Activity's name because it is what CtsVerifier expects
+        String name = super.getClass().getName();
         String summary = mTestLogger.getOverallSummary();
-        String name = testDetails.getName();
-        switch(testDetails.getResultCode()) {
+        SensorTestDetails.ResultCode resultCode = testDetails.getResultCode();
+        switch(resultCode) {
             case SKIPPED:
                 TestResult.setPassedResult(this, name, summary);
                 break;
@@ -424,48 +438,50 @@
             case FAIL:
                 TestResult.setFailedResult(this, name, summary);
                 break;
+            case INTERRUPTED:
+                // do not set a result, just return so the test can complete
+                break;
+            default:
+                throw new IllegalStateException("Unknown ResultCode: " + resultCode);
         }
     }
 
-    private SensorTestDetails deactivateSensorFeatures() {
-        String testName = getTestClassName();
-        try {
-            mSensorFeaturesDeactivator.requestDeactivationOfFeatures();
-        } catch (Throwable e) {
-            return new SensorTestDetails(
-                    testName,
-                    SensorTestDetails.ResultCode.FAIL,
-                    "[DeactivateSensorFeatures] " + e.getMessage());
-        }
-        return new SensorTestDetails(
-                testName,
-                SensorTestDetails.ResultCode.PASS,
-                null /* summary */);
-    }
-
-    private SensorTestDetails executeActivitySetUp() {
-        String testName = getTestClassName();
+    private SensorTestDetails executeActivityTests(String testName) {
+        SensorTestDetails testDetails;
         try {
             activitySetUp();
-        } catch (SensorTestStateNotSupportedException e) {
-            return new SensorTestDetails(
-                    testName,
-                    SensorTestDetails.ResultCode.SKIPPED,
-                    e.getMessage());
+            testDetails = new SensorTestDetails(testName, SensorTestDetails.ResultCode.PASS);
         } catch (Throwable e) {
-            return new SensorTestDetails(
-                    testName,
-                    SensorTestDetails.ResultCode.FAIL,
-                    "[ActivitySetUp] " + e.getMessage());
+            testDetails = new SensorTestDetails(testName, "ActivitySetUp", e);
         }
-        return new SensorTestDetails(
-                testName,
-                SensorTestDetails.ResultCode.PASS,
-                null /* summary */);
+
+        SensorTestDetails.ResultCode resultCode = testDetails.getResultCode();
+        if (resultCode == SensorTestDetails.ResultCode.PASS) {
+            // TODO: implement execution filters:
+            //      - execute all tests and report results officially
+            //      - execute single test or failed tests only
+            try {
+                testDetails = executeTests();
+            } catch (Throwable e) {
+                // we catch and continue because we have to guarantee a proper clean-up sequence
+                testDetails = new SensorTestDetails(testName, "TestExecution", e);
+            }
+        }
+
+        // clean-up executes for all states, even on SKIPPED and INTERRUPTED there might be some
+        // intermediate state that needs to be taken care of
+        try {
+            activityCleanUp();
+        } catch (Throwable e) {
+            testDetails = new SensorTestDetails(testName, "ActivityCleanUp", e);
+        }
+
+        return testDetails;
     }
 
     private void promptUserToSetResult(SensorTestDetails testDetails) {
-        if (testDetails.getResultCode() == SensorTestDetails.ResultCode.FAIL) {
+        SensorTestDetails.ResultCode resultCode = testDetails.getResultCode();
+        if (resultCode == SensorTestDetails.ResultCode.FAIL) {
             mTestLogger.logInstructions(R.string.snsr_test_complete_with_errors);
             enableTestResultButton(
                     mPassButton,
@@ -475,7 +491,7 @@
                     mFailButton,
                     R.string.fail_button_text,
                     testDetails.cloneAndChangeResultCode(SensorTestDetails.ResultCode.FAIL));
-        } else {
+        } else if (resultCode != SensorTestDetails.ResultCode.INTERRUPTED) {
             mTestLogger.logInstructions(R.string.snsr_test_complete);
             enableTestResultButton(
                     mPassButton,
@@ -548,7 +564,8 @@
         public void logTestDetails(SensorTestDetails testDetails) {
             String name = testDetails.getName();
             String summary = testDetails.getSummary();
-            switch (testDetails.getResultCode()) {
+            SensorTestDetails.ResultCode resultCode = testDetails.getResultCode();
+            switch (resultCode) {
                 case SKIPPED:
                     logTestSkip(name, summary);
                     break;
@@ -558,9 +575,11 @@
                 case FAIL:
                     logTestFail(name, summary);
                     break;
+                case INTERRUPTED:
+                    // do nothing, the test was interrupted so do we
+                    break;
                 default:
-                    throw new InvalidParameterException(
-                            "Invalid SensorTestDetails.ResultCode: " + testDetails.getResultCode());
+                    throw new IllegalStateException("Unknown ResultCode: " + resultCode);
             }
         }
 
@@ -589,6 +608,17 @@
             return mOverallSummaryBuilder.toString();
         }
 
+        void logExecutionTime(long startTimeNs) {
+            if (Thread.currentThread().isInterrupted()) {
+                return;
+            }
+            long executionTimeNs = SystemClock.elapsedRealtimeNanos() - startTimeNs;
+            long executionTimeSec = TimeUnit.NANOSECONDS.toSeconds(executionTimeNs);
+            // TODO: find a way to format times with nanosecond accuracy and longer than 24hrs
+            String formattedElapsedTime = DateUtils.formatElapsedTime(executionTimeSec);
+            logMessage(R.string.snsr_execution_time, formattedElapsedTime);
+        }
+
         private void logTestEnd(int textViewResId, String testSummary) {
             TextAppender textAppender = new TextAppender(textViewResId);
             textAppender.setText(testSummary);
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/base/ISensorTestStateContainer.java b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/base/ISensorTestStateContainer.java
index 3ef7e21..2ba74e3 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/base/ISensorTestStateContainer.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/base/ISensorTestStateContainer.java
@@ -33,7 +33,7 @@
     /**
      * Waits for the operator to acknowledge to continue execution.
      */
-    void waitForUserToContinue();
+    void waitForUserToContinue() throws InterruptedException;
 
     /**
      * @param resId The resource Id to extract.
@@ -55,7 +55,7 @@
      * @param action The action to start the Activity.
      * @return The Activity's result code.
      */
-    int executeActivity(String action);
+    int executeActivity(String action) throws InterruptedException;
 
     /**
      * Starts an Activity and blocks until it completes, then it returns its result back to the
@@ -64,7 +64,7 @@
      * @param intent The intent to start the Activity.
      * @return The Activity's result code.
      */
-    int executeActivity(Intent intent);
+    int executeActivity(Intent intent) throws InterruptedException;
 
     /**
      * @return The {@link ContentResolver} associated with the test.
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/base/SensorCtsTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/base/SensorCtsTestActivity.java
index 35bff24..16c5fcd 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/base/SensorCtsTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/base/SensorCtsTestActivity.java
@@ -37,7 +37,6 @@
 import org.junit.runners.model.InitializationError;
 import org.junit.runners.model.RunnerBuilder;
 
-import android.app.KeyguardManager;
 import android.content.Context;
 import android.hardware.cts.SensorTestCase;
 import android.os.PowerManager;
@@ -67,11 +66,11 @@
     }
 
     @Override
-    protected void activitySetUp() {
-        mScreenManipulator = new SensorTestScreenManipulator(getApplicationContext());
-        mScreenManipulator.initialize(this);
+    protected void activitySetUp() throws InterruptedException {
         PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
         mWakeLock =  powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "SensorCtsTests");
+        mScreenManipulator = new SensorTestScreenManipulator(getApplicationContext());
+        mScreenManipulator.initialize(this);
 
         SensorTestLogger logger = getTestLogger();
         logger.logInstructions(R.string.snsr_no_interaction);
@@ -93,7 +92,9 @@
             }
         });
         mScreenManipulator.turnScreenOn();
-        mWakeLock.release();
+        if (mWakeLock.isHeld()) {
+            mWakeLock.release();
+        }
     }
 
     @Override
@@ -101,6 +102,7 @@
         super.onDestroy();
         if (mScreenManipulator != null) {
             mScreenManipulator.releaseScreenOn();
+            mScreenManipulator.close();
         }
     }
 
@@ -154,7 +156,7 @@
             return new JUnit38ClassRunner(sensorTestSuite);
         }
 
-        private boolean hasSuiteMethod(Class testClass) {
+        private boolean hasSuiteMethod(Class<?> testClass) {
             try {
                 testClass.getMethod("suite");
                 return true;
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/base/SensorCtsTestResult.java b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/base/SensorCtsTestResult.java
index 851d405..5bbaaf7 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/base/SensorCtsTestResult.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/base/SensorCtsTestResult.java
@@ -39,6 +39,8 @@
     private final Context mContext;
     private final TestResult mWrappedTestResult;
 
+    private volatile boolean mInterrupted;
+
     public SensorCtsTestResult(Context context, TestResult testResult) {
         mContext = context;
         mWrappedTestResult = testResult;
@@ -96,12 +98,23 @@
 
     @Override
     public void runProtected(Test test, Protectable protectable) {
-        mWrappedTestResult.runProtected(test, protectable);
+        try {
+            protectable.protect();
+        } catch (AssertionFailedError e) {
+            addFailure(test, e);
+        } catch (ThreadDeath e) {
+            throw e;
+        } catch (InterruptedException e) {
+            mInterrupted = true;
+            addError(test, e);
+        } catch (Throwable e) {
+            addError(test, e);
+        }
     }
 
     @Override
     public boolean shouldStop() {
-        return mWrappedTestResult.shouldStop();
+        return mInterrupted || mWrappedTestResult.shouldStop();
     }
 
     @Override
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/base/SensorCtsVerifierTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/base/SensorCtsVerifierTestActivity.java
index 09753cc..a88abd0 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/base/SensorCtsVerifierTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/base/SensorCtsVerifierTestActivity.java
@@ -19,8 +19,6 @@
 
 import com.android.cts.verifier.sensors.reporting.SensorTestDetails;
 
-import android.hardware.cts.helpers.SensorTestStateNotSupportedException;
-
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
@@ -47,29 +45,16 @@
     }
 
     /**
-     * {@inheritDoc}
-     */
-    protected SensorCtsVerifierTestActivity(
-            Class<? extends SensorCtsVerifierTestActivity> testClass,
-            int layoutId) {
-        super(testClass, layoutId);
-    }
-
-    /**
      * Executes Semi-automated Sensor tests.
      * Execution is driven by this class, and allows discovery of tests using reflection.
      */
     @Override
-    protected SensorTestDetails executeTests() {
+    protected SensorTestDetails executeTests() throws InterruptedException {
         // TODO: use reporting to log individual test results
-        StringBuilder overallTestResults = new StringBuilder();
         for (Method testMethod : findTestMethods()) {
             SensorTestDetails testDetails = executeTest(testMethod);
             getTestLogger().logTestDetails(testDetails);
-            overallTestResults.append(testDetails.toString());
-            overallTestResults.append("\n");
         }
-
         return new SensorTestDetails(
                 getApplicationContext(),
                 getTestClassName(),
@@ -91,34 +76,40 @@
         return testMethods;
     }
 
-    private SensorTestDetails executeTest(Method testMethod) {
+    private SensorTestDetails executeTest(Method testMethod) throws InterruptedException {
         String testMethodName = testMethod.getName();
         String testName = String.format("%s#%s", getTestClassName(), testMethodName);
-        String testSummary;
-        SensorTestDetails.ResultCode testResultCode;
 
+        SensorTestDetails testDetails;
         try {
-            getTestLogger().logTestStart(testMethod.getName());
-            testSummary = (String) testMethod.invoke(this);
-            testResultCode = SensorTestDetails.ResultCode.PASS;
-            ++mTestPassedCounter;
+            getTestLogger().logTestStart(testMethodName);
+            String testSummary = (String) testMethod.invoke(this);
+            testDetails =
+                    new SensorTestDetails(testName, SensorTestDetails.ResultCode.PASS, testSummary);
         } catch (InvocationTargetException e) {
             // get the inner exception, because we use reflection APIs to execute the test
-            Throwable cause = e.getCause();
-            testSummary = cause.getMessage();
-            if (cause instanceof SensorTestStateNotSupportedException) {
-                testResultCode = SensorTestDetails.ResultCode.SKIPPED;
-                ++mTestSkippedCounter;
-            } else {
-                testResultCode = SensorTestDetails.ResultCode.FAIL;
-                ++mTestFailedCounter;
-            }
+            testDetails = new SensorTestDetails(testName, "TestExecution", e.getCause());
         } catch (Throwable e) {
-            testSummary = e.getMessage();
-            testResultCode = SensorTestDetails.ResultCode.FAIL;
-            ++mTestFailedCounter;
+            testDetails = new SensorTestDetails(testName, "TestInfrastructure", e);
         }
 
-        return new SensorTestDetails(testName, testResultCode, testSummary);
+        SensorTestDetails.ResultCode resultCode = testDetails.getResultCode();
+        switch(resultCode) {
+            case PASS:
+                ++mTestPassedCounter;
+                break;
+            case SKIPPED:
+                ++mTestSkippedCounter;
+                break;
+            case INTERRUPTED:
+                throw new InterruptedException();
+            case FAIL:
+                ++mTestFailedCounter;
+                break;
+            default:
+                throw new IllegalStateException("Unknown ResultCode: " + resultCode);
+        }
+
+        return testDetails;
     }
 }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/helpers/PowerTestHostLink.java b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/helpers/PowerTestHostLink.java
index 0041aec..ed2fea3 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/helpers/PowerTestHostLink.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/helpers/PowerTestHostLink.java
@@ -53,7 +53,7 @@
     public interface HostToDeviceInterface {
         void logTestResult(SensorTestDetails testDetails);
         void raiseError(String testName, String message) throws Exception;
-        void waitForUserAcknowledgement(String message);
+        void waitForUserAcknowledgement(String message) throws InterruptedException;
         void logText(String text);
         void turnScreenOff();
     }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/helpers/SensorFeaturesDeactivator.java b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/helpers/SensorFeaturesDeactivator.java
index d69d343..65a3a4d 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/helpers/SensorFeaturesDeactivator.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/helpers/SensorFeaturesDeactivator.java
@@ -48,7 +48,7 @@
         mStateContainer = stateContainer;
     }
 
-    public synchronized void requestDeactivationOfFeatures() {
+    public synchronized void requestDeactivationOfFeatures() throws InterruptedException {
         captureInitialState();
 
         mAirplaneMode.requestToSetMode(mStateContainer, true);
@@ -63,11 +63,17 @@
         mStateContainer.waitForUserToContinue();
     }
 
-    public synchronized void requestToRestoreFeatures() {
+    public synchronized void requestToRestoreFeatures() throws InterruptedException {
         if (!isInitialStateCaptured()) {
             return;
         }
 
+        if (Thread.currentThread().isInterrupted()) {
+            // TODO: in the future, if the thread is interrupted, we might need to serialize the
+            //       intermediate state we acquired so we can restore when we have a chance
+            return;
+        }
+
         mAirplaneMode.requestToResetMode(mStateContainer);
         mScreenBrightnessMode.requestToResetMode(mStateContainer);
         mAutoRotateScreenMode.requestToResetMode(mStateContainer);
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/helpers/SensorSettingContainer.java b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/helpers/SensorSettingContainer.java
index 9a0d7e5..82df502 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/helpers/SensorSettingContainer.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/helpers/SensorSettingContainer.java
@@ -43,7 +43,7 @@
 
     public synchronized void requestToSetMode(
             ISensorTestStateContainer stateContainer,
-            boolean modeOn) {
+            boolean modeOn) throws  InterruptedException {
         trySetMode(stateContainer, modeOn);
         if (getCurrentSettingMode() != modeOn) {
             String message = stateContainer.getString(
@@ -54,11 +54,13 @@
         }
     }
 
-    public synchronized void requestToResetMode(ISensorTestStateContainer stateContainer) {
+    public synchronized void requestToResetMode(ISensorTestStateContainer stateContainer)
+            throws InterruptedException {
         trySetMode(stateContainer, mCapturedModeOn);
     }
 
-    private void trySetMode(ISensorTestStateContainer stateContainer, boolean modeOn) {
+    private void trySetMode(ISensorTestStateContainer stateContainer, boolean modeOn)
+            throws InterruptedException {
         BaseSensorTestActivity.SensorTestLogger logger = stateContainer.getTestLogger();
         String settingName = getSettingName(stateContainer);
         if (getCurrentSettingMode() == modeOn) {
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/helpers/SensorTestScreenManipulator.java b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/helpers/SensorTestScreenManipulator.java
index 0263975..835ff56 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/helpers/SensorTestScreenManipulator.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/helpers/SensorTestScreenManipulator.java
@@ -85,7 +85,8 @@
      * NOTE: Initialization will bring up an Activity to let the user activate the Device Admin,
      * this method will block until the user completes the operation.
      */
-    public synchronized void initialize(ISensorTestStateContainer stateContainer) {
+    public synchronized void initialize(ISensorTestStateContainer stateContainer)
+            throws InterruptedException {
         if (!isDeviceAdminInitialized()) {
             Intent intent = new Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN);
             intent.putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, mComponentName);
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/reporting/SensorTestDetails.java b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/reporting/SensorTestDetails.java
index dcf6c4a..c88187c 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/reporting/SensorTestDetails.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/reporting/SensorTestDetails.java
@@ -22,6 +22,7 @@
 import org.junit.runner.Result;
 
 import android.content.Context;
+import android.hardware.cts.helpers.SensorTestStateNotSupportedException;
 
 /**
  * A class that holds the result of a Sensor test execution.
@@ -34,7 +35,12 @@
     public enum ResultCode {
         SKIPPED,
         PASS,
-        FAIL
+        FAIL,
+        INTERRUPTED
+    }
+
+    public SensorTestDetails(String name, ResultCode resultCode) {
+        this(name, resultCode, null /* summary */);
     }
 
     public SensorTestDetails(String name, ResultCode resultCode, String summary) {
@@ -69,6 +75,21 @@
                 result.getFailureCount());
     }
 
+    public SensorTestDetails(String name, String tag, Throwable cause) {
+        ResultCode resultCode = ResultCode.FAIL;
+        if (cause instanceof InterruptedException) {
+            resultCode = ResultCode.INTERRUPTED;
+            // the interrupted status must be restored, so other routines can consume it
+            Thread.currentThread().interrupt();
+        } else if (cause instanceof SensorTestStateNotSupportedException) {
+            resultCode = ResultCode.SKIPPED;
+        }
+
+        mName = name;
+        mResultCode = resultCode;
+        mSummary = String.format("[%s] %s", tag, cause.getMessage());
+    }
+
     public String getName() {
         return mName;
     }
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/ActivityResultMultiplexedLatch.java b/tests/tests/hardware/src/android/hardware/cts/helpers/ActivityResultMultiplexedLatch.java
index bfc59c4..093a659 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/ActivityResultMultiplexedLatch.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/ActivityResultMultiplexedLatch.java
@@ -16,9 +16,6 @@
 
 package android.hardware.cts.helpers;
 
-import android.app.Activity;
-import android.util.Log;
-
 import java.util.HashMap;
 import java.util.concurrent.CountDownLatch;
 
@@ -53,13 +50,8 @@
          *
          * @return The result code of the Activity executed.
          */
-        public int await() {
-            try {
-                mEntry.latch.await();
-            } catch (InterruptedException e) {
-                Log.e(TAG, "Error waiting for Activity result.", e);
-                return Activity.RESULT_CANCELED;
-            }
+        public int await() throws InterruptedException {
+            mEntry.latch.await();
             return mEntry.resultCode;
         }
 
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/CollectingSensorEventListener.java b/tests/tests/hardware/src/android/hardware/cts/helpers/CollectingSensorEventListener.java
index c11b29f..ca7d133 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/CollectingSensorEventListener.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/CollectingSensorEventListener.java
@@ -64,7 +64,7 @@
      * </p>
      */
     @Override
-    public void waitForEvents(int eventCount) {
+    public void waitForEvents(int eventCount) throws InterruptedException {
         clearEvents();
         super.waitForEvents(eventCount);
     }
@@ -76,7 +76,7 @@
      * </p>
      */
     @Override
-    public void waitForEvents(long duration, TimeUnit timeUnit) {
+    public void waitForEvents(long duration, TimeUnit timeUnit) throws InterruptedException {
         clearEvents();
         super.waitForEvents(duration, timeUnit);
     }
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/SensorCtsHelper.java b/tests/tests/hardware/src/android/hardware/cts/helpers/SensorCtsHelper.java
index 1f1b290..a79e5b1 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/SensorCtsHelper.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/SensorCtsHelper.java
@@ -26,7 +26,7 @@
 /**
  * Set of static helper methods for CTS tests.
  */
-//TODO: Refactor this class into several more well defined helper classes
+//TODO: Refactor this class into several more well defined helper classes, look at StatisticsUtils
 public class SensorCtsHelper {
 
     private static final long NANOS_PER_MILLI = 1000000;
@@ -143,13 +143,9 @@
     /**
      * Helper method to sleep for a given duration.
      */
-    public static void sleep(long duration, TimeUnit timeUnit) {
+    public static void sleep(long duration, TimeUnit timeUnit) throws InterruptedException {
         long durationNs = TimeUnit.NANOSECONDS.convert(duration, timeUnit);
-        try {
-            Thread.sleep(durationNs / NANOS_PER_MILLI, (int) (durationNs % NANOS_PER_MILLI));
-        } catch (InterruptedException e) {
-            // Ignore
-        }
+        Thread.sleep(durationNs / NANOS_PER_MILLI, (int) (durationNs % NANOS_PER_MILLI));
     }
 
     /**
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/TestSensorEventListener.java b/tests/tests/hardware/src/android/hardware/cts/helpers/TestSensorEventListener.java
index 4505633..9b3a5e4 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/TestSensorEventListener.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/TestSensorEventListener.java
@@ -21,6 +21,7 @@
 import android.hardware.Sensor;
 import android.hardware.SensorEvent;
 import android.hardware.SensorEventListener2;
+import android.os.SystemClock;
 import android.util.Log;
 
 import java.util.Arrays;
@@ -87,30 +88,20 @@
      */
     @Override
     public void onSensorChanged(SensorEvent event) {
+        mListener.onSensorChanged(event);
+        if (mLogEvents) {
+            Log.v(LOG_TAG, String.format(
+                    "Sensor %d: sensor_timestamp=%dns, received_timestamp=%dns, values=%s",
+                    mEnvironment.getSensor().getType(),
+                    event.timestamp,
+                    SystemClock.elapsedRealtimeNanos(),
+                    Arrays.toString(event.values)));
+        }
+
         CountDownLatch eventLatch = mEventLatch;
         if(eventLatch != null) {
             eventLatch.countDown();
         }
-        mListener.onSensorChanged(event);
-        if (mLogEvents) {
-            StringBuilder valuesSb = new StringBuilder();
-            if (event.values.length == 1) {
-                valuesSb.append(String.format("%.2f", event.values[0]));
-            } else {
-                valuesSb.append("[").append(String.format("%.2f", event.values[0]));
-                for (int i = 1; i < event.values.length; i++) {
-                    valuesSb.append(String.format(", %.2f", event.values[i]));
-                }
-                valuesSb.append("]");
-            }
-
-            Log.v(LOG_TAG, String.format(
-                    "Sensor %d: sensor_timestamp=%d, received_timestamp=%d, values=%s",
-                    mEnvironment.getSensor().getType(),
-                    event.timestamp,
-                    System.nanoTime(),
-                    Arrays.toString(event.values)));
-        }
     }
 
     /**
@@ -139,17 +130,14 @@
      *
      * @throws AssertionError if there was a timeout after {@link #FLUSH_TIMEOUT_US} &micro;s
      */
-    public void waitForFlushComplete() {
+    public void waitForFlushComplete() throws InterruptedException {
         CountDownLatch latch = mFlushLatch;
-        try {
-            if(latch != null) {
-                Assert.assertTrue(
-                        SensorCtsHelper.formatAssertionMessage("WaitForFlush", mEnvironment),
-                        latch.await(FLUSH_TIMEOUT_US, TimeUnit.MICROSECONDS));
-            }
-        } catch(InterruptedException e) {
-            // Ignore
+        if(latch == null) {
+            return;
         }
+        Assert.assertTrue(
+                SensorCtsHelper.formatAssertionMessage("WaitForFlush", mEnvironment),
+                latch.await(FLUSH_TIMEOUT_US, TimeUnit.MICROSECONDS));
     }
 
     /**
@@ -157,15 +145,14 @@
      *
      * @throws AssertionError if there was a timeout after {@link #FLUSH_TIMEOUT_US} &micro;s
      */
-    public void waitForEvents(int eventCount) {
+    public void waitForEvents(int eventCount) throws InterruptedException {
         mEventLatch = new CountDownLatch(eventCount);
         try {
             int rateUs = mEnvironment.getExpectedSamplingPeriodUs();
             // Timeout is 2 * event count * expected period + batch timeout + default wait
-            long timeoutUs = ((2 * eventCount * rateUs)
+            long timeoutUs = (2 * eventCount * rateUs)
                     + mEnvironment.getMaxReportLatencyUs()
-                    + EVENT_TIMEOUT_US);
-
+                    + EVENT_TIMEOUT_US;
             String message = SensorCtsHelper.formatAssertionMessage(
                     "WaitForEvents",
                     mEnvironment,
@@ -173,8 +160,6 @@
                     eventCount,
                     eventCount - mEventLatch.getCount());
             Assert.assertTrue(message, mEventLatch.await(timeoutUs, TimeUnit.MICROSECONDS));
-        } catch(InterruptedException e) {
-            // Ignore
         } finally {
             mEventLatch = null;
         }
@@ -183,7 +168,7 @@
     /**
      * Collect {@link TestSensorEvent} for a specific duration.
      */
-    public void waitForEvents(long duration, TimeUnit timeUnit) {
+    public void waitForEvents(long duration, TimeUnit timeUnit) throws InterruptedException {
         SensorCtsHelper.sleep(duration, timeUnit);
     }
 }
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/TestSensorManager.java b/tests/tests/hardware/src/android/hardware/cts/helpers/TestSensorManager.java
index d72a2ce..dc40ff4 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/TestSensorManager.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/TestSensorManager.java
@@ -120,24 +120,22 @@
     /**
      * Wait for a specific number of events.
      */
-    public void waitForEvents(int eventCount) {
+    public void waitForEvents(int eventCount) throws InterruptedException {
         if (mTestSensorEventListener == null) {
             Log.w(LOG_TAG, "No listener registered, returning.");
             return;
         }
-
         mTestSensorEventListener.waitForEvents(eventCount);
     }
 
     /**
      * Wait for a specific duration.
      */
-    public void waitForEvents(long duration, TimeUnit timeUnit) {
+    public void waitForEvents(long duration, TimeUnit timeUnit) throws InterruptedException {
         if (mTestSensorEventListener == null) {
             Log.w(LOG_TAG, "No listener registered, returning.");
             return;
         }
-
         mTestSensorEventListener.waitForEvents(duration, timeUnit);
     }
 
@@ -168,7 +166,6 @@
         if (mTestSensorEventListener == null) {
             return;
         }
-
         mTestSensorEventListener.waitForFlushComplete();
     }
 
@@ -185,7 +182,6 @@
         if (mTestSensorEventListener == null) {
             return;
         }
-
         startFlush();
         waitForFlushCompleted();
     }
@@ -193,12 +189,12 @@
     /**
      * Register a listener, wait for a specific number of events, and then unregister the listener.
      */
-    public void runSensor(TestSensorEventListener listener, int eventCount) {
+    public void runSensor(TestSensorEventListener listener, int eventCount)
+            throws InterruptedException {
         if (mTestSensorEventListener != null) {
             Log.w(LOG_TAG, "Listener already registered, returning.");
             return;
         }
-
         try {
             registerListener(listener);
             waitForEvents(eventCount);
@@ -210,12 +206,12 @@
     /**
      * Register a listener, wait for a specific duration, and then unregister the listener.
      */
-    public void runSensor(TestSensorEventListener listener, long duration, TimeUnit timeUnit) {
+    public void runSensor(TestSensorEventListener listener, long duration, TimeUnit timeUnit)
+            throws InterruptedException {
         if (mTestSensorEventListener != null) {
             Log.w(LOG_TAG, "Listener already registered, returning.");
             return;
         }
-
         try {
             registerListener(listener);
             waitForEvents(duration, timeUnit);
@@ -231,7 +227,7 @@
     public void runSensorAndFlush(
             TestSensorEventListener listener,
             long duration,
-            TimeUnit timeUnit) {
+            TimeUnit timeUnit) throws InterruptedException {
         if (mTestSensorEventListener != null) {
             Log.w(LOG_TAG, "Listener already registered, returning.");
             return;
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/AlarmOperation.java b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/AlarmOperation.java
index 95f1248..88e4954 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/AlarmOperation.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/AlarmOperation.java
@@ -72,7 +72,7 @@
      * {@inheritDoc}
      */
     @Override
-    public void execute() {
+    public void execute() throws InterruptedException {
         // Start alarm
         IntentFilter intentFilter = new IntentFilter(ACTION);
         BroadcastReceiver receiver = new BroadcastReceiver() {
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/DelaySensorOperation.java b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/DelaySensorOperation.java
index bf43189..b4d1f23 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/DelaySensorOperation.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/DelaySensorOperation.java
@@ -50,8 +50,8 @@
      * {@inheritDoc}
      */
     @Override
-    public void execute() {
-        sleep(mDelay, mTimeUnit);
+    public void execute() throws InterruptedException {
+        SensorCtsHelper.sleep(mDelay, mTimeUnit);
         mOperation.execute();
     }
 
@@ -70,11 +70,4 @@
     public DelaySensorOperation clone() {
         return new DelaySensorOperation(mOperation.clone(), mDelay, mTimeUnit);
     }
-
-    /**
-     * Helper method to sleep for a given number of ns. Exposed for unit testing.
-     */
-    void sleep(long delay, TimeUnit timeUnit) {
-        SensorCtsHelper.sleep(delay, timeUnit);
-    }
 }
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/ISensorOperation.java b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/ISensorOperation.java
index 4ae56ea..62a4e9e 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/ISensorOperation.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/ISensorOperation.java
@@ -38,8 +38,12 @@
     /**
      * Executes the sensor operation.  This may throw {@link RuntimeException}s such as
      * {@link AssertionError}s.
+     *
+     * NOTE: the operation is expected to handle interruption by:
+     * - cleaning up on {@link InterruptedException}
+     * - propagating the exception down the stack
      */
-    public void execute();
+    public void execute() throws InterruptedException;
 
     /**
      * Get the stats for the operation.
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/ParallelSensorOperation.java b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/ParallelSensorOperation.java
index 4cca428..5a4466c 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/ParallelSensorOperation.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/ParallelSensorOperation.java
@@ -16,15 +16,21 @@
 
 package android.hardware.cts.helpers.sensoroperations;
 
-import android.hardware.cts.helpers.SensorStats;
-import android.util.Log;
-
 import junit.framework.Assert;
 
+import android.hardware.cts.helpers.SensorStats;
+import android.os.SystemClock;
+
 import java.util.ArrayList;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 
 /**
  * A {@link ISensorOperation} that executes a set of children {@link ISensorOperation}s in parallel.
@@ -34,9 +40,6 @@
 public class ParallelSensorOperation extends AbstractSensorOperation {
     public static final String STATS_TAG = "parallel";
 
-    private static final String TAG = "ParallelSensorOperation";
-    private static final int NANOS_PER_MILLI = 1000000;
-
     private final List<ISensorOperation> mOperations = new LinkedList<ISensorOperation>();
     private final Long mTimeout;
     private final TimeUnit mTimeUnit;
@@ -44,6 +47,7 @@
     /**
      * Constructor for the {@link ParallelSensorOperation} without a timeout.
      */
+    // TODO: sensor tests must always provide a timeout to prevent tests from running forever
     public ParallelSensorOperation() {
         mTimeout = null;
         mTimeUnit = null;
@@ -77,57 +81,67 @@
      * operations, the first exception will be thrown once all operations are completed.
      */
     @Override
-    public void execute() {
-        Long timeoutTimeNs = null;
-        if (mTimeout != null && mTimeUnit != null) {
-            timeoutTimeNs = System.nanoTime() + TimeUnit.NANOSECONDS.convert(mTimeout, mTimeUnit);
-        }
+    public void execute() throws InterruptedException {
+        int operationsCount = mOperations.size();
+        ThreadPoolExecutor executor = new ThreadPoolExecutor(
+                operationsCount,
+                operationsCount,
+                1 /* keepAliveTime */,
+                TimeUnit.SECONDS,
+                new LinkedBlockingQueue<Runnable>());
+        executor.allowCoreThreadTimeOut(true);
+        executor.prestartAllCoreThreads();
 
-        List<OperationThread> threadPool = new ArrayList<OperationThread>(mOperations.size());
+        ArrayList<Future<ISensorOperation>> futures = new ArrayList<Future<ISensorOperation>>();
         for (final ISensorOperation operation : mOperations) {
-            OperationThread thread = new OperationThread(operation);
-            thread.start();
-            threadPool.add(thread);
+            Future<ISensorOperation> future = executor.submit(new Callable<ISensorOperation>() {
+                @Override
+                public ISensorOperation call() throws Exception {
+                    operation.execute();
+                    return operation;
+                }
+            });
+            futures.add(future);
         }
 
-        List<Integer> timeoutIndices = new ArrayList<Integer>();
-        List<OperationExceptionInfo> exceptions = new ArrayList<OperationExceptionInfo>();
-        Throwable earliestException = null;
-        Long earliestExceptionTime = null;
+        Long executionTimeNs = null;
+        if (mTimeout != null) {
+            executionTimeNs = SystemClock.elapsedRealtimeNanos()
+                    + TimeUnit.NANOSECONDS.convert(mTimeout, mTimeUnit);
+        }
 
-        for (int i = 0; i < threadPool.size(); i++) {
-            OperationThread thread = threadPool.get(i);
-            join(thread, timeoutTimeNs);
-            if (thread.isAlive()) {
+        boolean hasAssertionErrors = false;
+        ArrayList<Integer> timeoutIndices = new ArrayList<Integer>();
+        ArrayList<Throwable> exceptions = new ArrayList<Throwable>();
+        for (int i = 0; i < operationsCount; ++i) {
+            Future<ISensorOperation> future = futures.get(i);
+            try {
+                ISensorOperation operation = getFutureResult(future, executionTimeNs);
+                addSensorStats(STATS_TAG, i, operation.getStats());
+            } catch (ExecutionException e) {
+                // extract the exception thrown by the worker thread
+                Throwable cause = e.getCause();
+                hasAssertionErrors |= (cause instanceof AssertionError);
+                exceptions.add(e.getCause());
+                addSensorStats(STATS_TAG, i, mOperations.get(i).getStats());
+            } catch (TimeoutException e) {
+                // we log, but we also need to interrupt the operation to terminate cleanly
                 timeoutIndices.add(i);
-                thread.interrupt();
+                future.cancel(true /* mayInterruptIfRunning */);
+            } catch (InterruptedException e) {
+                // clean-up after ourselves by interrupting all the worker threads, and propagate
+                // the interruption status, so we stop the outer loop as well
+                executor.shutdownNow();
+                throw e;
             }
-
-            Throwable exception = thread.getException();
-            Long exceptionTime = thread.getExceptionTime();
-            if (exception != null && exceptionTime != null) {
-                if (exception instanceof AssertionError) {
-                    exceptions.add(new OperationExceptionInfo(i, (AssertionError) exception));
-                }
-                if (earliestExceptionTime == null || exceptionTime < earliestExceptionTime) {
-                    earliestException = exception;
-                    earliestExceptionTime = exceptionTime;
-                }
-            }
-
-            addSensorStats(STATS_TAG, i, thread.getSensorOperation().getStats());
         }
 
-        if (earliestException == null) {
-            if (timeoutIndices.size() > 0) {
-                Assert.fail(getTimeoutMessage(timeoutIndices));
-            }
-        } else if (earliestException instanceof AssertionError) {
-            String msg = getExceptionMessage(exceptions, timeoutIndices);
-            getStats().addValue(SensorStats.ERROR, msg);
-            throw new AssertionError(msg, earliestException);
-        } else if (earliestException instanceof RuntimeException) {
-            throw (RuntimeException) earliestException;
+        String summary = getSummaryMessage(exceptions, timeoutIndices);
+        if (hasAssertionErrors) {
+            getStats().addValue(SensorStats.ERROR, summary);
+        }
+        if (!exceptions.isEmpty() || !timeoutIndices.isEmpty()) {
+            Assert.fail(summary);
         }
     }
 
@@ -144,114 +158,39 @@
     }
 
     /**
-     * Helper method that joins a thread at a given time in the future.
+     * Helper method that waits for a {@link Future} to complete, and returns its result.
      */
-    private void join(Thread thread, Long timeoutTimeNs) {
-        try {
-            if (timeoutTimeNs == null) {
-                thread.join();
-            } else {
-                // Cap wait time to 1ns so that join doesn't block indefinitely.
-                long waitTimeNs = Math.max(timeoutTimeNs - System.nanoTime(), 1);
-                thread.join(waitTimeNs / NANOS_PER_MILLI, (int) waitTimeNs % NANOS_PER_MILLI);
-            }
-        } catch (InterruptedException e) {
-            // Log and ignore
-            Log.w(TAG, "Thread interrupted during join, operations may timeout before expected"
-                    + " time");
+    private ISensorOperation getFutureResult(Future<ISensorOperation> future, Long timeoutNs)
+            throws ExecutionException, TimeoutException, InterruptedException {
+        if (timeoutNs == null) {
+            return future.get();
         }
+        // cap timeout to 1ns so that join doesn't block indefinitely
+        long waitTimeNs = Math.max(timeoutNs - SystemClock.elapsedRealtimeNanos(), 1);
+        return future.get(waitTimeNs, TimeUnit.NANOSECONDS);
     }
 
     /**
-     * Helper method for joining the exception messages used in assertions.
+     * Helper method for joining the exception and timeout messages used in assertions.
      */
-    private String getExceptionMessage(List<OperationExceptionInfo> exceptions,
-            List<Integer> timeoutIndices) {
+    private String getSummaryMessage(List<Throwable> exceptions, List<Integer> timeoutIndices) {
         StringBuilder sb = new StringBuilder();
-        sb.append(exceptions.get(0).toString());
-        for (int i = 1; i < exceptions.size(); i++) {
-            sb.append(", ").append(exceptions.get(i).toString());
-        }
-        if (timeoutIndices.size() > 0) {
-            sb.append(", ").append(getTimeoutMessage(timeoutIndices));
-        }
-        return sb.toString();
-    }
-
-    /**
-     * Helper method for formatting the operation timed out message used in assertions
-     */
-    private String getTimeoutMessage(List<Integer> indices) {
-        StringBuilder sb = new StringBuilder();
-        sb.append("Operation");
-        if (indices.size() != 1) {
-            sb.append("s");
-        }
-        sb.append(" ").append(indices.get(0));
-        for (int i = 1; i < indices.size(); i++) {
-            sb.append(", ").append(indices.get(i));
-        }
-        sb.append(" timed out");
-        return sb.toString();
-    }
-
-    /**
-     * Helper class for holding operation index and exception
-     */
-    private class OperationExceptionInfo {
-        private final int mIndex;
-        private final AssertionError mException;
-
-        public OperationExceptionInfo(int index, AssertionError exception) {
-            mIndex = index;
-            mException = exception;
+        for (Throwable exception : exceptions) {
+            sb.append(exception.toString()).append(", ");
         }
 
-        @Override
-        public String toString() {
-            return String.format("Operation %d failed: \"%s\"", mIndex, mException.getMessage());
-        }
-    }
-
-    /**
-     * Helper class to run the {@link ISensorOperation} in its own thread.
-     */
-    private class OperationThread extends Thread {
-        final private ISensorOperation mOperation;
-        private Throwable mException = null;
-        private Long mExceptionTime = null;
-
-        public OperationThread(ISensorOperation operation) {
-            mOperation = operation;
-        }
-
-        /**
-         * Run the thread catching {@link RuntimeException}s and {@link AssertionError}s and
-         * the time it happened.
-         */
-        @Override
-        public void run() {
-            try {
-                mOperation.execute();
-            } catch (AssertionError e) {
-                mExceptionTime = System.nanoTime();
-                mException = e;
-            } catch (RuntimeException e) {
-                mExceptionTime = System.nanoTime();
-                mException = e;
+        if (!timeoutIndices.isEmpty()) {
+            sb.append("Operation");
+            if (timeoutIndices.size() != 1) {
+                sb.append("s");
             }
+            sb.append(" [");
+            for (Integer index : timeoutIndices) {
+                sb.append(index).append(", ");
+            }
+            sb.append("] timed out");
         }
 
-        public ISensorOperation getSensorOperation() {
-            return mOperation;
-        }
-
-        public Throwable getException() {
-            return mException;
-        }
-
-        public Long getExceptionTime() {
-            return mExceptionTime;
-        }
+        return sb.toString();
     }
 }
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/RepeatingSensorOperation.java b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/RepeatingSensorOperation.java
index 5e023e5..3d682fe 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/RepeatingSensorOperation.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/RepeatingSensorOperation.java
@@ -48,7 +48,7 @@
      * in one iterations, it is thrown and all subsequent iterations will not run.
      */
     @Override
-    public void execute() {
+    public void execute() throws InterruptedException {
         for(int i = 0; i < mIterations; ++i) {
             ISensorOperation operation = mOperation.clone();
             try {
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/SensorOperationTest.java b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/SensorOperationTest.java
index 15b6978..bc48725 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/SensorOperationTest.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/SensorOperationTest.java
@@ -35,7 +35,7 @@
      * Test that the {@link FakeSensorOperation} functions correctly. Other tests in this class
      * rely on this operation.
      */
-    public void testFakeSensorOperation() {
+    public void testFakeSensorOperation() throws InterruptedException {
         final int opDurationMs = 100;
 
         ISensorOperation op = new FakeSensorOperation(opDurationMs, TimeUnit.MILLISECONDS);
@@ -60,7 +60,7 @@
     /**
      * Test that the {@link DelaySensorOperation} functions correctly.
      */
-    public void testDelaySensorOperation() {
+    public void testDelaySensorOperation() throws InterruptedException {
         final int opDurationMs = 500;
         final int subOpDurationMs = 100;
 
@@ -77,7 +77,7 @@
     /**
      * Test that the {@link ParallelSensorOperation} functions correctly.
      */
-    public void testParallelSensorOperation() {
+    public void testParallelSensorOperation() throws InterruptedException {
         final int subOpCount = 100;
         final int subOpDurationMs = 500;
 
@@ -118,7 +118,7 @@
      * Test that the {@link ParallelSensorOperation} functions correctly if there is a failure in
      * a child operation.
      */
-    public void testParallelSensorOperation_fail() {
+    public void testParallelSensorOperation_fail() throws InterruptedException {
         final int subOpCount = 100;
 
         ParallelSensorOperation op = new ParallelSensorOperation();
@@ -158,7 +158,7 @@
      * Test that the {@link ParallelSensorOperation} functions correctly if a child exceeds the
      * timeout.
      */
-    public void testParallelSensorOperation_timeout() {
+    public void testParallelSensorOperation_timeout() throws InterruptedException {
         final int subOpCount = 100;
 
         ParallelSensorOperation op = new ParallelSensorOperation(1, TimeUnit.SECONDS);
@@ -192,7 +192,7 @@
     /**
      * Test that the {@link RepeatingSensorOperation} functions correctly.
      */
-    public void testRepeatingSensorOperation() {
+    public void testRepeatingSensorOperation() throws InterruptedException {
         final int iterations = 10;
         final int subOpDurationMs = 100;
 
@@ -219,7 +219,7 @@
      * Test that the {@link RepeatingSensorOperation} functions correctly if there is a failure in
      * a child operation.
      */
-    public void testRepeatingSensorOperation_fail() {
+    public void testRepeatingSensorOperation_fail() throws InterruptedException {
         final int iterations = 100;
         final int failCount = 75;
 
@@ -277,7 +277,7 @@
     /**
      * Test that the {@link SequentialSensorOperation} functions correctly.
      */
-    public void testSequentialSensorOperation() {
+    public void testSequentialSensorOperation() throws InterruptedException {
         final int subOpCount = 10;
         final int subOpDurationMs = 100;
 
@@ -308,7 +308,7 @@
      * Test that the {@link SequentialSensorOperation} functions correctly if there is a failure in
      * a child operation.
      */
-    public void testSequentialSensorOperation_fail() {
+    public void testSequentialSensorOperation_fail() throws InterruptedException {
         final int subOpCount = 100;
         final int failCount = 75;
 
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/SequentialSensorOperation.java b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/SequentialSensorOperation.java
index 050a8f6..2ed0ca6 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/SequentialSensorOperation.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/SequentialSensorOperation.java
@@ -48,7 +48,7 @@
      * in one operation, it is thrown and all subsequent operations will not run.
      */
     @Override
-    public void execute() {
+    public void execute() throws InterruptedException {
         for (int i = 0; i < mOperations.size(); i++) {
             ISensorOperation operation = mOperations.get(i);
             try {
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/TestSensorFlushOperation.java b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/TestSensorFlushOperation.java
index 1fb6bef..d5aa4b9 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/TestSensorFlushOperation.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/TestSensorFlushOperation.java
@@ -53,7 +53,7 @@
      * {@inheritDoc}
      */
     @Override
-    protected void doExecute(TestSensorEventListener listener) {
+    protected void doExecute(TestSensorEventListener listener) throws InterruptedException {
         mSensorManager.runSensorAndFlush(listener, mDuration, mTimeUnit);
     }
 
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/TestSensorOperation.java b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/TestSensorOperation.java
index 6c3851e..695e1a7 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/TestSensorOperation.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/TestSensorOperation.java
@@ -76,7 +76,7 @@
      * {@inheritDoc}
      */
     @Override
-    protected void doExecute(TestSensorEventListener listener) {
+    protected void doExecute(TestSensorEventListener listener) throws InterruptedException {
         if (mEventCount != null) {
             mSensorManager.runSensor(listener, mEventCount);
         } else {
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/VerifiableSensorOperation.java b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/VerifiableSensorOperation.java
index c635a75..57018eb 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/VerifiableSensorOperation.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/VerifiableSensorOperation.java
@@ -94,7 +94,7 @@
      * Collect the specified number of events from the sensor and run all enabled verifications.
      */
     @Override
-    public void execute() {
+    public void execute() throws InterruptedException {
         getStats().addValue("sensor_name", mEnvironment.getSensor().getName());
 
         ValidatingSensorEventListener listener = new ValidatingSensorEventListener(mVerifications);
@@ -131,7 +131,7 @@
     /**
      * Execute operations in a {@link TestSensorManager}.
      */
-    protected abstract void doExecute(TestSensorEventListener listener);
+    protected abstract void doExecute(TestSensorEventListener listener) throws InterruptedException;
 
     /**
      * Clone the subclass operation.
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/WakeLockOperation.java b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/WakeLockOperation.java
index 73da9c9..b500ea7 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/WakeLockOperation.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/WakeLockOperation.java
@@ -59,7 +59,7 @@
      * {@inheritDoc}
      */
     @Override
-    public void execute() {
+    public void execute() throws InterruptedException {
         PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
         WakeLock wakeLock = pm.newWakeLock(mWakelockFlags, TAG);