Cherrypick the following CLs from LMP MR1

Cherrypick CLs from lmp-mr1 and fixed a typo in method name in
android.hardware.cts.SensorTest.

List of the CLs:

  Move timestamp synchronization test cases to sensor test platform.
  b/17643223

  - Improves the logging of timestamp synchronization verifications
  - Refactor batching tests to use sensor test platform

  Original-Change-Id: Ic6924d5c794ddcf646f618f446cbda11b938d184

  Refactor Sensor test platform Operations.
  b/17838681

  The refactoring is required in preparation to add support to save sensor events upon failure.

  Original-Change-Id: I13ed41e3fa5393c61ccee766f0fdb9f4fd6aadd5

  Fix handling of Sensor test platform exceptions.
  b/18705736

  Original-Change-Id: I82f4a4f18b52ff3ea263b3ff9f063e1c5459eca5

  Refactor sensor test platform listeners.
  b/17838681

  The refactoring is required in preparation to add support to save sensor events upon failure.

  Original-Change-Id: I5540451d70426fbbbe19a10b2a727b2e010c3838

  Refactor SensorStats.
  b/17838681

  The refactoring is required in preparation to add support to save sensor events upon failure.

  Original-Change-Id: I4661647b13296912fe89f5b20e01dad12553f72d

  Save all sensor events when a verification fails.
  b/17838681

  It also prepares the sensor test platform for logging and maintaining a hierarchy of elements.
  Most of the changes are only required to propagate the hierarchy information down to all the
  elements of the sensor test platform.

  Original-Change-Id: I08f750cfb426502240db584b5a1209481856c13c

Change-Id: Ia68a7d9952b7ac7bfd100cdf70dace494b17e7b3
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/AccelerometerMeasurementTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/AccelerometerMeasurementTestActivity.java
index dfcf120..52b3dee 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/AccelerometerMeasurementTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/AccelerometerMeasurementTestActivity.java
@@ -97,11 +97,11 @@
                 Sensor.TYPE_ACCELEROMETER,
                 SensorManager.SENSOR_DELAY_FASTEST);
         TestSensorOperation verifyMeasurements =
-                new TestSensorOperation(environment, 100 /* event count */);
+                TestSensorOperation.createOperation(environment, 100 /* event count */);
         verifyMeasurements.addVerification(new MeanVerification(
                 expectations,
                 new float[]{1.95f, 1.95f, 1.95f} /* m / s^2 */));
-        verifyMeasurements.execute();
+        verifyMeasurements.execute(getCurrentTestNode());
         return null;
     }
 
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 6f0a7aa..7ef63d7 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/BatchingTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/BatchingTestActivity.java
@@ -22,9 +22,7 @@
 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 java.util.concurrent.TimeUnit;
 
@@ -128,7 +126,7 @@
 
         int testDurationSec = maxBatchReportLatencySec + BATCHING_PADDING_TIME_S;
         TestSensorOperation operation =
-                new TestSensorOperation(environment, testDurationSec,TimeUnit.SECONDS);
+                TestSensorOperation.createOperation(environment, testDurationSec,TimeUnit.SECONDS);
         return executeTest(operation);
     }
 
@@ -145,15 +143,14 @@
                 maxBatchReportLatencyUs);
 
         int flushDurationSec = maxBatchReportLatencySec / 2;
-        TestSensorFlushOperation operation =
-                new TestSensorFlushOperation(environment, flushDurationSec, TimeUnit.SECONDS);
+        TestSensorOperation operation = TestSensorOperation
+                .createFlushOperation(environment, flushDurationSec, TimeUnit.SECONDS);
         return executeTest(operation);
     }
 
-    private String executeTest(VerifiableSensorOperation operation) throws InterruptedException {
+    private String executeTest(TestSensorOperation operation) throws InterruptedException {
         operation.addDefaultVerifications();
-        operation.setLogEvents(true);
-        operation.execute();
+        operation.execute(getCurrentTestNode());
         return null;
     }
 }
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 4b2a7f4..7be0fb1 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/GyroscopeMeasurementTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/GyroscopeMeasurementTestActivity.java
@@ -156,8 +156,8 @@
                 getApplicationContext(),
                 Sensor.TYPE_GYROSCOPE,
                 SensorManager.SENSOR_DELAY_FASTEST);
-        TestSensorOperation sensorOperation =
-                new TestSensorOperation(environment, ROTATION_COLLECTION_SEC, TimeUnit.SECONDS);
+        TestSensorOperation sensorOperation = TestSensorOperation
+                .createOperation(environment, ROTATION_COLLECTION_SEC, TimeUnit.SECONDS);
 
         int gyroscopeAxes = environment.getSensorAxesCount();
         int[] expectationsDeg = getExpectationsDeg(gyroscopeAxes, rotationAxis, expectationDeg);
@@ -167,7 +167,7 @@
         sensorOperation.addVerification(integrationVerification);
 
         try {
-            sensorOperation.execute();
+            sensorOperation.execute(getCurrentTestNode());
         } finally {
             playSound();
         }
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 553147b..229a9dc 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/MagneticFieldMeasurementTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/MagneticFieldMeasurementTestActivity.java
@@ -21,7 +21,6 @@
 
 import android.hardware.Sensor;
 import android.hardware.SensorEvent;
-import android.hardware.SensorEventListener2;
 import android.hardware.SensorManager;
 import android.hardware.cts.helpers.SensorCalibratedUncalibratedVerifier;
 import android.hardware.cts.helpers.SensorCtsHelper;
@@ -79,7 +78,7 @@
                 Sensor.TYPE_MAGNETIC_FIELD,
                 SensorManager.SENSOR_DELAY_FASTEST);
         TestSensorOperation verifyNorm =
-                new TestSensorOperation(environment, 100 /* event count */);
+                TestSensorOperation.createOperation(environment, 100 /* event count */);
 
         float expectedMagneticFieldEarth =
                 (SensorManager.MAGNETIC_FIELD_EARTH_MAX + SensorManager.MAGNETIC_FIELD_EARTH_MIN) / 2;
@@ -88,7 +87,7 @@
         verifyNorm.addVerification(new MagnitudeVerification(
                 expectedMagneticFieldEarth,
                 magneticFieldEarthThreshold));
-        verifyNorm.execute();
+        verifyNorm.execute(getCurrentTestNode());
         return null;
     }
 
@@ -124,11 +123,11 @@
                 Sensor.TYPE_MAGNETIC_FIELD,
                 SensorManager.SENSOR_DELAY_FASTEST);
         TestSensorOperation verifyStdDev =
-                new TestSensorOperation(environment, 100 /* event count */);
+                TestSensorOperation.createOperation(environment, 100 /* event count */);
 
         verifyStdDev.addVerification(new StandardDeviationVerification(
                 new float[]{2f, 2f, 2f} /* uT */));
-        verifyStdDev.execute();
+        verifyStdDev.execute(getCurrentTestNode());
         return null;
     }
 
@@ -166,7 +165,12 @@
      * A routine to help operators calibrate the magnetometer.
      */
     private void calibrateMagnetometer() throws InterruptedException {
-        SensorEventListener2 listener = new SensorEventListener2() {
+        TestSensorEnvironment environment = new TestSensorEnvironment(
+                getApplicationContext(),
+                Sensor.TYPE_MAGNETIC_FIELD,
+                SensorManager.SENSOR_DELAY_NORMAL);
+
+        TestSensorEventListener listener = new TestSensorEventListener(environment) {
             @Override
             public void onSensorChanged(SensorEvent event) {
                 clearText();
@@ -184,21 +188,11 @@
                 // TODO: automate finding out when the magnetometer is calibrated
                 logger.logInstructions(R.string.snsr_mag_calibration_complete);
             }
-
-            @Override
-            public void onAccuracyChanged(Sensor sensor, int accuracy) {}
-
-            @Override
-            public void onFlushCompleted(Sensor sensor) {}
         };
 
-        TestSensorEnvironment environment = new TestSensorEnvironment(
-                getApplicationContext(),
-                Sensor.TYPE_MAGNETIC_FIELD,
-                SensorManager.SENSOR_DELAY_NORMAL);
         TestSensorManager magnetometer = new TestSensorManager(environment);
         try {
-            magnetometer.registerListener(new TestSensorEventListener(listener));
+            magnetometer.registerListener(listener);
             waitForUserToContinue();
         } finally {
             magnetometer.unregisterListener();
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 5bbaaf7..380b282 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
@@ -26,6 +26,8 @@
 
 import android.content.Context;
 import android.hardware.cts.SensorTestCase;
+import android.hardware.cts.helpers.SensorTestPlatformException;
+import android.hardware.cts.helpers.reporting.ISensorTestNode;
 
 import java.util.Enumeration;
 
@@ -138,10 +140,24 @@
             SensorTestCase sensorTestCase = (SensorTestCase) testCase;
             sensorTestCase.setContext(mContext);
             sensorTestCase.setEmulateSensorUnderLoad(false);
+            sensorTestCase.setCurrentTestNode(new TestNode(testCase));
             // TODO: set delayed assertion provider
         } else {
             throw new IllegalStateException("TestCase must be an instance of SensorTestCase.");
         }
         super.run(testCase);
     }
+
+    private class TestNode implements ISensorTestNode {
+        private final TestCase mTestCase;
+
+        public TestNode(TestCase testCase) {
+            mTestCase = testCase;
+        }
+
+        @Override
+        public String getName() throws SensorTestPlatformException {
+            return mTestCase.getClass().getSimpleName() + "_" + mTestCase.getName();
+        }
+    }
 }
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 a88abd0..8cf287a 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,6 +19,8 @@
 
 import com.android.cts.verifier.sensors.reporting.SensorTestDetails;
 
+import android.hardware.cts.helpers.reporting.ISensorTestNode;
+
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
@@ -35,6 +37,7 @@
     private volatile int mTestPassedCounter;
     private volatile int mTestSkippedCounter;
     private volatile int mTestFailedCounter;
+    private volatile ISensorTestNode mCurrentTestNode;
 
     /**
      * {@inheritDoc}
@@ -63,8 +66,12 @@
                 mTestFailedCounter);
     }
 
+    protected ISensorTestNode getCurrentTestNode() {
+        return mCurrentTestNode;
+    }
+
     private List<Method> findTestMethods() {
-        ArrayList<Method> testMethods = new ArrayList<Method>();
+        ArrayList<Method> testMethods = new ArrayList<>();
         for (Method method : mTestClass.getDeclaredMethods()) {
             if (Modifier.isPublic(method.getModifiers())
                     && method.getParameterTypes().length == 0
@@ -79,6 +86,7 @@
     private SensorTestDetails executeTest(Method testMethod) throws InterruptedException {
         String testMethodName = testMethod.getName();
         String testName = String.format("%s#%s", getTestClassName(), testMethodName);
+        mCurrentTestNode = new TestNode(testMethod);
 
         SensorTestDetails testDetails;
         try {
@@ -112,4 +120,17 @@
 
         return testDetails;
     }
+
+    private class TestNode implements ISensorTestNode {
+        private final Method mTestMethod;
+
+        public TestNode(Method testMethod) {
+            mTestMethod = testMethod;
+        }
+
+        @Override
+        public String getName() {
+            return mTestClass.getSimpleName() + "_" + mTestMethod.getName();
+        }
+    }
 }
diff --git a/tests/tests/hardware/src/android/hardware/cts/SensorBatchingTests.java b/tests/tests/hardware/src/android/hardware/cts/SensorBatchingTests.java
index 687826c..7640cd7 100644
--- a/tests/tests/hardware/src/android/hardware/cts/SensorBatchingTests.java
+++ b/tests/tests/hardware/src/android/hardware/cts/SensorBatchingTests.java
@@ -20,9 +20,7 @@
 import android.hardware.SensorManager;
 import android.hardware.cts.helpers.SensorStats;
 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.hardware.cts.helpers.sensorverification.ISensorVerification;
 
 import java.util.concurrent.TimeUnit;
@@ -263,7 +261,7 @@
                 rateUs,
                 maxBatchReportLatencyUs);
         TestSensorOperation operation =
-                new TestSensorOperation(environment, testDurationSec, TimeUnit.SECONDS);
+                TestSensorOperation.createOperation(environment, testDurationSec, TimeUnit.SECONDS);
 
         executeTest(environment, operation, false /* flushExpected */);
     }
@@ -279,23 +277,23 @@
                 shouldEmulateSensorUnderLoad(),
                 rateUs,
                 maxBatchReportLatencyUs);
-        TestSensorFlushOperation operation =
-                new TestSensorFlushOperation(environment, flushDurationSec, TimeUnit.SECONDS);
+        TestSensorOperation operation = TestSensorOperation
+                .createFlushOperation(environment, flushDurationSec, TimeUnit.SECONDS);
 
         executeTest(environment, operation, true /* flushExpected */);
     }
 
     private void executeTest(
             TestSensorEnvironment environment,
-            VerifiableSensorOperation operation,
+            TestSensorOperation operation,
             boolean flushExpected) throws Throwable {
         operation.addDefaultVerifications();
-        operation.setLogEvents(true);
 
         try {
-            operation.execute();
+            operation.execute(getCurrentTestNode());
         } finally {
-            SensorStats.logStats(TAG, operation.getStats());
+            SensorStats stats = operation.getStats();
+            stats.log(TAG);
 
             String sensorRate;
             if (environment.getRequestedSamplingPeriodUs() == SensorManager.SENSOR_DELAY_FASTEST) {
@@ -311,7 +309,7 @@
                     sensorRate,
                     batching,
                     flush);
-            SensorStats.logStatsToFile(fileName, operation.getStats());
+            stats.logToFile(fileName);
         }
     }
 }
diff --git a/tests/tests/hardware/src/android/hardware/cts/SensorIntegrationTests.java b/tests/tests/hardware/src/android/hardware/cts/SensorIntegrationTests.java
index 50cb12d..8c3fb7a 100644
--- a/tests/tests/hardware/src/android/hardware/cts/SensorIntegrationTests.java
+++ b/tests/tests/hardware/src/android/hardware/cts/SensorIntegrationTests.java
@@ -18,13 +18,11 @@
 import android.content.Context;
 import android.hardware.Sensor;
 import android.hardware.SensorManager;
-import android.hardware.cts.helpers.SensorStats;
 import android.hardware.cts.helpers.TestSensorEnvironment;
 import android.hardware.cts.helpers.sensoroperations.ParallelSensorOperation;
 import android.hardware.cts.helpers.sensoroperations.RepeatingSensorOperation;
 import android.hardware.cts.helpers.sensoroperations.SequentialSensorOperation;
 import android.hardware.cts.helpers.sensoroperations.TestSensorOperation;
-import android.hardware.cts.helpers.sensoroperations.VerifiableSensorOperation;
 import android.hardware.cts.helpers.sensorverification.EventOrderingVerification;
 
 import java.util.Random;
@@ -81,7 +79,7 @@
                     shouldEmulateSensorUnderLoad(),
                     SensorManager.SENSOR_DELAY_FASTEST);
             TestSensorOperation continuousOperation =
-                    new TestSensorOperation(environment, 100 /* eventCount */);
+                    TestSensorOperation.createOperation(environment, 100 /* eventCount */);
             continuousOperation.addVerification(new EventOrderingVerification());
             operation.add(new RepeatingSensorOperation(continuousOperation, ITERATIONS));
 
@@ -93,12 +91,12 @@
                     sensor.getMinDelay(),
                     MAX_REPORTING_LATENCY_US);
             TestSensorOperation batchingOperation =
-                    new TestSensorOperation(batchingEnvironment, 100 /* eventCount */);
+                    TestSensorOperation.createOperation(batchingEnvironment, 100 /* eventCount */);
             batchingOperation.addVerification(new EventOrderingVerification());
             operation.add(new RepeatingSensorOperation(batchingOperation, ITERATIONS));
         }
-        operation.execute();
-        SensorStats.logStats(TAG, operation.getStats());
+        operation.execute(getCurrentTestNode());
+        operation.getStats().log(TAG);
     }
 
     /**
@@ -145,7 +143,7 @@
                             generateSamplingRateInUs(sensorType),
                             generateReportLatencyInUs());
                     TestSensorOperation sensorOperation =
-                            new TestSensorOperation(environment, 100 /* eventCount */);
+                            TestSensorOperation.createOperation(environment, 100 /* eventCount */);
                     sensorOperation.addVerification(new EventOrderingVerification());
                     sequentialOperation.add(sensorOperation);
                 }
@@ -153,8 +151,8 @@
             }
         }
 
-        operation.execute();
-        SensorStats.logStats(TAG, operation.getStats());
+        operation.execute(getCurrentTestNode());
+        operation.getStats().log(TAG);
     }
 
     /**
@@ -229,7 +227,7 @@
                 shouldEmulateSensorUnderLoad(),
                 SensorManager.SENSOR_DELAY_FASTEST);
         TestSensorOperation tester =
-                new TestSensorOperation(testerEnvironment, 100 /* event count */);
+                TestSensorOperation.createOperation(testerEnvironment, 100 /* event count */);
         tester.addVerification(new EventOrderingVerification());
 
         TestSensorEnvironment testeeEnvironment = new TestSensorEnvironment(
@@ -237,18 +235,18 @@
                 sensorTypeTestee,
                 shouldEmulateSensorUnderLoad(),
                 SensorManager.SENSOR_DELAY_FASTEST);
-        VerifiableSensorOperation testee =
-                new TestSensorOperation(testeeEnvironment, 100 /* event count */);
+        TestSensorOperation testee =
+                TestSensorOperation.createOperation(testeeEnvironment, 100 /* event count */);
         testee.addVerification(new EventOrderingVerification());
 
         ParallelSensorOperation operation = new ParallelSensorOperation();
         operation.add(tester, testee);
-        operation.execute();
-        SensorStats.logStats(TAG, operation.getStats());
+        operation.execute(getCurrentTestNode());
+        operation.getStats().log(TAG);
 
         testee = testee.clone();
-        testee.execute();
-        SensorStats.logStats(TAG, testee.getStats());
+        testee.execute(getCurrentTestNode());
+        testee.getStats().log(TAG);
     }
 
     /**
diff --git a/tests/tests/hardware/src/android/hardware/cts/SensorTest.java b/tests/tests/hardware/src/android/hardware/cts/SensorTest.java
index e8e0f252..90893ee 100644
--- a/tests/tests/hardware/src/android/hardware/cts/SensorTest.java
+++ b/tests/tests/hardware/src/android/hardware/cts/SensorTest.java
@@ -18,6 +18,8 @@
 
 import com.android.cts.util.TimeoutReq;
 
+import junit.framework.Assert;
+
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.hardware.Sensor;
@@ -27,6 +29,16 @@
 import android.hardware.SensorManager;
 import android.hardware.TriggerEvent;
 import android.hardware.TriggerEventListener;
+import android.hardware.cts.helpers.SensorNotSupportedException;
+import android.hardware.cts.helpers.SensorTestStateNotSupportedException;
+import android.hardware.cts.helpers.TestSensorEnvironment;
+import android.hardware.cts.helpers.TestSensorEventListener;
+import android.hardware.cts.helpers.TestSensorManager;
+import android.hardware.cts.helpers.sensoroperations.ParallelSensorOperation;
+import android.hardware.cts.helpers.sensoroperations.TestSensorOperation;
+import android.hardware.cts.helpers.sensorverification.EventGapVerification;
+import android.hardware.cts.helpers.sensorverification.EventOrderingVerification;
+import android.hardware.cts.helpers.sensorverification.EventTimestampSynchronizationVerification;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.PowerManager;
@@ -35,37 +47,52 @@
 
 import java.util.ArrayList;
 import java.util.List;
-import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
 public class SensorTest extends SensorTestCase {
-    private SensorManager mSensorManager;
-    private TriggerListener mTriggerListener;
-    private SensorListener mSensorListener;
-    private List<Sensor> mSensorList;
     private static final String TAG = "SensorTest";
+
     // Test only SDK defined sensors. Any sensors with type > 100 are ignored.
     private static final int MAX_OFFICIAL_ANDROID_SENSOR_TYPE = 100;
-    private static final long TIMEOUT_TOLERANCE_US = TimeUnit.SECONDS.toMicros(5);
-    private static final double MIN_SAMPLING_FREQUENCY_MULTIPLIER_TOLERANCE = 0.9;
+
     private PowerManager.WakeLock mWakeLock;
+    private SensorManager mSensorManager;
+    private NullTriggerEventListener mNullTriggerEventListener;
+    private NullSensorEventListener mNullSensorEventListener;
+    private List<Sensor> mSensorList;
 
     @Override
     protected void setUp() throws Exception {
-        super.setUp();
-        mSensorManager = (SensorManager) getContext().getSystemService(Context.SENSOR_SERVICE);
-        mTriggerListener = new TriggerListener();
-        mSensorListener = new SensorListener();
-        mSensorList = mSensorManager.getSensorList(Sensor.TYPE_ALL);
-        PowerManager pm = (PowerManager) getContext().getSystemService(Context.POWER_SERVICE);
+        Context context = getContext();
+        PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
         mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
+
+        mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
+        mNullTriggerEventListener = new NullTriggerEventListener();
+        mNullSensorEventListener = new NullSensorEventListener();
+
+        mSensorList = mSensorManager.getSensorList(Sensor.TYPE_ALL);
+        assertNotNull("SensorList was null.", mSensorList);
+        if (mSensorList.isEmpty()) {
+            // several devices will not have sensors, so we need to skip the tests in those cases
+            throw new SensorTestStateNotSupportedException(
+                    "Sensors are not available in the system.");
+        }
+
+        mWakeLock.acquire();
     }
 
+    @Override
+    protected void tearDown(){
+        if (mWakeLock != null && mWakeLock.isHeld()) {
+            mWakeLock.release();
+        }
+    }
+
+    @SuppressWarnings("deprecation")
     public void testSensorOperations() {
         // Because we can't know every sensors unit details, so we can't assert
         // get values with specified values.
-        List<Sensor> sensors = mSensorManager.getSensorList(Sensor.TYPE_ALL);
-        assertNotNull(sensors);
         Sensor sensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
         boolean hasAccelerometer = getContext().getPackageManager().hasSystemFeature(
                 PackageManager.FEATURE_SENSOR_ACCELEROMETER);
@@ -150,7 +177,6 @@
         }
         assertTrue(sensors.get(0).getName() + " defined as non-wake-up sensor",
                 sensors.get(0).isWakeUpSensor());
-        return;
     }
 
     // Some sensors like proximity, significant motion etc. are defined as wake-up sensors by
@@ -208,371 +234,137 @@
 
     public void testRequestTriggerWithNonTriggerSensor() {
         Sensor sensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
-        boolean result;
-        if (sensor != null) {
-            result = mSensorManager.requestTriggerSensor(mTriggerListener, sensor);
-            assertFalse(result);
+        if (sensor == null) {
+            throw new SensorNotSupportedException(Sensor.TYPE_ACCELEROMETER);
         }
+        boolean  result = mSensorManager.requestTriggerSensor(mNullTriggerEventListener, sensor);
+        assertFalse(result);
     }
 
     public void testCancelTriggerWithNonTriggerSensor() {
         Sensor sensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
-        boolean result;
-        if (sensor != null) {
-            result = mSensorManager.cancelTriggerSensor(mTriggerListener, sensor);
-            assertFalse(result);
+        if (sensor == null) {
+            throw new SensorNotSupportedException(Sensor.TYPE_ACCELEROMETER);
         }
+        boolean result = mSensorManager.cancelTriggerSensor(mNullTriggerEventListener, sensor);
+        assertFalse(result);
     }
 
     public void testRegisterWithTriggerSensor() {
         Sensor sensor = mSensorManager.getDefaultSensor(Sensor.TYPE_SIGNIFICANT_MOTION);
-        boolean result;
-        if (sensor != null) {
-            result = mSensorManager.registerListener(mSensorListener, sensor,
-                    SensorManager.SENSOR_DELAY_NORMAL);
-            assertFalse(result);
+        if (sensor == null) {
+            throw new SensorNotSupportedException(Sensor.TYPE_SIGNIFICANT_MOTION);
         }
+        boolean result = mSensorManager.registerListener(
+                mNullSensorEventListener,
+                sensor,
+                SensorManager.SENSOR_DELAY_NORMAL);
+        assertFalse(result);
     }
 
     public void testRegisterTwiceWithSameSensor() {
         Sensor sensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
-        boolean result;
-        if (sensor != null) {
-            result = mSensorManager.registerListener(mSensorListener, sensor,
-                    SensorManager.SENSOR_DELAY_NORMAL);
-            assertTrue(result);
-            result = mSensorManager.registerListener(mSensorListener, sensor,
-                    SensorManager.SENSOR_DELAY_NORMAL);
-            assertFalse(result);
+        if (sensor == null) {
+            throw new SensorNotSupportedException(Sensor.TYPE_ACCELEROMETER);
         }
+
+        boolean result = mSensorManager.registerListener(mNullSensorEventListener, sensor,
+                SensorManager.SENSOR_DELAY_NORMAL);
+        assertTrue(result);
+
+        result = mSensorManager.registerListener(mNullSensorEventListener, sensor,
+                SensorManager.SENSOR_DELAY_NORMAL);
+        assertFalse(result);
     }
 
-    class SensorEventTimeStampListener implements SensorEventListener {
-        SensorEventTimeStampListener(long eventReportLatencyNs, CountDownLatch latch) {
-            mEventReportLatencyNs = eventReportLatencyNs;
-            mPrevTimeStampNs = -1;
-            mLatch = latch;
-            numErrors = 0;
-        }
-
-        @Override
-        public void onSensorChanged(SensorEvent event) {
-            if (mPrevTimeStampNs == -1) {
-                mPrevTimeStampNs = event.timestamp;
-                return;
-            }
-            long currTimeStampNs = event.timestamp;
-            if (currTimeStampNs <= mPrevTimeStampNs) {
-                Log.w(TAG, "Timestamps not monotonically increasing curr_ts_ns=" +
-                        event.timestamp + " prev_ts_ns=" + mPrevTimeStampNs);
-                numErrors++;
-                mPrevTimeStampNs = currTimeStampNs;
-                return;
-            }
-            mLatch.countDown();
-
-            final long elapsedRealtimeNs = SystemClock.elapsedRealtimeNanos();
-
-            if (elapsedRealtimeNs <= currTimeStampNs) {
-                Log.w(TAG, "Timestamps into the future curr elapsedRealTimeNs=" + elapsedRealtimeNs
-                         + " current sensor ts_ns=" + currTimeStampNs);
-                ++numErrors;
-            } else if (elapsedRealtimeNs-currTimeStampNs > SYNC_TOLERANCE + mEventReportLatencyNs) {
-                Log.w(TAG, "Timestamp sync error elapsedRealTimeNs=" + elapsedRealtimeNs +
-                        " curr_ts_ns=" + currTimeStampNs +
-                        " diff_ns=" + (elapsedRealtimeNs - currTimeStampNs) +
-                        " SYNC_TOLERANCE_NS=" + SYNC_TOLERANCE +
-                        " eventReportLatencyNs=" + mEventReportLatencyNs);
-                ++numErrors;
-            }
-            mPrevTimeStampNs = currTimeStampNs;
-        }
-
-        public int getNumErrors() {
-            return numErrors;
-        }
-
-        @Override
-        public void onAccuracyChanged(Sensor sensor, int accuracy) {
-        }
-
-        private int numErrors;
-        private long mEventReportLatencyNs;
-        private long mPrevTimeStampNs;
-        private final CountDownLatch mLatch;
-        private final long SYNC_TOLERANCE = 500000000L; // 500 milli seconds approx.
-    }
-
-    // Register for each sensor and compare the timestamps of SensorEvents that you get with
-    // elapsedRealTimeNano.
+    // TODO: remove when parametized tests are supported and EventTimestampSynchronization
+    //       verification is added to default verifications
     @TimeoutReq(minutes=60)
     public void testSensorTimeStamps() throws Exception {
-        final int numEvents = 2000;
-        try {
-            mWakeLock.acquire();
-            int numErrors = 0;
-            for (Sensor sensor : mSensorList) {
-                // Skip OEM defined sensors and non continuous sensors.
-                if (sensor.getReportingMode() != Sensor.REPORTING_MODE_CONTINUOUS) {
-                    continue;
-                }
-
-                for (int iterations = 0; iterations < 2; ++iterations) {
-                    // Test in both batch mode and non-batch mode for every sensor.
-                    long maxBatchReportLatencyNs = 10000000000L; // 10 secs
-                    if (iterations % 2 == 0) maxBatchReportLatencyNs = 0;
-
-                    final long samplingPeriodNs = (long)(TimeUnit.MICROSECONDS.toNanos(
-                            sensor.getMinDelay())/MIN_SAMPLING_FREQUENCY_MULTIPLIER_TOLERANCE);
-                    // If there is a FIFO and a wake-lock is held, events will be reported when
-                    // the batch timeout expires or when the FIFO is full which ever occurs
-                    // earlier.
-                    final long eventReportLatencyNs = Math.min(maxBatchReportLatencyNs,
-                            sensor.getFifoMaxEventCount() * samplingPeriodNs);
-
-                    final CountDownLatch eventsRemaining = new CountDownLatch(numEvents);
-                    SensorEventTimeStampListener listener = new SensorEventTimeStampListener(
-                            eventReportLatencyNs, eventsRemaining);
-
-                    Log.i(TAG, "Running timeStamp test on " + sensor.getName());
-                    boolean result = mSensorManager.registerListener(listener, sensor,
-                            SensorManager.SENSOR_DELAY_FASTEST,
-                            (int)maxBatchReportLatencyNs/1000);
-                    assertTrue("Sensor registerListener failed ", result);
-
-                    long timeToWaitUs = samplingPeriodNs/1000 + eventReportLatencyNs/1000 +
-                            TIMEOUT_TOLERANCE_US;
-                    long totalTimeWaitedUs = waitToCollectAllEvents(timeToWaitUs,
-                            (int)(eventReportLatencyNs/1000), eventsRemaining);
-
-                    mSensorManager.unregisterListener(listener);
-                    if (eventsRemaining.getCount() > 0) {
-                        failTimedOut(sensor.getName(), (double) totalTimeWaitedUs/1000,
-                                numEvents, (double) sensor.getMinDelay()/1000,
-                                eventsRemaining.getCount(),
-                                numEvents - eventsRemaining.getCount());
-                    }
-                    if (listener.getNumErrors() > 5) {
-                        fail("Check logcat. Timestamp test failed. numErrors=" +
-                                listener.getNumErrors() + " " + sensor.getName() +
-                                " maxBatchReportLatencyNs=" + maxBatchReportLatencyNs +
-                                " samplingPeriodNs=" + sensor.getMinDelay());
-                        numErrors += listener.getNumErrors();
-                    } else {
-                        Log.i(TAG, "TimeStamp test PASS'd on " + sensor.getName());
-                    }
-                }
-            }
-        } finally {
-            mWakeLock.release();
+        ArrayList<Throwable> errorsFound = new ArrayList<>();
+        for (Sensor sensor : mSensorList) {
+            // test both continuous and batching mode sensors
+            verifyLongActivation(sensor, 0 /* maxReportLatencyUs */, errorsFound);
+            verifyLongActivation(sensor, (int) TimeUnit.SECONDS.toMicros(10), errorsFound);
         }
+        assertOnErrors(errorsFound);
     }
 
-    private void failTimedOut(String sensorName, double totalTimeWaitedMs, int numEvents,
-            double minDelayMs, long eventsRemaining, long eventsReceived) {
-        final String TIMED_OUT_FORMAT = "Timed out waiting for events from %s " +
-                "waited for time=%.1fms to receive totalEvents=%d at samplingRate=%.1fHz" +
-                " remainingEvents=%d  received events=%d";
-        fail(String.format(TIMED_OUT_FORMAT, sensorName, totalTimeWaitedMs, numEvents,
-                1000/minDelayMs, eventsRemaining, eventsReceived));
-    }
-
-    // Register for updates from each continuous mode sensor, wait for N events, call flush and
-    // wait for flushCompleteEvent before unregistering for the sensor.
+    // TODO: remove when parameterized tests are supported (see SensorBatchingTests.java)
     @TimeoutReq(minutes=20)
     public void testBatchAndFlush() throws Exception {
-        try {
-            mWakeLock.acquire();
-            for (Sensor sensor : mSensorList) {
-                // Skip ONLY one-shot sensors.
-                if (sensor.getReportingMode() != Sensor.REPORTING_MODE_ONE_SHOT) {
-                    registerListenerCallFlush(sensor, null);
-                }
-            }
-        } finally {
-            mWakeLock.release();
+        ArrayList<Throwable> errorsFound = new ArrayList<>();
+        for (Sensor sensor : mSensorList) {
+            verifyRegisterListenerCallFlush(sensor, null /* handler */, errorsFound);
         }
+        assertOnErrors(errorsFound);
     }
 
-    // Same as testBatchAndFlush but using Handler version of the API to register for sensors.
-    // onSensorChanged is now called on a background thread.
+    /**
+     * Verifies that sensor events arrive in the given message queue (Handler).
+     */
     @TimeoutReq(minutes=10)
     public void testBatchAndFlushWithHandler() throws Exception {
-        try {
-            mWakeLock.acquire();
-            HandlerThread handlerThread = new HandlerThread("sensorThread");
-            handlerThread.start();
-            Handler handler = new Handler(handlerThread.getLooper());
-            for (Sensor sensor : mSensorList) {
-                // Skip ONLY one-shot sensors.
-                if (sensor.getReportingMode() != Sensor.REPORTING_MODE_ONE_SHOT) {
-                    registerListenerCallFlush(sensor, handler);
-                }
+        Sensor sensor = null;
+        for (Sensor s : mSensorList) {
+            if (s.getReportingMode() == Sensor.REPORTING_MODE_CONTINUOUS) {
+                sensor = s;
+                break;
             }
-        }  finally {
-            mWakeLock.release();
         }
+        if (sensor == null) {
+            throw new SensorTestStateNotSupportedException(
+                    "There are no Continuous sensors in the device.");
+        }
+
+        TestSensorEnvironment environment = new TestSensorEnvironment(
+                getContext(),
+                sensor,
+                SensorManager.SENSOR_DELAY_FASTEST,
+                (int) TimeUnit.SECONDS.toMicros(5));
+        TestSensorManager sensorManager = new TestSensorManager(environment);
+
+        HandlerThread handlerThread = new HandlerThread("sensorThread");
+        handlerThread.start();
+        Handler handler = new Handler(handlerThread.getLooper());
+        TestSensorEventListener listener = new TestSensorEventListener(environment, handler);
+
+        sensorManager.registerListener(listener);
+        listener.waitForEvents(1);
+        sensorManager.requestFlush();
+        listener.waitForFlushComplete();
+        listener.assertEventsReceivedInHandler();
     }
 
-    private void registerListenerCallFlush(Sensor sensor, Handler handler)
-            throws InterruptedException {
-        if (sensor.getReportingMode() == Sensor.REPORTING_MODE_ONE_SHOT) {
-            return;
-        }
-        final int numEvents = 500;
-        final int rateUs = 0; // DELAY_FASTEST
-        final int maxBatchReportLatencyUs = 10000000;
-        final CountDownLatch eventsRemaining = new CountDownLatch(numEvents);
-        final CountDownLatch flushReceived = new CountDownLatch(1);
-        SensorEventListener2 listener = new SensorEventListener2() {
-            @Override
-            public void onSensorChanged(SensorEvent event) {
-                eventsRemaining.countDown();
-            }
+    // TODO: after L release move to SensorBatchingTests and run in all sensors with default
+    //       verifications enabled
+    public void testBatchAndFlushWithMultipleSensors() throws Exception {
+        final int maxSensors = 3;
+        final int maxReportLatencyUs = (int) TimeUnit.SECONDS.toMicros(10);
+        int sensorsCount = mSensorList.size();
+        int numSensors = sensorsCount < maxSensors ? sensorsCount : maxSensors;
 
-            @Override
-            public void onAccuracyChanged(Sensor sensor, int accuracy) {
-            }
-
-            @Override
-            public void onFlushCompleted(Sensor sensor) {
-                flushReceived.countDown();
-            }
-        };
-        // Consider only continuous mode sensors for testing registerListener.
-        // For on-change sensors, call registerListener() so that the listener is associated
-        // with the sensor so that flush(listener) can be called on it.
-        try {
-            Log.i(TAG, "testBatch " + sensor.getName());
-            boolean result = mSensorManager.registerListener(listener, sensor,
-                    rateUs, maxBatchReportLatencyUs, handler);
-            assertTrue("registerListener failed " + sensor.getName(), result);
-
-            // Wait for 500 events or N seconds before the test times out.
+        StringBuilder builder = new StringBuilder();
+        ParallelSensorOperation parallelSensorOperation = new ParallelSensorOperation();
+        for (int i = 0; i < sensorsCount && numSensors > 0; ++i) {
+            Sensor sensor = mSensorList.get(i);
+            // skip all non-continuous sensors
             if (sensor.getReportingMode() == Sensor.REPORTING_MODE_CONTINUOUS) {
-                // Wait for approximately the time required to generate these events + a tolerance
-                // of 10 seconds.
-                long timeToWaitUs =
-                  numEvents*(long)(sensor.getMinDelay()/MIN_SAMPLING_FREQUENCY_MULTIPLIER_TOLERANCE)
-                        + maxBatchReportLatencyUs + TIMEOUT_TOLERANCE_US;
-
-                long totalTimeWaitedUs = waitToCollectAllEvents(timeToWaitUs,
-                        maxBatchReportLatencyUs, eventsRemaining);
-                if (eventsRemaining.getCount() > 0) {
-                    failTimedOut(sensor.getName(), (double)totalTimeWaitedUs/1000, numEvents,
-                            (double)sensor.getMinDelay()/1000,
-                            eventsRemaining.getCount(), numEvents - eventsRemaining.getCount());
-                }
-            }
-            Log.i(TAG, "testFlush " + sensor.getName());
-            result = mSensorManager.flush(listener);
-            assertTrue("flush failed " + sensor.getName(), result);
-            boolean collectedAllEvents = flushReceived.await(TIMEOUT_TOLERANCE_US,
-                    TimeUnit.MICROSECONDS);
-            if (!collectedAllEvents) {
-                fail("Timed out waiting for flushCompleteEvent from " + sensor.getName() +
-                        " waitedFor="+ TIMEOUT_TOLERANCE_US/1000 + "ms");
-            }
-            Log.i(TAG, "testBatchAndFlush PASS " + sensor.getName());
-        } finally {
-            mSensorManager.unregisterListener(listener);
-        }
-    }
-
-    // Wait till the CountDownLatch counts down to zero. If the events are not delivered within
-    // timetoWaitUs, wait an additional maxReportLantencyUs and check if the sensor is streaming
-    // data or not. If the sensor is not streaming at all, fail the test or else wait in increments
-    // of maxReportLantencyUs to collect sensor events.
-    private long waitToCollectAllEvents(long timeToWaitUs, int maxReportLatencyUs,
-            CountDownLatch eventsRemaining)
-            throws InterruptedException {
-        boolean collectedAllEvents = false;
-        long totalTimeWaitedUs = 0;
-        long remainingEvents;
-        final long INCREMENTAL_WAIT_US = maxReportLatencyUs + TimeUnit.SECONDS.toMicros(1);
-        do {
-            totalTimeWaitedUs += timeToWaitUs;
-            remainingEvents = eventsRemaining.getCount();
-            collectedAllEvents = eventsRemaining.await(timeToWaitUs, TimeUnit.MICROSECONDS);
-            timeToWaitUs = INCREMENTAL_WAIT_US;
-        } while (!collectedAllEvents &&
-                (remainingEvents - eventsRemaining.getCount() >=(long)INCREMENTAL_WAIT_US/1000000));
-        return totalTimeWaitedUs;
-    }
-
-    // Call registerListener for multiple sensors at a time and call flush.
-    public void testBatchAndFlushWithMutipleSensors() throws Exception {
-        final int MAX_SENSORS = 3;
-        List<Sensor> sensorsToTest = new ArrayList<Sensor>();
-        for (Sensor sensor : mSensorList) {
-            if (sensor.getReportingMode() == Sensor.REPORTING_MODE_CONTINUOUS) {
-                sensorsToTest.add(sensor);
-                if (sensorsToTest.size()  == MAX_SENSORS) break;
+                TestSensorEnvironment environment = new TestSensorEnvironment(
+                        getContext(),
+                        sensor,
+                        shouldEmulateSensorUnderLoad(),
+                        SensorManager.SENSOR_DELAY_FASTEST,
+                        maxReportLatencyUs);
+                FlushExecutor executor = new FlushExecutor(environment, 500 /* eventCount */);
+                parallelSensorOperation.add(new TestSensorOperation(environment, executor));
+                --numSensors;
+                builder.append(sensor.getName()).append(", ");
             }
         }
 
-        final int numSensorsToTest = sensorsToTest.size();
-        if (numSensorsToTest == 0) {
-            return;
-        }
-        final int numEvents = 500;
-        int rateUs = 0; // DELAY_FASTEST
-        final int maxBatchReportLatencyUs = 10000000;
-        final CountDownLatch eventsRemaining = new CountDownLatch(numSensorsToTest * numEvents);
-        final CountDownLatch flushReceived = new CountDownLatch(numSensorsToTest);
-        SensorEventListener2 listener = new SensorEventListener2() {
-            @Override
-            public void onSensorChanged(SensorEvent event) {
-                eventsRemaining.countDown();
-            }
-
-            @Override
-            public void onAccuracyChanged(Sensor sensor, int accuracy) {
-            }
-
-            @Override
-            public void onFlushCompleted(Sensor sensor) {
-                flushReceived.countDown();
-            }
-        };
-
-        try {
-            mWakeLock.acquire();
-            StringBuilder registeredSensors = new StringBuilder(30);
-            for (Sensor sensor : sensorsToTest) {
-                rateUs = Math.max(sensor.getMinDelay(), rateUs);
-                boolean result = mSensorManager.registerListener(listener, sensor,
-                        SensorManager.SENSOR_DELAY_FASTEST, maxBatchReportLatencyUs);
-                assertTrue("registerListener failed for " + sensor.getName(), result);
-                registeredSensors.append(sensor.getName());
-                registeredSensors.append(" ");
-            }
-
-            Log.i(TAG, "testBatchAndFlushWithMutipleSensors " + registeredSensors);
-            long timeToWaitUs =
-                numEvents*(long)(rateUs/MIN_SAMPLING_FREQUENCY_MULTIPLIER_TOLERANCE) +
-                maxBatchReportLatencyUs + TIMEOUT_TOLERANCE_US;
-
-            long totalTimeWaitedUs = waitToCollectAllEvents(timeToWaitUs, maxBatchReportLatencyUs,
-                    eventsRemaining);
-            if (eventsRemaining.getCount() > 0) {
-                failTimedOut(registeredSensors.toString(), (double)totalTimeWaitedUs/1000,
-                        numEvents, (double)rateUs/1000, eventsRemaining.getCount(),
-                        numEvents - eventsRemaining.getCount());
-            }
-            boolean result = mSensorManager.flush(listener);
-            assertTrue("flush failed " + registeredSensors.toString(), result);
-            boolean collectedFlushEvent =
-                flushReceived.await(TIMEOUT_TOLERANCE_US, TimeUnit.MICROSECONDS);
-            if (!collectedFlushEvent) {
-                fail("Timed out waiting for flushCompleteEvent from " +
-                        registeredSensors.toString() + " waited for=" + timeToWaitUs/1000 + "ms");
-            }
-            Log.i(TAG, "testBatchAndFlushWithMutipleSensors PASS'd");
-        } finally {
-            mSensorManager.unregisterListener(listener);
-            mWakeLock.release();
-        }
+        Log.i(TAG, "Testing batch/flush for sensors: " + builder);
+        parallelSensorOperation.execute(getCurrentTestNode());
     }
 
     private void assertSensorValues(Sensor sensor) {
@@ -583,8 +375,8 @@
         assertTrue("Max resolution must be positive. Resolution=" + sensor.getResolution() +
                 " " + sensor.getName(), sensor.getResolution() >= 0);
         assertNotNull("Vendor name must not be null " + sensor.getName(), sensor.getVendor());
-        assertTrue("Version must be positive version="  + sensor.getVersion() + " " +
-                    sensor.getName(), sensor.getVersion() > 0);
+        assertTrue("Version must be positive version=" + sensor.getVersion() + " " +
+                sensor.getName(), sensor.getVersion() > 0);
         int fifoMaxEventCount = sensor.getFifoMaxEventCount();
         int fifoReservedEventCount = sensor.getFifoReservedEventCount();
         assertTrue(fifoMaxEventCount >= 0);
@@ -617,19 +409,142 @@
         assertEquals(sensors, mSensorManager.getSensors());
     }
 
-    class TriggerListener extends TriggerEventListener {
-        @Override
-        public void onTrigger(TriggerEvent event) {
+    /**
+     * Verifies that a continuous sensor produces events that have timestamps synchronized with
+     * {@link SystemClock#elapsedRealtimeNanos()}.
+     */
+    private void verifyLongActivation(
+            Sensor sensor,
+            int maxReportLatencyUs,
+            ArrayList<Throwable> errorsFound) throws InterruptedException {
+        if (sensor.getReportingMode() != Sensor.REPORTING_MODE_CONTINUOUS) {
+            return;
+        }
+
+        try {
+            TestSensorEnvironment environment = new TestSensorEnvironment(
+                    getContext(),
+                    sensor,
+                    shouldEmulateSensorUnderLoad(),
+                    SensorManager.SENSOR_DELAY_FASTEST,
+                    maxReportLatencyUs);
+            TestSensorOperation operation =
+                    TestSensorOperation.createOperation(environment, 20, TimeUnit.SECONDS);
+            operation.addVerification(EventGapVerification.getDefault(environment));
+            operation.addVerification(EventOrderingVerification.getDefault(environment));
+            operation.addVerification(
+                    EventTimestampSynchronizationVerification.getDefault(environment));
+
+            Log.i(TAG, "Running timestamp test on: " + sensor.getName());
+            operation.execute(getCurrentTestNode());
+        } catch (InterruptedException e) {
+            // propagate so the test can stop
+            throw e;
+        } catch (Throwable e) {
+            errorsFound.add(e);
+            Log.e(TAG, e.getMessage());
         }
     }
 
-    class SensorListener implements SensorEventListener {
-        @Override
-        public void onSensorChanged(SensorEvent event) {
+    /**
+     * Verifies that a client can listen for events, and that
+     * {@link SensorManager#flush(SensorEventListener)} will trigger the appropriate notification
+     * for {@link SensorEventListener2#onFlushCompleted(Sensor)}.
+     */
+    private void verifyRegisterListenerCallFlush(
+            Sensor sensor,
+            Handler handler,
+            ArrayList<Throwable> errorsFound)
+            throws InterruptedException {
+        if (sensor.getReportingMode() == Sensor.REPORTING_MODE_ONE_SHOT) {
+            return;
         }
 
-        @Override
-        public void onAccuracyChanged(Sensor sensor, int accuracy) {
+        try {
+            TestSensorEnvironment environment = new TestSensorEnvironment(
+                    getContext(),
+                    sensor,
+                    shouldEmulateSensorUnderLoad(),
+                    SensorManager.SENSOR_DELAY_FASTEST,
+                    (int) TimeUnit.SECONDS.toMicros(10));
+            FlushExecutor executor = new FlushExecutor(environment, 500 /* eventCount */);
+            TestSensorOperation operation = new TestSensorOperation(environment, executor, handler);
+
+            Log.i(TAG, "Running flush test on: " + sensor.getName());
+            operation.execute(getCurrentTestNode());
+        } catch (InterruptedException e) {
+            // propagate so the test can stop
+            throw e;
+        } catch (Throwable e) {
+            errorsFound.add(e);
+            Log.e(TAG, e.getMessage());
         }
     }
+
+    private void assertOnErrors(List<Throwable> errorsFound) {
+        if (!errorsFound.isEmpty()) {
+            StringBuilder builder = new StringBuilder();
+            for (Throwable error : errorsFound) {
+                builder.append(error.getMessage()).append("\n");
+            }
+            Assert.fail(builder.toString());
+        }
+    }
+
+    /**
+     * A delegate that drives the execution of Batch/Flush tests.
+     * It performs several operations in order:
+     * - registration
+     * - for continuous sensors it first ensures that the FIFO is filled
+     *      - if events do not arrive on time, an assert will be triggered
+     * - requests flush of sensor data
+     * - waits for {@link SensorEventListener2#onFlushCompleted(Sensor)}
+     *      - if the event does not arrive, an assert will be triggered
+     */
+    private class FlushExecutor implements TestSensorOperation.Executor {
+        private final TestSensorEnvironment mEnvironment;
+        private final int mEventCount;
+
+        public FlushExecutor(TestSensorEnvironment environment, int eventCount) {
+            mEnvironment = environment;
+            mEventCount = eventCount;
+        }
+
+        /**
+         * Consider only continuous mode sensors for testing register listener.
+         *
+         * For on-change sensors, we only use
+         * {@link TestSensorManager#registerListener(TestSensorEventListener)} to associate the
+         * listener with the sensor. So that {@link TestSensorManager#requestFlush()} can be
+         * invoked on it.
+         */
+        @Override
+        public void execute(TestSensorManager sensorManager, TestSensorEventListener listener)
+                throws InterruptedException {
+            int sensorReportingMode = mEnvironment.getSensor().getReportingMode();
+            try {
+                sensorManager.registerListener(listener);
+                if (sensorReportingMode == Sensor.REPORTING_MODE_CONTINUOUS) {
+                    listener.waitForEvents(mEventCount);
+                }
+                sensorManager.requestFlush();
+                listener.waitForFlushComplete();
+            } finally {
+                sensorManager.unregisterListener();
+            }
+        }
+    }
+
+    private class NullTriggerEventListener extends TriggerEventListener {
+        @Override
+        public void onTrigger(TriggerEvent event) {}
+    }
+
+    private class NullSensorEventListener implements SensorEventListener {
+        @Override
+        public void onSensorChanged(SensorEvent event) {}
+
+        @Override
+        public void onAccuracyChanged(Sensor sensor, int accuracy) {}
+    }
 }
diff --git a/tests/tests/hardware/src/android/hardware/cts/SensorTestCase.java b/tests/tests/hardware/src/android/hardware/cts/SensorTestCase.java
index 6454678..42b8d33 100644
--- a/tests/tests/hardware/src/android/hardware/cts/SensorTestCase.java
+++ b/tests/tests/hardware/src/android/hardware/cts/SensorTestCase.java
@@ -16,17 +16,11 @@
 
 package android.hardware.cts;
 
-import com.android.cts.util.ReportLog;
-import com.android.cts.util.ResultType;
-import com.android.cts.util.ResultUnit;
-
-import android.app.Instrumentation;
-import android.cts.util.DeviceReportLog;
 import android.hardware.Sensor;
-import android.hardware.cts.helpers.SensorStats;
 import android.hardware.cts.helpers.SensorTestStateNotSupportedException;
 import android.hardware.cts.helpers.TestSensorEnvironment;
-import android.hardware.cts.helpers.sensoroperations.ISensorOperation;
+import android.hardware.cts.helpers.reporting.ISensorTestNode;
+import android.hardware.cts.helpers.sensoroperations.SensorOperation;
 import android.test.AndroidTestCase;
 import android.util.Log;
 
@@ -35,25 +29,30 @@
  */
 public abstract class SensorTestCase extends AndroidTestCase {
     // TODO: consolidate all log tags
-    protected final String LOG_TAG = "TestRunner";
+    protected static final String LOG_TAG = "TestRunner";
 
     /**
      * By default tests need to run in a {@link TestSensorEnvironment} that assumes each sensor is
      * running with a load of several listeners, requesting data at different rates.
      *
-     * In a better world the component acting as builder of {@link ISensorOperation} would compute
+     * In a better world the component acting as builder of {@link SensorOperation} would compute
      * this value based on the tests composed.
      *
      * Ideally, each {@link Sensor} object would expose this information to clients.
      */
     private volatile boolean mEmulateSensorUnderLoad = true;
 
+    /**
+     * By default the test class is the root of the test hierarchy.
+     */
+    private volatile ISensorTestNode mCurrentTestNode = new TestClassNode(getClass());
+
     protected SensorTestCase() {}
 
     @Override
-    public void runTest() throws Throwable {
+    public void runBare() throws Throwable {
         try {
-            super.runTest();
+            super.runBare();
         } catch (SensorTestStateNotSupportedException e) {
             // the sensor state is not supported in the device, log a warning and skip the test
             Log.w(LOG_TAG, e.getMessage());
@@ -68,33 +67,24 @@
         return mEmulateSensorUnderLoad;
     }
 
-    /**
-     * Utility method to log selected stats to a {@link ReportLog} object.  The stats must be
-     * a number or an array of numbers.
-     */
-    public static void logSelectedStatsToReportLog(Instrumentation instrumentation, int depth,
-            String[] keys, SensorStats stats) {
-        DeviceReportLog reportLog = new DeviceReportLog(depth);
+    public void setCurrentTestNode(ISensorTestNode value) {
+        mCurrentTestNode = value;
+    }
 
-        for (String key : keys) {
-            Object value = stats.getValue(key);
-            if (value instanceof Integer) {
-                reportLog.printValue(key, (Integer) value, ResultType.NEUTRAL, ResultUnit.NONE);
-            } else if (value instanceof Double) {
-                reportLog.printValue(key, (Double) value, ResultType.NEUTRAL, ResultUnit.NONE);
-            } else if (value instanceof Float) {
-                reportLog.printValue(key, (Float) value, ResultType.NEUTRAL, ResultUnit.NONE);
-            } else if (value instanceof double[]) {
-                reportLog.printArray(key, (double[]) value, ResultType.NEUTRAL, ResultUnit.NONE);
-            } else if (value instanceof float[]) {
-                float[] tmpFloat = (float[]) value;
-                double[] tmpDouble = new double[tmpFloat.length];
-                for (int i = 0; i < tmpDouble.length; i++) tmpDouble[i] = tmpFloat[i];
-                reportLog.printArray(key, tmpDouble, ResultType.NEUTRAL, ResultUnit.NONE);
-            }
+    protected ISensorTestNode getCurrentTestNode() {
+        return mCurrentTestNode;
+    }
+
+    private class TestClassNode implements ISensorTestNode {
+        private final Class<?> mTestClass;
+
+        public TestClassNode(Class<?> testClass) {
+            mTestClass = testClass;
         }
 
-        reportLog.printSummary("summary", 0, ResultType.NEUTRAL, ResultUnit.NONE);
-        reportLog.deliverReportToHost(instrumentation);
+        @Override
+        public String getName() {
+            return mTestClass.getSimpleName();
+        }
     }
 }
diff --git a/tests/tests/hardware/src/android/hardware/cts/SingleSensorTests.java b/tests/tests/hardware/src/android/hardware/cts/SingleSensorTests.java
index 3bd4a03..42cbdfb 100644
--- a/tests/tests/hardware/src/android/hardware/cts/SingleSensorTests.java
+++ b/tests/tests/hardware/src/android/hardware/cts/SingleSensorTests.java
@@ -105,7 +105,7 @@
      */
     public void testSensorProperties() {
         // sensor type: [getMinDelay()]
-        Map<Integer, Object[]> expectedProperties = new HashMap<Integer, Object[]>(3);
+        Map<Integer, Object[]> expectedProperties = new HashMap<>(3);
         expectedProperties.put(Sensor.TYPE_ACCELEROMETER, new Object[]{10000});
         expectedProperties.put(Sensor.TYPE_GYROSCOPE, new Object[]{10000});
         expectedProperties.put(Sensor.TYPE_MAGNETIC_FIELD, new Object[]{100000});
@@ -541,25 +541,21 @@
                 sensorType,
                 shouldEmulateSensorUnderLoad(),
                 rateUs);
-        TestSensorOperation op = new TestSensorOperation(environment, 5, TimeUnit.SECONDS);
+        TestSensorOperation op =
+                TestSensorOperation.createOperation(environment, 5, TimeUnit.SECONDS);
         op.addDefaultVerifications();
-        op.setLogEvents(true);
-        try {
-            op.execute();
-        } finally {
-            SensorStats.logStats(TAG, op.getStats());
 
-            String sensorRate;
-            if (rateUs == SensorManager.SENSOR_DELAY_FASTEST) {
-                sensorRate = "fastest";
-            } else {
-                sensorRate = String.format("%.0fhz", environment.getFrequencyHz());
-            }
+        try {
+            op.execute(getCurrentTestNode());
+        } finally {
+            SensorStats stats = op.getStats();
+            stats.log(TAG);
+
             String fileName = String.format(
                     "single_%s_%s.txt",
                     SensorStats.getSanitizedSensorName(environment.getSensor()),
-                    sensorRate);
-            SensorStats.logStatsToFile(fileName, op.getStats());
+                    environment.getFrequencyString());
+            stats.logToFile(fileName);
         }
     }
 }
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/CollectingSensorEventListener.java b/tests/tests/hardware/src/android/hardware/cts/helpers/CollectingSensorEventListener.java
deleted file mode 100644
index ca7d133..0000000
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/CollectingSensorEventListener.java
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * Copyright (C) 2014 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 android.hardware.cts.helpers;
-
-import android.hardware.SensorEvent;
-import android.hardware.SensorEventListener2;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-
-/**
- * A {@link TestSensorEventListener} which collects events to be processed after the test is run.
- * This should only be used for short tests.
- */
-public class CollectingSensorEventListener extends TestSensorEventListener {
-    private final ArrayList<TestSensorEvent> mSensorEventsList = new ArrayList<TestSensorEvent>();
-
-    /**
-     * Constructs a {@link CollectingSensorEventListener} with an additional
-     * {@link SensorEventListener2}.
-     */
-    public CollectingSensorEventListener(SensorEventListener2 listener) {
-        super(listener);
-    }
-
-    /**
-     * Constructs a {@link CollectingSensorEventListener}.
-     */
-    public CollectingSensorEventListener() {
-        this(null);
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void onSensorChanged(SensorEvent event) {
-        super.onSensorChanged(event);
-        synchronized (mSensorEventsList) {
-            mSensorEventsList.add(new TestSensorEvent(event));
-        }
-    }
-
-    /**
-     * {@inheritDoc}
-     * <p>
-     * Clears the event queue before starting.
-     * </p>
-     */
-    @Override
-    public void waitForEvents(int eventCount) throws InterruptedException {
-        clearEvents();
-        super.waitForEvents(eventCount);
-    }
-
-    /**
-     * {@inheritDoc}
-     * <p>
-     * Clears the event queue before starting.
-     * </p>
-     */
-    @Override
-    public void waitForEvents(long duration, TimeUnit timeUnit) throws InterruptedException {
-        clearEvents();
-        super.waitForEvents(duration, timeUnit);
-    }
-
-    /**
-     * Get the {@link TestSensorEvent} array from the event queue.
-     */
-    public List<TestSensorEvent> getEvents() {
-        synchronized (mSensorEventsList) {
-            return Collections.unmodifiableList(mSensorEventsList);
-        }
-    }
-
-    /**
-     * Clear the event queue.
-     */
-    public void clearEvents() {
-        synchronized (mSensorEventsList) {
-            mSensorEventsList.clear();
-        }
-    }
-}
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/SensorCalibratedUncalibratedVerifier.java b/tests/tests/hardware/src/android/hardware/cts/helpers/SensorCalibratedUncalibratedVerifier.java
index b3b8559..d3dcf37 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/SensorCalibratedUncalibratedVerifier.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/SensorCalibratedUncalibratedVerifier.java
@@ -35,6 +35,8 @@
 
     private final TestSensorManager mCalibratedSensorManager;
     private final TestSensorManager mUncalibratedSensorManager;
+    private final TestSensorEventListener mCalibratedTestListener;
+    private final TestSensorEventListener mUncalibratedTestListener;
     private final float mThreshold;
 
     public SensorCalibratedUncalibratedVerifier(
@@ -43,6 +45,8 @@
             float threshold) {
         mCalibratedSensorManager = new TestSensorManager(calibratedEnvironment);
         mUncalibratedSensorManager = new TestSensorManager(uncalibratedEnvironment);
+        mCalibratedTestListener = new TestSensorEventListener(calibratedEnvironment);
+        mUncalibratedTestListener = new TestSensorEventListener(uncalibratedEnvironment);
         mThreshold = threshold;
     }
 
@@ -50,10 +54,8 @@
      * Executes the operation: it collects the data and run verifications on it.
      */
     public void execute() throws Throwable {
-        CollectingSensorEventListener calibratedTestListener = new CollectingSensorEventListener();
-        CollectingSensorEventListener uncalibratedTestListener = new CollectingSensorEventListener();
-        mCalibratedSensorManager.registerListener(calibratedTestListener);
-        mUncalibratedSensorManager.registerListener(uncalibratedTestListener);
+        mCalibratedSensorManager.registerListener(mCalibratedTestListener);
+        mUncalibratedSensorManager.registerListener(mUncalibratedTestListener);
 
         Thread.sleep(TimeUnit.SECONDS.toMillis(10));
 
@@ -61,8 +63,8 @@
         mUncalibratedSensorManager.unregisterListener();
 
         verifyMeasurements(
-                calibratedTestListener.getEvents(),
-                uncalibratedTestListener.getEvents(),
+                mCalibratedTestListener.getCollectedEvents(),
+                mUncalibratedTestListener.getCollectedEvents(),
                 mThreshold);
     }
 
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 a79e5b1..490e965 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/SensorCtsHelper.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/SensorCtsHelper.java
@@ -192,7 +192,7 @@
             TestSensorEnvironment environment,
             String extras) {
         return String.format(
-                "%s | sensor='%s', samplingPeriodUs=%d, maxReportLatencyUs=%d | %s",
+                "%s | sensor='%s', samplingPeriod=%dus, maxReportLatency=%dus | %s",
                 label,
                 environment.getSensor().getName(),
                 environment.getRequestedSamplingPeriodUs(),
@@ -219,6 +219,25 @@
     }
 
     /**
+     * Sanitizes a string so it can be used in file names.
+     *
+     * @param value The string to sanitize.
+     * @return The sanitized string.
+     *
+     * @throws SensorTestPlatformException If the string cannot be sanitized.
+     */
+    public static String sanitizeStringForFileName(String value)
+            throws SensorTestPlatformException {
+        String sanitizedValue = value.replaceAll("[^a-zA-Z0-9_\\-]", "_");
+        if (sanitizedValue.matches("_*")) {
+            throw new SensorTestPlatformException(
+                    "Unable to sanitize string '%s' for file name.",
+                    value);
+        }
+        return sanitizedValue;
+    }
+
+    /**
      * Ensures that the directory structure represented by the given {@link File} is created.
      */
     private static File createDirectoryStructure(File directoryStructure) throws IOException {
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/SensorStats.java b/tests/tests/hardware/src/android/hardware/cts/helpers/SensorStats.java
index 7be6c0c..8067aed 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/SensorStats.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/SensorStats.java
@@ -17,7 +17,7 @@
 package android.hardware.cts.helpers;
 
 import android.hardware.Sensor;
-import android.hardware.cts.helpers.sensoroperations.ISensorOperation;
+import android.hardware.cts.helpers.sensoroperations.SensorOperation;
 import android.util.Log;
 
 import java.io.BufferedWriter;
@@ -34,28 +34,29 @@
 import java.util.Set;
 
 /**
- * Class used to store stats related to {@link ISensorOperation}s.  Sensor stats may be linked
+ * Class used to store stats related to {@link SensorOperation}s. Sensor stats may be linked
  * together so that they form a tree.
  */
 public class SensorStats {
     public static final String DELIMITER = "__";
 
-    public static final String FIRST_TIMESTAMP_KEY = "first_timestamp";
-    public static final String LAST_TIMESTAMP_KEY = "last_timestamp";
     public static final String ERROR = "error";
-    public static final String EVENT_COUNT_KEY = "event_count";
     public static final String EVENT_GAP_COUNT_KEY = "event_gap_count";
     public static final String EVENT_GAP_POSITIONS_KEY = "event_gap_positions";
     public static final String EVENT_OUT_OF_ORDER_COUNT_KEY = "event_out_of_order_count";
     public static final String EVENT_OUT_OF_ORDER_POSITIONS_KEY = "event_out_of_order_positions";
+    public static final String EVENT_TIME_SYNCHRONIZATION_COUNT_KEY =
+            "event_time_synchronization_count";
+    public static final String EVENT_TIME_SYNCHRONIZATION_POSITIONS_KEY =
+            "event_time_synchronization_positions";
     public static final String FREQUENCY_KEY = "frequency";
     public static final String JITTER_95_PERCENTILE_PERCENT_KEY = "jitter_95_percentile_percent";
     public static final String MEAN_KEY = "mean";
     public static final String STANDARD_DEVIATION_KEY = "standard_deviation";
     public static final String MAGNITUDE_KEY = "magnitude";
 
-    private final Map<String, Object> mValues = new HashMap<String, Object>();
-    private final Map<String, SensorStats> mSensorStats = new HashMap<String, SensorStats>();
+    private final Map<String, Object> mValues = new HashMap<>();
+    private final Map<String, SensorStats> mSensorStats = new HashMap<>();
 
     /**
      * Add a value.
@@ -72,7 +73,7 @@
 
     /**
      * Add a nested {@link SensorStats}. This is useful for keeping track of stats in a
-     * {@link ISensorOperation} tree.
+     * {@link SensorOperation} tree.
      *
      * @param key the key
      * @param stats the sub {@link SensorStats} object.
@@ -103,13 +104,13 @@
     /**
      * Flattens the map and all sub {@link SensorStats} objects. Keys will be flattened using
      * {@value #DELIMITER}. For example, if a sub {@link SensorStats} is added with key
-     * {@code "key1"} containing the key value pair {@code ("key2", "value")}, the flattened map
-     * will contain the entry {@code ("key1__key2", "value")}.
+     * {@code "key1"} containing the key value pair {@code \("key2", "value"\)}, the flattened map
+     * will contain the entry {@code \("key1__key2", "value"\)}.
      *
      * @return a {@link Map} containing all stats from the value and sub {@link SensorStats}.
      */
     public synchronized Map<String, Object> flatten() {
-        final Map<String, Object> flattenedMap = new HashMap<String, Object>(mValues);
+        final Map<String, Object> flattenedMap = new HashMap<>(mValues);
         for (Entry<String, SensorStats> statsEntry : mSensorStats.entrySet()) {
             for (Entry<String, Object> valueEntry : statsEntry.getValue().flatten().entrySet()) {
                 String key = statsEntry.getKey() + DELIMITER + valueEntry.getKey();
@@ -122,8 +123,8 @@
     /**
      * Utility method to log the stats to the logcat.
      */
-    public static void logStats(String tag, SensorStats stats) {
-        final Map<String, Object> flattened = stats.flatten();
+    public void log(String tag) {
+        final Map<String, Object> flattened = flatten();
         for (String key : getSortedKeys(flattened)) {
             Object value = flattened.get(key);
             Log.v(tag, String.format("%s: %s", key, getValueString(value)));
@@ -133,39 +134,29 @@
     /**
      * Utility method to log the stats to a file. Will overwrite the file if it already exists.
      */
-    public static void logStatsToFile(String fileName, SensorStats stats) throws IOException {
+    public void logToFile(String fileName) throws IOException {
         File statsDirectory = SensorCtsHelper.getSensorTestDataDirectory("stats/");
         File logFile = new File(statsDirectory, fileName);
-        final BufferedWriter writer =
-                new BufferedWriter(new FileWriter(logFile, false /* append */));
-        final Map<String, Object> flattened = stats.flatten();
-        try {
+        final Map<String, Object> flattened = flatten();
+        FileWriter fileWriter = new FileWriter(logFile, false /* append */);
+        try (BufferedWriter writer = new BufferedWriter(fileWriter)) {
             for (String key : getSortedKeys(flattened)) {
                 Object value = flattened.get(key);
                 writer.write(String.format("%s: %s\n", key, getValueString(value)));
             }
-        } finally {
-            writer.flush();
-            writer.close();
         }
     }
 
     /**
      * Provides a sanitized sensor name, that can be used in file names.
-     * See {@link #logStatsToFile(String, SensorStats)}.
+     * See {@link #logToFile(String)}.
      */
-    public static String getSanitizedSensorName(Sensor sensor) throws IOException {
-        String sensorType = sensor.getStringType();
-        String sanitizedSensorType = sensorType.replaceAll("[^a-zA-Z0-9_\\-]", "_");
-        if (sanitizedSensorType.matches("_*")) {
-            throw new IOException("Unable to sanitize sensor type (" + sensorType + "). This is a"
-                    + " 'test framework' issue and the sanitation routine must be fixed.");
-        }
-        return sanitizedSensorType;
+    public static String getSanitizedSensorName(Sensor sensor) throws SensorTestPlatformException {
+        return SensorCtsHelper.sanitizeStringForFileName(sensor.getStringType());
     }
 
     private static List<String> getSortedKeys(Map<String, Object> flattenedStats) {
-        List<String> keys = new ArrayList<String>(flattenedStats.keySet());
+        List<String> keys = new ArrayList<>(flattenedStats.keySet());
         Collections.sort(keys);
         return keys;
     }
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/SensorTestPlatformException.java b/tests/tests/hardware/src/android/hardware/cts/helpers/SensorTestPlatformException.java
new file mode 100644
index 0000000..b230749
--- /dev/null
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/SensorTestPlatformException.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2014 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 android.hardware.cts.helpers;
+
+/**
+ * Exception that indicates an issue in the Sensor Test Platform.
+ * It usually constitutes a bug in the platform that needs to be fixed.
+ */
+public class SensorTestPlatformException extends Exception {
+    public SensorTestPlatformException(String format, Object ... params) {
+        this(String.format(format, params));
+    }
+
+    public SensorTestPlatformException(String message) {
+        super(message + " (This is a 'sensor test platform' issue, please report it so it can be fixed)");
+    }
+}
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/TestSensorEnvironment.java b/tests/tests/hardware/src/android/hardware/cts/helpers/TestSensorEnvironment.java
index 8f33f92..7d10f91 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/TestSensorEnvironment.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/TestSensorEnvironment.java
@@ -19,15 +19,22 @@
 import android.content.Context;
 import android.hardware.Sensor;
 import android.hardware.SensorManager;
-import android.hardware.cts.helpers.sensoroperations.ISensorOperation;
+import android.hardware.cts.helpers.sensoroperations.SensorOperation;
 
 import java.util.concurrent.TimeUnit;
 
 /**
- * A class that encapsulates base environment information for the {@link ISensorOperation}.
+ * A class that encapsulates base environment information for the {@link SensorOperation}.
  * The environment is self contained and carries its state around all the sensor test framework.
  */
 public class TestSensorEnvironment {
+
+    /**
+     * It represents the fraction of the expected sampling frequency, at which the sensor can
+     * actually produce events.
+     */
+    private static final float MAXIMUM_EXPECTED_SAMPLING_FREQUENCY_MULTIPLIER = 0.9f;
+
     private final Context mContext;
     private final Sensor mSensor;
     private final boolean mSensorMightHaveMoreListeners;
@@ -40,7 +47,10 @@
      * @param context The context for the test
      * @param sensorType The type of the sensor under test
      * @param samplingPeriodUs The requested collection period for the sensor under test
+     *
+     * @deprecated Use variants with {@link Sensor} objects.
      */
+    @Deprecated
     public TestSensorEnvironment(Context context, int sensorType, int samplingPeriodUs) {
         this(context, sensorType, false /* sensorMightHaveMoreListeners */, samplingPeriodUs);
     }
@@ -52,7 +62,10 @@
      * @param sensorType The type of the sensor under test
      * @param samplingPeriodUs The requested collection period for the sensor under test
      * @param maxReportLatencyUs The requested collection report latency for the sensor under test
+     *
+     * @deprecated Use variants with {@link Sensor} objects.
      */
+    @Deprecated
     public TestSensorEnvironment(
             Context context,
             int sensorType,
@@ -72,7 +85,10 @@
      * @param sensorType The type of the sensor under test
      * @param sensorMightHaveMoreListeners Whether the sensor under test is acting under load
      * @param samplingPeriodUs The requested collection period for the sensor under test
+     *
+     * @deprecated Use variants with {@link Sensor} objects.
      */
+    @Deprecated
     public TestSensorEnvironment(
             Context context,
             int sensorType,
@@ -93,7 +109,10 @@
      * @param sensorMightHaveMoreListeners Whether the sensor under test is acting under load
      * @param samplingPeriodUs The requested collection period for the sensor under test
      * @param maxReportLatencyUs The requested collection report latency for the sensor under test
+     *
+     * @deprecated Use variants with {@link Sensor} objects.
      */
+    @Deprecated
     public TestSensorEnvironment(
             Context context,
             int sensorType,
@@ -112,6 +131,26 @@
      *
      * @param context The context for the test
      * @param sensor The sensor under test
+     * @param samplingPeriodUs The requested collection period for the sensor under test
+     * @param maxReportLatencyUs The requested collection report latency for the sensor under test
+     */
+    public TestSensorEnvironment(
+            Context context,
+            Sensor sensor,
+            int samplingPeriodUs,
+            int maxReportLatencyUs) {
+        this(context,
+                sensor,
+                false /* sensorMightHaveMoreListeners */,
+                samplingPeriodUs,
+                maxReportLatencyUs);
+    }
+
+    /**
+     * Constructs an environment for sensor testing.
+     *
+     * @param context The context for the test
+     * @param sensor The sensor under test
      * @param sensorMightHaveMoreListeners Whether the sensor under test is acting under load (this
      *                                     usually implies that there are several listeners
      *                                     requesting different sampling periods)
@@ -160,6 +199,17 @@
     }
 
     /**
+     * @return A string representing the frequency equivalent to
+     * {@link #getRequestedSamplingPeriodUs()}.
+     */
+    public String getFrequencyString() {
+        if (mSamplingPeriodUs == SensorManager.SENSOR_DELAY_FASTEST) {
+            return "fastest";
+        }
+        return String.format("%.0fhz", getFrequencyHz());
+    }
+
+    /**
      * @return The requested collection max batch report latency in microseconds.
      */
     public int getMaxReportLatencyUs() {
@@ -171,7 +221,8 @@
      * data at different sampling rates (the rates are unknown); false otherwise.
      */
     public boolean isSensorSamplingRateOverloaded() {
-        return mSensorMightHaveMoreListeners && mSamplingPeriodUs != SensorManager.SENSOR_DELAY_FASTEST;
+        return mSensorMightHaveMoreListeners
+                && mSamplingPeriodUs != SensorManager.SENSOR_DELAY_FASTEST;
     }
 
     /**
@@ -197,6 +248,15 @@
     }
 
     /**
+     * @return The actual sampling period at which a sensor can sample data. This value is a
+     *         fraction of {@link #getExpectedSamplingPeriodUs()}.
+     */
+    public int getMaximumExpectedSamplingPeriodUs() {
+        int expectedSamplingPeriodUs = getExpectedSamplingPeriodUs();
+        return (int) (expectedSamplingPeriodUs / MAXIMUM_EXPECTED_SAMPLING_FREQUENCY_MULTIPLIER);
+    }
+
+    /**
      * @return The number of axes in the coordinate system of the sensor under test.
      */
     public int getSensorAxesCount() {
@@ -266,6 +326,17 @@
         return TimeUnit.SECONDS.toNanos(reportLatencySec);
     }
 
+    @Override
+    public String toString() {
+        return String.format(
+                "Sensor='%s', SamplingRateOverloaded=%s, SamplingPeriod=%sus, "
+                        + "MaxReportLatency=%sus",
+                mSensor,
+                isSensorSamplingRateOverloaded(),
+                mSamplingPeriodUs,
+                mMaxReportLatencyUs);
+    }
+
     /**
      * Return true if {@link #getRequestedSamplingPeriodUs()} is not one of
      * {@link SensorManager#SENSOR_DELAY_GAME}, {@link SensorManager#SENSOR_DELAY_UI}, or
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/TestSensorEvent.java b/tests/tests/hardware/src/android/hardware/cts/helpers/TestSensorEvent.java
index e8500f1..86b2436 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/TestSensorEvent.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/TestSensorEvent.java
@@ -21,6 +21,8 @@
 import android.hardware.SensorEventListener2;
 import android.os.SystemClock;
 
+import java.util.Arrays;
+
 /**
  * Class for holding information about individual {@link SensorEvent}s.
  */
@@ -75,4 +77,14 @@
         this.accuracy = accuracy;
         this.values = values;
     }
+
+    @Override
+    public String toString() {
+        return String.format(
+                "Timestamp=%sns, ReceivedTimestamp=%sns, Accuracy=%s, Values=%s",
+                this.timestamp,
+                this.receivedTimestamp,
+                this.accuracy,
+                Arrays.toString(this.values));
+    }
 }
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 6567be2..a60428f 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/TestSensorEventListener.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/TestSensorEventListener.java
@@ -21,12 +21,21 @@
 import android.hardware.Sensor;
 import android.hardware.SensorEvent;
 import android.hardware.SensorEventListener2;
+import android.os.Handler;
+import android.os.Looper;
 import android.os.SystemClock;
-import android.util.Log;
 
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
 
 /**
  * A {@link SensorEventListener2} which performs operations such as waiting for a specific number of
@@ -36,51 +45,39 @@
  */
 public class TestSensorEventListener implements SensorEventListener2 {
     public static final String LOG_TAG = "TestSensorEventListener";
-    private static final long EVENT_TIMEOUT_US = TimeUnit.MICROSECONDS.convert(5, TimeUnit.SECONDS);
-    private static final long FLUSH_TIMEOUT_US = TimeUnit.MICROSECONDS.convert(5, TimeUnit.SECONDS);
 
-    private final SensorEventListener2 mListener;
+    private static final long EVENT_TIMEOUT_US = TimeUnit.SECONDS.toMicros(5);
+    private static final long FLUSH_TIMEOUT_US = TimeUnit.SECONDS.toMicros(10);
 
-    private volatile CountDownLatch mEventLatch;
-    private volatile CountDownLatch mFlushLatch = new CountDownLatch(1);
-    private volatile TestSensorEnvironment mEnvironment;
-    private volatile boolean mLogEvents;
+    private final List<TestSensorEvent> mCollectedEvents = new ArrayList<>();
+    private final List<CountDownLatch> mEventLatches = new ArrayList<>();
+    private final List<CountDownLatch> mFlushLatches = new ArrayList<>();
+    private final AtomicInteger mEventsReceivedOutsideHandler = new AtomicInteger();
+
+    private final Handler mHandler;
+    private final TestSensorEnvironment mEnvironment;
+
+    /**
+     * @deprecated Use {@link TestSensorEventListener(TestSensorEnvironment)}.
+     */
+    @Deprecated
+    public TestSensorEventListener() {
+        this(null /* environment */);
+    }
 
     /**
      * Construct a {@link TestSensorEventListener}.
      */
-    public TestSensorEventListener() {
-        this(null);
+    public TestSensorEventListener(TestSensorEnvironment environment) {
+        this(environment, null /* handler */);
     }
 
     /**
-     * Construct a {@link TestSensorEventListener} that wraps a {@link SensorEventListener2}.
+     * Construct a {@link TestSensorEventListener}.
      */
-    public TestSensorEventListener(SensorEventListener2 listener) {
-        if (listener != null) {
-            mListener = listener;
-        } else {
-            // use a Null Object to simplify handling the listener
-            mListener = new SensorEventListener2() {
-                public void onFlushCompleted(Sensor sensor) {}
-                public void onSensorChanged(SensorEvent sensorEvent) {}
-                public void onAccuracyChanged(Sensor sensor, int i) {}
-            };
-        }
-    }
-
-    /**
-     * Set the sensor, rate, and batch report latency used for the assertions.
-     */
-    public void setEnvironment(TestSensorEnvironment environment) {
+    public TestSensorEventListener(TestSensorEnvironment environment, Handler handler) {
         mEnvironment = environment;
-    }
-
-    /**
-     * Set whether or not to log events
-     */
-    public void setLogEvents(boolean log) {
-        mLogEvents = log;
+        mHandler = handler;
     }
 
     /**
@@ -88,19 +85,15 @@
      */
     @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)));
+        long timestampNs = SystemClock.elapsedRealtimeNanos();
+        checkHandler();
+        synchronized (mCollectedEvents) {
+            mCollectedEvents.add(new TestSensorEvent(event, timestampNs));
         }
-
-        CountDownLatch eventLatch = mEventLatch;
-        if(eventLatch != null) {
-            eventLatch.countDown();
+        synchronized (mEventLatches) {
+            for (CountDownLatch latch : mEventLatches) {
+                latch.countDown();
+            }
         }
     }
 
@@ -109,7 +102,7 @@
      */
     @Override
     public void onAccuracyChanged(Sensor sensor, int accuracy) {
-        mListener.onAccuracyChanged(sensor, accuracy);
+        checkHandler();
     }
 
     /**
@@ -117,12 +110,71 @@
      */
     @Override
     public void onFlushCompleted(Sensor sensor) {
-        CountDownLatch latch = mFlushLatch;
-        mFlushLatch = null;
-        if(latch != null) {
-            latch.countDown();
+        checkHandler();
+        synchronized (mFlushLatches) {
+            for (CountDownLatch latch : mFlushLatches) {
+                latch.countDown();
+            }
         }
-        mListener.onFlushCompleted(sensor);
+    }
+
+    /**
+     * @return The handler (if any) associated with the instance.
+     */
+    public Handler getHandler() {
+        return mHandler;
+    }
+
+    /**
+     * @return A list of {@link TestSensorEvent}s collected by the listener.
+     */
+    public List<TestSensorEvent> getCollectedEvents() {
+        synchronized (mCollectedEvents){
+            return Collections.unmodifiableList(mCollectedEvents);
+        }
+    }
+
+    /**
+     * Clears the internal list of collected {@link TestSensorEvent}s.
+     */
+    public void clearEvents() {
+        synchronized (mCollectedEvents) {
+            mCollectedEvents.clear();
+        }
+    }
+
+
+    /**
+     * Utility method to log the collected events to a file.
+     * It will overwrite the file if it already exists, the file is created in a relative directory
+     * named 'events' under the sensor test directory (part of external storage).
+     */
+    public void logCollectedEventsToFile(String fileName) throws IOException {
+        StringBuilder builder = new StringBuilder();
+        builder.append("Sensor='").append(mEnvironment.getSensor()).append("', ");
+        builder.append("SamplingRateOverloaded=")
+                .append(mEnvironment.isSensorSamplingRateOverloaded()).append(", ");
+        builder.append("RequestedSamplingPeriod=")
+                .append(mEnvironment.getRequestedSamplingPeriodUs()).append("us, ");
+        builder.append("MaxReportLatency=")
+                .append(mEnvironment.getMaxReportLatencyUs()).append("us");
+
+        synchronized (mCollectedEvents) {
+            for (TestSensorEvent event : mCollectedEvents) {
+                builder.append("\n");
+                builder.append("Timestamp=").append(event.timestamp).append("ns, ");
+                builder.append("ReceivedTimestamp=").append(event.receivedTimestamp).append("ns, ");
+                builder.append("Accuracy=").append(event.accuracy).append(", ");
+                builder.append("Values=").append(Arrays.toString(event.values));
+            }
+        }
+
+        File eventsDirectory = SensorCtsHelper.getSensorTestDataDirectory("events/");
+        File logFile = new File(eventsDirectory, fileName);
+        FileWriter fileWriter = new FileWriter(logFile, false /* append */);
+        try (BufferedWriter writer = new BufferedWriter(fileWriter)) {
+            writer.write(builder.toString());
+        }
     }
 
     /**
@@ -131,13 +183,24 @@
      * @throws AssertionError if there was a timeout after {@link #FLUSH_TIMEOUT_US} &micro;s
      */
     public void waitForFlushComplete() throws InterruptedException {
-        CountDownLatch latch = mFlushLatch;
-        if(latch == null) {
-            return;
+        clearEvents();
+        CountDownLatch latch = new CountDownLatch(1);
+        synchronized (mFlushLatches) {
+            mFlushLatches.add(latch);
         }
-        Assert.assertTrue(
-                SensorCtsHelper.formatAssertionMessage("WaitForFlush", mEnvironment),
-                latch.await(FLUSH_TIMEOUT_US, TimeUnit.MICROSECONDS));
+
+        try {
+            String message = SensorCtsHelper.formatAssertionMessage(
+                    "WaitForFlush",
+                    mEnvironment,
+                    "timeout=%dus",
+                    FLUSH_TIMEOUT_US);
+            Assert.assertTrue(message, latch.await(FLUSH_TIMEOUT_US, TimeUnit.MICROSECONDS));
+        } finally {
+            synchronized (mFlushLatches) {
+                mFlushLatches.remove(latch);
+            }
+        }
     }
 
     /**
@@ -146,22 +209,36 @@
      * @throws AssertionError if there was a timeout after {@link #FLUSH_TIMEOUT_US} &micro;s
      */
     public void waitForEvents(int eventCount) throws InterruptedException {
-        mEventLatch = new CountDownLatch(eventCount);
+        clearEvents();
+        CountDownLatch eventLatch = new CountDownLatch(eventCount);
+        synchronized (mEventLatches) {
+            mEventLatches.add(eventLatch);
+        }
+
         try {
-            int rateUs = mEnvironment.getExpectedSamplingPeriodUs();
-            // Timeout is 2 * event count * expected period + batch timeout + default wait
-            long timeoutUs = (2 * eventCount * rateUs)
+            long samplingPeriodUs = mEnvironment.getMaximumExpectedSamplingPeriodUs();
+            // timeout is 2 * event count * expected period + batch timeout + default wait
+            // we multiply by two as not to raise an error in this function even if the events are
+            // streaming at a lower rate than expected, as long as it's not streaming twice as slow
+            // as expected
+            long timeoutUs = (2 * eventCount * samplingPeriodUs)
                     + mEnvironment.getMaxReportLatencyUs()
                     + EVENT_TIMEOUT_US;
-            String message = SensorCtsHelper.formatAssertionMessage(
-                    "WaitForEvents",
-                    mEnvironment,
-                    "requested:%d, received:%d",
-                    eventCount,
-                    eventCount - mEventLatch.getCount());
-            Assert.assertTrue(message, mEventLatch.await(timeoutUs, TimeUnit.MICROSECONDS));
+            boolean success = eventLatch.await(timeoutUs, TimeUnit.MICROSECONDS);
+            if (!success) {
+                String message = SensorCtsHelper.formatAssertionMessage(
+                        "WaitForEvents",
+                        mEnvironment,
+                        "requested=%d, received=%d, timeout=%dus",
+                        eventCount,
+                        eventCount - eventLatch.getCount(),
+                        timeoutUs);
+                Assert.fail(message);
+            }
         } finally {
-            mEventLatch = null;
+            synchronized (mEventLatches) {
+                mEventLatches.remove(eventLatch);
+            }
         }
     }
 
@@ -171,4 +248,28 @@
     public void waitForEvents(long duration, TimeUnit timeUnit) throws InterruptedException {
         SensorCtsHelper.sleep(duration, timeUnit);
     }
+
+    /**
+     * Asserts that sensor events arrived in the proper thread if a {@link Handler} was associated
+     * with the current instance.
+     *
+     * If no events were received this assertion will be evaluated to {@code true}.
+     */
+    public void assertEventsReceivedInHandler() {
+        int eventsOutsideHandler = mEventsReceivedOutsideHandler.get();
+        String message = String.format(
+                "Events arrived outside the associated Looper. Expected=0, Found=%d",
+                eventsOutsideHandler);
+        Assert.assertEquals(message, 0 /* expected */, eventsOutsideHandler);
+    }
+
+    /**
+     * Keeps track of the number of events that arrived in a different {@link Looper} than the one
+     * associated with the {@link TestSensorEventListener}.
+     */
+    private void checkHandler() {
+        if (mHandler != null && mHandler.getLooper() != Looper.myLooper()) {
+            mEventsReceivedOutsideHandler.incrementAndGet();
+        }
+    }
 }
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 dc40ff4..fdd851e 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/TestSensorManager.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/TestSensorManager.java
@@ -19,35 +19,18 @@
 import junit.framework.Assert;
 
 import android.content.Context;
-import android.hardware.Sensor;
 import android.hardware.SensorEventListener;
-import android.hardware.SensorEventListener2;
 import android.hardware.SensorManager;
 import android.util.Log;
 
-import java.util.concurrent.TimeUnit;
-
 /**
- * A test class that performs the actions of {@link SensorManager} on a single sensor. This
- * class allows for a single sensor to be registered and unregistered as well as performing
- * operations such as flushing the sensor events and gathering events. This class also manages
- * performing the test verifications for the sensor manager.
- * <p>
- * This class requires that operations are performed in the following order:
- * <p><ul>
- * <li>{@link #registerListener(TestSensorEventListener)}</li>
- * <li>{@link #startFlush()}, {@link #waitForFlushCompleted()}, or {@link #flush()}.
- * <li>{@link #unregisterListener()}</li>
- * </ul><p>Or:</p><ul>
- * <li>{@link #runSensor(TestSensorEventListener, int)}</li>
- * </ul><p>Or:</p><ul>
- * <li>{@link #runSensor(TestSensorEventListener, long, TimeUnit)}</li>
- * </ul><p>
- * If methods are called outside of this order, they will print a warning to the log and then
- * return. Both {@link #runSensor(TestSensorEventListener, int)}} and
- * {@link #runSensor(TestSensorEventListener, long, TimeUnit)} will perform the appropriate
- * set up and tear down.
- * <p>
+ * A test class that performs the actions of {@link SensorManager} on a single sensor.
+ * This class allows for a single sensor to be registered and unregistered as well as performing
+ * operations such as flushing the sensor events and gathering events.
+ * This class also manages performing the test verifications for the sensor manager.
+ *
+ * NOTE: this class is expected to mirror {@link SensorManager} operations, and perform the
+ * required test verifications along with them.
  */
 public class TestSensorManager {
     private static final String LOG_TAG = "TestSensorManager";
@@ -55,7 +38,7 @@
     private final SensorManager mSensorManager;
     private final TestSensorEnvironment mEnvironment;
 
-    private TestSensorEventListener mTestSensorEventListener;
+    private volatile TestSensorEventListener mTestSensorEventListener;
 
     /**
      * @deprecated Use {@link #TestSensorManager(TestSensorEnvironment)} instead.
@@ -90,15 +73,14 @@
             return;
         }
 
-        mTestSensorEventListener = listener != null ? listener : new TestSensorEventListener();
-        mTestSensorEventListener.setEnvironment(mEnvironment);
-
+        mTestSensorEventListener = listener;
         String message = SensorCtsHelper.formatAssertionMessage("registerListener", mEnvironment);
         boolean result = mSensorManager.registerListener(
                 mTestSensorEventListener,
                 mEnvironment.getSensor(),
                 mEnvironment.getRequestedSamplingPeriodUs(),
-                mEnvironment.getMaxReportLatencyUs());
+                mEnvironment.getMaxReportLatencyUs(),
+                mTestSensorEventListener.getHandler());
         Assert.assertTrue(message, result);
     }
 
@@ -110,136 +92,24 @@
             Log.w(LOG_TAG, "No listener registered, returning.");
             return;
         }
-
-        mSensorManager.unregisterListener(
-                mTestSensorEventListener,
-                mEnvironment.getSensor());
+        mSensorManager.unregisterListener(mTestSensorEventListener, mEnvironment.getSensor());
+        mTestSensorEventListener.assertEventsReceivedInHandler();
         mTestSensorEventListener = null;
     }
 
     /**
-     * Wait for a specific number of events.
-     */
-    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) throws InterruptedException {
-        if (mTestSensorEventListener == null) {
-            Log.w(LOG_TAG, "No listener registered, returning.");
-            return;
-        }
-        mTestSensorEventListener.waitForEvents(duration, timeUnit);
-    }
-
-    /**
      * Call {@link SensorManager#flush(SensorEventListener)}. This method will perform a no-op if
      * the sensor is not registered.
      *
-     * @throws AssertionError if {@link SensorManager#flush(SensorEventListener)} returns false
+     * @throws AssertionError if {@link SensorManager#flush(SensorEventListener)} fails.
      */
-    public void startFlush() {
+    public void requestFlush() {
         if (mTestSensorEventListener == null) {
+            Log.w(LOG_TAG, "No listener registered, returning.");
             return;
         }
-
         Assert.assertTrue(
                 SensorCtsHelper.formatAssertionMessage("Flush", mEnvironment),
                 mSensorManager.flush(mTestSensorEventListener));
     }
-
-    /**
-     * Wait for {@link SensorEventListener2#onFlushCompleted(Sensor)} to be called. This method will
-     * perform a no-op if the sensor is not registered.
-     *
-     * @throws AssertionError if there is a time out
-     * @throws InterruptedException if the thread was interrupted
-     */
-    public void waitForFlushCompleted() throws InterruptedException {
-        if (mTestSensorEventListener == null) {
-            return;
-        }
-        mTestSensorEventListener.waitForFlushComplete();
-    }
-
-    /**
-     * Call {@link SensorManager#flush(SensorEventListener)} and wait for
-     * {@link SensorEventListener2#onFlushCompleted(Sensor)} to be called. This method will perform
-     * a no-op if the sensor is not registered.
-     *
-     * @throws AssertionError if {@link SensorManager#flush(SensorEventListener)} returns false or
-     * if there is a time out
-     * @throws InterruptedException if the thread was interrupted
-     */
-    public void flush() throws InterruptedException {
-        if (mTestSensorEventListener == null) {
-            return;
-        }
-        startFlush();
-        waitForFlushCompleted();
-    }
-
-    /**
-     * Register a listener, wait for a specific number of events, and then unregister the listener.
-     */
-    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);
-        } finally {
-            unregisterListener();
-        }
-    }
-
-    /**
-     * Register a listener, wait for a specific duration, and then unregister the listener.
-     */
-    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);
-        } finally {
-            unregisterListener();
-        }
-    }
-
-    /**
-     * Registers a listener, waits for a specific duration, calls flush, and waits for flush to
-     * complete.
-     */
-    public void runSensorAndFlush(
-            TestSensorEventListener listener,
-            long duration,
-            TimeUnit timeUnit) throws InterruptedException {
-        if (mTestSensorEventListener != null) {
-            Log.w(LOG_TAG, "Listener already registered, returning.");
-            return;
-        }
-
-        try {
-            registerListener(listener);
-            SensorCtsHelper.sleep(duration, timeUnit);
-            startFlush();
-            listener.waitForFlushComplete();
-        } finally {
-            unregisterListener();
-        }
-    }
 }
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/ValidatingSensorEventListener.java b/tests/tests/hardware/src/android/hardware/cts/helpers/ValidatingSensorEventListener.java
deleted file mode 100644
index 299f470..0000000
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/ValidatingSensorEventListener.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright (C) 2014 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 android.hardware.cts.helpers;
-
-import android.hardware.SensorEvent;
-import android.hardware.SensorEventListener2;
-import android.hardware.cts.helpers.sensorverification.ISensorVerification;
-
-import java.util.Collection;
-import java.util.LinkedList;
-
-/**
- * A {@link TestSensorEventListener} which performs validations on the received events on the fly.
- * This class is useful for long running tests where it is not practical to store all the events to
- * be processed after.
- */
-public class ValidatingSensorEventListener extends TestSensorEventListener {
-
-    private final Collection<ISensorVerification> mVerifications =
-            new LinkedList<ISensorVerification>();
-
-    /**
-     * Construct a {@link ValidatingSensorEventListener} with an additional
-     * {@link SensorEventListener2}.
-     */
-    public ValidatingSensorEventListener(SensorEventListener2 listener,
-            ISensorVerification ... verifications) {
-        super(listener);
-        for (ISensorVerification verification : verifications) {
-            mVerifications.add(verification);
-        }
-    }
-
-    /**
-     * Construct a {@link ValidatingSensorEventListener} with an additional
-     * {@link SensorEventListener2}.
-     */
-    public ValidatingSensorEventListener(SensorEventListener2 listener,
-            Collection<ISensorVerification> verifications) {
-        this(listener, verifications.toArray(new ISensorVerification[0]));
-    }
-
-    /**
-     * Construct a {@link ValidatingSensorEventListener}.
-     */
-    public ValidatingSensorEventListener(ISensorVerification ... verifications) {
-        this(null, verifications);
-    }
-
-    /**
-     * Construct a {@link ValidatingSensorEventListener}.
-     */
-    public ValidatingSensorEventListener(Collection<ISensorVerification> verifications) {
-        this(null, verifications);
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void onSensorChanged(SensorEvent event) {
-        TestSensorEvent testEvent = new TestSensorEvent(event);
-        for (ISensorVerification verification : mVerifications) {
-            verification.addSensorEvent(testEvent);
-        }
-        super.onSensorChanged(event);
-    }
-}
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/reporting/ISensorTestNode.java b/tests/tests/hardware/src/android/hardware/cts/helpers/reporting/ISensorTestNode.java
new file mode 100644
index 0000000..d34c0af
--- /dev/null
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/reporting/ISensorTestNode.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2014 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 android.hardware.cts.helpers.reporting;
+
+import android.hardware.cts.helpers.SensorTestPlatformException;
+
+/**
+ * Interface that represents a node in a hierarchy built by the sensor test platform.
+ */
+// TODO: this is an intermediate state to introduce a full-blown centralized recorder data produced
+//       by sensor tests
+public interface ISensorTestNode {
+
+    /**
+     * Provides a name (tag) that can be used to identify the current node.
+     */
+    String getName() throws SensorTestPlatformException;
+}
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/AbstractSensorOperation.java b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/AbstractSensorOperation.java
deleted file mode 100644
index 5b969f2..0000000
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/AbstractSensorOperation.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright (C) 2014 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 android.hardware.cts.helpers.sensoroperations;
-
-import android.hardware.cts.helpers.SensorStats;
-
-/**
- * A {@link ISensorOperation} which contains a common implementation for gathering
- * {@link SensorStats}.
- */
-public abstract class AbstractSensorOperation implements ISensorOperation {
-
-    private final SensorStats mStats = new SensorStats();
-
-    /**
-     * Wrapper around {@link SensorStats#addSensorStats(String, SensorStats)}
-     */
-    protected void addSensorStats(String key, SensorStats stats) {
-        mStats.addSensorStats(key, stats);
-    }
-
-    /**
-     * Wrapper around {@link SensorStats#addSensorStats(String, SensorStats)} that allows an index
-     * to be added. This is useful for {@link ISensorOperation}s that have many iterations or child
-     * operations. The key added is in the form {@code key + "_" + index} where index may be zero
-     * padded.
-     */
-    protected void addSensorStats(String key, int index, SensorStats stats) {
-        addSensorStats(String.format("%s_%03d", key, index), stats);
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public SensorStats getStats() {
-        return mStats;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public abstract ISensorOperation clone();
-
-}
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 88e4954..436a7cf 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
@@ -22,14 +22,14 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.hardware.cts.helpers.SensorStats;
+import android.hardware.cts.helpers.reporting.ISensorTestNode;
 import android.os.PowerManager;
 import android.os.PowerManager.WakeLock;
 
 import java.util.concurrent.TimeUnit;
 
 /**
- * An {@link ISensorOperation} which performs another {@link ISensorOperation} and then wakes up
+ * An {@link SensorOperation} which performs another {@link SensorOperation} and then wakes up
  * after a specified period of time and waits for the child operation to complete.
  * <p>
  * This operation can be used to allow the device to go to sleep and wake it up after a specified
@@ -40,11 +40,11 @@
  * but wake the device one time at the specified period.
  * </p>
  */
-public class AlarmOperation extends AbstractSensorOperation {
+public class AlarmOperation extends SensorOperation {
     private static final String ACTION = "AlarmOperationAction";
     private static final String WAKE_LOCK_TAG = "AlarmOperationWakeLock";
 
-    private final ISensorOperation mOperation;
+    private final SensorOperation mOperation;
     private final Context mContext;
     private final long mSleepDuration;
     private final TimeUnit mTimeUnit;
@@ -55,13 +55,17 @@
     /**
      * Constructor for {@link DelaySensorOperation}
      *
-     * @param operation the child {@link ISensorOperation} to perform after the delay
+     * @param operation the child {@link SensorOperation} to perform after the delay
      * @param context the context used to access the alarm manager
      * @param sleepDuration the amount of time to sleep
      * @param timeUnit the unit of the duration
      */
-    public AlarmOperation(ISensorOperation operation, Context context, long sleepDuration,
+    public AlarmOperation(
+            SensorOperation operation,
+            Context context,
+            long sleepDuration,
             TimeUnit timeUnit) {
+        super(operation.getStats());
         mOperation = operation;
         mContext = context;
         mSleepDuration = sleepDuration;
@@ -72,7 +76,7 @@
      * {@inheritDoc}
      */
     @Override
-    public void execute() throws InterruptedException {
+    public void execute(ISensorTestNode parent) throws InterruptedException {
         // Start alarm
         IntentFilter intentFilter = new IntentFilter(ACTION);
         BroadcastReceiver receiver = new BroadcastReceiver() {
@@ -92,7 +96,7 @@
 
         // Execute operation
         try {
-            mOperation.execute();
+            mOperation.execute(asTestNode(parent));
         } finally {
             releaseWakeLock();
         }
@@ -102,14 +106,6 @@
      * {@inheritDoc}
      */
     @Override
-    public SensorStats getStats() {
-        return mOperation.getStats();
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
     public AlarmOperation clone() {
         return new AlarmOperation(mOperation, mContext, mSleepDuration, mTimeUnit);
     }
@@ -120,7 +116,7 @@
      */
     private synchronized void acquireWakeLock() {
         // Don't acquire wake lock if the operation has already completed.
-        if (mCompleted == true || mWakeLock != null) {
+        if (mCompleted || mWakeLock != null) {
             return;
         }
         PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
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 b4d1f23..8c52222 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
@@ -17,30 +17,28 @@
 package android.hardware.cts.helpers.sensoroperations;
 
 import android.hardware.cts.helpers.SensorCtsHelper;
-import android.hardware.cts.helpers.SensorStats;
+import android.hardware.cts.helpers.reporting.ISensorTestNode;
 
 import java.util.concurrent.TimeUnit;
 
 /**
- * An {@link ISensorOperation} which delays for a specified period of time before performing another
- * {@link ISensorOperation}.
+ * An {@link SensorOperation} which delays for a specified period of time before performing another
+ * {@link SensorOperation}.
  */
-public class DelaySensorOperation implements ISensorOperation {
-    private final ISensorOperation mOperation;
+public class DelaySensorOperation extends SensorOperation {
+    private final SensorOperation mOperation;
     private final long mDelay;
     private final TimeUnit mTimeUnit;
 
     /**
      * Constructor for {@link DelaySensorOperation}
      *
-     * @param operation the child {@link ISensorOperation} to perform after the delay
+     * @param operation the child {@link SensorOperation} to perform after the delay
      * @param delay the amount of time to delay
      * @param timeUnit the unit of the delay
      */
-    public DelaySensorOperation(ISensorOperation operation, long delay, TimeUnit timeUnit) {
-        if (operation == null || timeUnit == null) {
-            throw new IllegalArgumentException("Arguments cannot be null");
-        }
+    public DelaySensorOperation(SensorOperation operation, long delay, TimeUnit timeUnit) {
+        super(operation.getStats());
         mOperation = operation;
         mDelay = delay;
         mTimeUnit = timeUnit;
@@ -50,17 +48,9 @@
      * {@inheritDoc}
      */
     @Override
-    public void execute() throws InterruptedException {
+    public void execute(ISensorTestNode parent) throws InterruptedException {
         SensorCtsHelper.sleep(mDelay, mTimeUnit);
-        mOperation.execute();
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public SensorStats getStats() {
-        return mOperation.getStats();
+        mOperation.execute(asTestNode(parent));
     }
 
     /**
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/FakeSensorOperation.java b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/FakeSensorOperation.java
index bb64dfa..238956b 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/FakeSensorOperation.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/FakeSensorOperation.java
@@ -17,16 +17,17 @@
 package android.hardware.cts.helpers.sensoroperations;
 
 import android.hardware.cts.helpers.SensorStats;
+import android.hardware.cts.helpers.reporting.ISensorTestNode;
 
 import junit.framework.Assert;
 
 import java.util.concurrent.TimeUnit;
 
 /**
- * A fake {@ISensorOperation} that will run for a specified time and then pass or fail. Useful when
- * debugging the framework.
+ * A fake {@link SensorOperation} that will run for a specified time and then pass or fail. Useful
+ * when debugging the framework.
  */
-public class FakeSensorOperation extends AbstractSensorOperation {
+public class FakeSensorOperation extends SensorOperation {
     private static final int NANOS_PER_MILLI = 1000000;
 
     private final boolean mFail;
@@ -56,7 +57,7 @@
      * {@inheritDoc}
      */
     @Override
-    public void execute() {
+    public void execute(ISensorTestNode parent) {
         long delayNs = TimeUnit.NANOSECONDS.convert(mDelay, mTimeUnit);
         try {
             Thread.sleep(delayNs / NANOS_PER_MILLI, (int) (delayNs % NANOS_PER_MILLI));
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
deleted file mode 100644
index 62a4e9e..0000000
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/ISensorOperation.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright (C) 2014 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 android.hardware.cts.helpers.sensoroperations;
-
-import android.hardware.cts.helpers.SensorStats;
-
-/**
- * Interface used by all sensor operations. This allows for complex operations such as chaining
- * operations together or running operations in parallel.
- * <p>
- * Certain restrictions exist for {@link ISensorOperation}s:
- * <p><ul>
- * <li>{@link #execute()} should only be called once and behavior is undefined for subsequent calls.
- * Once {@link #execute()} is called, the class should not be modified. Generally, there is no
- * synchronization for operations.</li>
- * <li>{@link #getStats()} should only be called after {@link #execute()}. If it is called before,
- * the returned value is undefined.</li>
- * <li>{@link #clone()} may be called any time and should return an operation with the same
- * parameters as the original.</li>
- * </ul>
- */
-public interface ISensorOperation {
-
-    /**
-     * 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() throws InterruptedException;
-
-    /**
-     * Get the stats for the operation.
-     *
-     * @return The {@link SensorStats} for the operation.
-     */
-    public SensorStats getStats();
-
-    /**
-     * Clones the {@link ISensorOperation}. The implementation should also clone all child
-     * operations, so that a cloned operation will run with the exact same parameters as the
-     * original. The stats should not be cloned.
-     *
-     * @return The cloned {@link ISensorOperation}
-     */
-    public ISensorOperation clone();
-}
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 5a4466c..ed70b70 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
@@ -19,10 +19,10 @@
 import junit.framework.Assert;
 
 import android.hardware.cts.helpers.SensorStats;
+import android.hardware.cts.helpers.reporting.ISensorTestNode;
 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;
@@ -33,14 +33,14 @@
 import java.util.concurrent.TimeoutException;
 
 /**
- * A {@link ISensorOperation} that executes a set of children {@link ISensorOperation}s in parallel.
+ * A {@link SensorOperation} that executes a set of children {@link SensorOperation}s in parallel.
  * The children are run in parallel but are given an index label in the order they are added. This
- * class can be combined to compose complex {@link ISensorOperation}s.
+ * class can be combined to compose complex {@link SensorOperation}s.
  */
-public class ParallelSensorOperation extends AbstractSensorOperation {
+public class ParallelSensorOperation extends SensorOperation {
     public static final String STATS_TAG = "parallel";
 
-    private final List<ISensorOperation> mOperations = new LinkedList<ISensorOperation>();
+    private final ArrayList<SensorOperation> mOperations = new ArrayList<>();
     private final Long mTimeout;
     private final TimeUnit mTimeUnit;
 
@@ -65,10 +65,10 @@
     }
 
     /**
-     * Add a set of {@link ISensorOperation}s.
+     * Add a set of {@link SensorOperation}s.
      */
-    public void add(ISensorOperation ... operations) {
-        for (ISensorOperation operation : operations) {
+    public void add(SensorOperation ... operations) {
+        for (SensorOperation operation : operations) {
             if (operation == null) {
                 throw new IllegalArgumentException("Arguments cannot be null");
             }
@@ -77,11 +77,11 @@
     }
 
     /**
-     * Executes the {@link ISensorOperation}s in parallel. If an exception occurs one or more
+     * Executes the {@link SensorOperation}s in parallel. If an exception occurs one or more
      * operations, the first exception will be thrown once all operations are completed.
      */
     @Override
-    public void execute() throws InterruptedException {
+    public void execute(final ISensorTestNode parent) throws InterruptedException {
         int operationsCount = mOperations.size();
         ThreadPoolExecutor executor = new ThreadPoolExecutor(
                 operationsCount,
@@ -92,12 +92,13 @@
         executor.allowCoreThreadTimeOut(true);
         executor.prestartAllCoreThreads();
 
-        ArrayList<Future<ISensorOperation>> futures = new ArrayList<Future<ISensorOperation>>();
-        for (final ISensorOperation operation : mOperations) {
-            Future<ISensorOperation> future = executor.submit(new Callable<ISensorOperation>() {
+        final ISensorTestNode currentNode = asTestNode(parent);
+        ArrayList<Future<SensorOperation>> futures = new ArrayList<>();
+        for (final SensorOperation operation : mOperations) {
+            Future<SensorOperation> future = executor.submit(new Callable<SensorOperation>() {
                 @Override
-                public ISensorOperation call() throws Exception {
-                    operation.execute();
+                public SensorOperation call() throws Exception {
+                    operation.execute(currentNode);
                     return operation;
                 }
             });
@@ -111,12 +112,12 @@
         }
 
         boolean hasAssertionErrors = false;
-        ArrayList<Integer> timeoutIndices = new ArrayList<Integer>();
-        ArrayList<Throwable> exceptions = new ArrayList<Throwable>();
+        ArrayList<Integer> timeoutIndices = new ArrayList<>();
+        ArrayList<Throwable> exceptions = new ArrayList<>();
         for (int i = 0; i < operationsCount; ++i) {
-            Future<ISensorOperation> future = futures.get(i);
+            Future<SensorOperation> future = futures.get(i);
             try {
-                ISensorOperation operation = getFutureResult(future, executionTimeNs);
+                SensorOperation operation = getFutureResult(future, executionTimeNs);
                 addSensorStats(STATS_TAG, i, operation.getStats());
             } catch (ExecutionException e) {
                 // extract the exception thrown by the worker thread
@@ -151,7 +152,7 @@
     @Override
     public ParallelSensorOperation clone() {
         ParallelSensorOperation operation = new ParallelSensorOperation();
-        for (ISensorOperation subOperation : mOperations) {
+        for (SensorOperation subOperation : mOperations) {
             operation.add(subOperation.clone());
         }
         return operation;
@@ -160,7 +161,7 @@
     /**
      * Helper method that waits for a {@link Future} to complete, and returns its result.
      */
-    private ISensorOperation getFutureResult(Future<ISensorOperation> future, Long timeoutNs)
+    private SensorOperation getFutureResult(Future<SensorOperation> future, Long timeoutNs)
             throws ExecutionException, TimeoutException, InterruptedException {
         if (timeoutNs == null) {
             return future.get();
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 3d682fe..5b333b8 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
@@ -17,42 +17,44 @@
 package android.hardware.cts.helpers.sensoroperations;
 
 import android.hardware.cts.helpers.SensorStats;
+import android.hardware.cts.helpers.SensorTestPlatformException;
+import android.hardware.cts.helpers.reporting.ISensorTestNode;
 
 /**
- * A {@link ISensorOperation} that executes a single {@link ISensorOperation} a given number of
- * times. This class can be combined to compose complex {@link ISensorOperation}s.
+ * A {@link SensorOperation} that executes a single {@link SensorOperation} a given number of
+ * times. This class can be combined to compose complex {@link SensorOperation}s.
  */
-public class RepeatingSensorOperation extends AbstractSensorOperation {
+public class RepeatingSensorOperation extends SensorOperation {
     public static final String STATS_TAG = "repeating";
 
-    private final ISensorOperation mOperation;
+    private final SensorOperation mOperation;
     private final int mIterations;
 
     /**
      * Constructor for {@link RepeatingSensorOperation}.
      *
-     * @param operation the {@link ISensorOperation} to run.
+     * @param operation the {@link SensorOperation} to run.
      * @param iterations the number of iterations to run the operation for.
      */
-    public RepeatingSensorOperation(ISensorOperation operation, int iterations) {
+    public RepeatingSensorOperation(SensorOperation operation, int iterations) {
         if (operation == null) {
             throw new IllegalArgumentException("Arguments cannot be null");
         }
         mOperation = operation;
         mIterations = iterations;
-
     }
 
     /**
-     * Executes the {@link ISensorOperation}s the given number of times. If an exception occurs
-     * in one iterations, it is thrown and all subsequent iterations will not run.
+     * Executes the {@link SensorOperation}s the given number of times. If an exception occurs in
+     * one iterations, it is thrown and all subsequent iterations will not run.
      */
     @Override
-    public void execute() throws InterruptedException {
+    public void execute(ISensorTestNode parent) throws InterruptedException {
+        ISensorTestNode currentNode = asTestNode(parent);
         for(int i = 0; i < mIterations; ++i) {
-            ISensorOperation operation = mOperation.clone();
+            SensorOperation operation = mOperation.clone();
             try {
-                operation.execute();
+                operation.execute(new TestNode(currentNode, i));
             } catch (AssertionError e) {
                 String msg = String.format("Iteration %d failed: \"%s\"", i, e.getMessage());
                 getStats().addValue(SensorStats.ERROR, msg);
@@ -70,4 +72,19 @@
     public RepeatingSensorOperation clone() {
         return new RepeatingSensorOperation(mOperation.clone(), mIterations);
     }
+
+    private class TestNode implements ISensorTestNode {
+        private final ISensorTestNode mTestNode;
+        private final int mIteration;
+
+        public TestNode(ISensorTestNode parent, int iteration) {
+            mTestNode = asTestNode(parent);
+            mIteration = iteration;
+        }
+
+        @Override
+        public String getName() throws SensorTestPlatformException {
+            return String.format("%s-iteration%d", mTestNode.getName(), mIteration);
+        }
+    }
 }
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/SensorOperation.java b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/SensorOperation.java
new file mode 100644
index 0000000..66604d3
--- /dev/null
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/SensorOperation.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2014 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 android.hardware.cts.helpers.sensoroperations;
+
+import android.hardware.cts.helpers.SensorStats;
+import android.hardware.cts.helpers.SensorTestPlatformException;
+import android.hardware.cts.helpers.reporting.ISensorTestNode;
+
+/**
+ * Base class used by all sensor operations. This allows for complex operations such as chaining
+ * operations together or running operations in parallel.
+ * <p>
+ * Certain restrictions exist for {@link SensorOperation}s:
+ * <p><ul>
+ * <li>{@link #execute(ISensorTestNode)} should only be called once and behavior is undefined for
+ * subsequent calls.
+ * Once {@link #execute(ISensorTestNode)} is called, the class should not be modified. Generally,
+ * there is no synchronization for operations.</li>
+ * <li>{@link #getStats()} should only be called after {@link #execute(ISensorTestNode)}. If it
+ * is called before, the returned value is undefined.</li>
+ * <li>{@link #clone()} may be called any time and should return an operation with the same
+ * parameters as the original.</li>
+ * </ul>
+ */
+public abstract class SensorOperation {
+    private final SensorStats mStats;
+
+    protected SensorOperation() {
+        this(new SensorStats());
+    }
+
+    protected SensorOperation(SensorStats stats) {
+        mStats = stats;
+    }
+
+    /**
+     * @return The {@link SensorStats} for the operation.
+     */
+    public SensorStats getStats() {
+        return mStats;
+    }
+
+    /**
+     * 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 abstract void execute(ISensorTestNode parent) throws InterruptedException;
+
+    /**
+     * @return The cloned {@link SensorOperation}.
+     *
+     * NOTE: The implementation should also clone all child operations, so that a cloned operation
+     * will run with the exact same parameters as the original. The stats should not be cloned.
+     */
+    public abstract SensorOperation clone();
+
+    /**
+     * Wrapper around {@link SensorStats#addSensorStats(String, SensorStats)}
+     */
+    protected void addSensorStats(String key, SensorStats stats) {
+        getStats().addSensorStats(key, stats);
+    }
+
+    /**
+     * Wrapper around {@link SensorStats#addSensorStats(String, SensorStats)} that allows an index
+     * to be added. This is useful for {@link SensorOperation}s that have many iterations or child
+     * operations. The key added is in the form {@code key + "_" + index} where index may be zero
+     * padded.
+     */
+    protected void addSensorStats(String key, int index, SensorStats stats) {
+        addSensorStats(String.format("%s_%03d", key, index), stats);
+    }
+
+    protected ISensorTestNode asTestNode(ISensorTestNode parent) {
+        return new SensorTestNode(parent, this);
+    }
+
+    private class SensorTestNode implements ISensorTestNode {
+        private final ISensorTestNode mParent;
+        private final SensorOperation mOperation;
+
+        public SensorTestNode(ISensorTestNode parent, SensorOperation operation) {
+            mParent = parent;
+            mOperation = operation;
+        }
+
+        @Override
+        public String getName() throws SensorTestPlatformException {
+            return mParent.getName() + "-" + mOperation.getClass().getSimpleName();
+        }
+    }
+}
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 bc48725..30da9a0 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
@@ -19,18 +19,27 @@
 import junit.framework.TestCase;
 
 import android.hardware.cts.helpers.SensorStats;
+import android.hardware.cts.helpers.SensorTestPlatformException;
+import android.hardware.cts.helpers.reporting.ISensorTestNode;
 
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
 /**
- * Tests for the primitive {@link ISensorOperation}s including {@link DelaySensorOperation},
+ * Tests for the primitive {@link SensorOperation}s including {@link DelaySensorOperation},
  * {@link ParallelSensorOperation}, {@link RepeatingSensorOperation} and
  * {@link SequentialSensorOperation}.
  */
 public class SensorOperationTest extends TestCase {
     private static final long TEST_DURATION_THRESHOLD_MS = TimeUnit.SECONDS.toMillis(5);
 
+    private final ISensorTestNode mTestNode = new ISensorTestNode() {
+        @Override
+        public String getName() throws SensorTestPlatformException {
+            return "SensorOperationUnitTest";
+        }
+    };
+
     /**
      * Test that the {@link FakeSensorOperation} functions correctly. Other tests in this class
      * rely on this operation.
@@ -38,18 +47,18 @@
     public void testFakeSensorOperation() throws InterruptedException {
         final int opDurationMs = 100;
 
-        ISensorOperation op = new FakeSensorOperation(opDurationMs, TimeUnit.MILLISECONDS);
+        SensorOperation op = new FakeSensorOperation(opDurationMs, TimeUnit.MILLISECONDS);
 
         assertFalse(op.getStats().flatten().containsKey("executed"));
         long start = System.currentTimeMillis();
-        op.execute();
+        op.execute(mTestNode);
         long duration = System.currentTimeMillis() - start;
         assertTrue(Math.abs(opDurationMs - duration) < TEST_DURATION_THRESHOLD_MS);
         assertTrue(op.getStats().flatten().containsKey("executed"));
 
         op = new FakeSensorOperation(true, 0, TimeUnit.MILLISECONDS);
         try {
-            op.execute();
+            op.execute(mTestNode);
             fail("AssertionError expected");
         } catch (AssertionError e) {
             // Expected
@@ -65,12 +74,12 @@
         final int subOpDurationMs = 100;
 
         FakeSensorOperation subOp = new FakeSensorOperation(subOpDurationMs, TimeUnit.MILLISECONDS);
-        ISensorOperation op = new DelaySensorOperation(subOp, opDurationMs, TimeUnit.MILLISECONDS);
+        SensorOperation op = new DelaySensorOperation(subOp, opDurationMs, TimeUnit.MILLISECONDS);
 
         long startMs = System.currentTimeMillis();
-        op.execute();
-        long dirationMs = System.currentTimeMillis() - startMs;
-        long durationDeltaMs = Math.abs(opDurationMs + subOpDurationMs - dirationMs);
+        op.execute(mTestNode);
+        long durationMs = System.currentTimeMillis() - startMs;
+        long durationDeltaMs = Math.abs(opDurationMs + subOpDurationMs - durationMs);
         assertTrue(durationDeltaMs < TEST_DURATION_THRESHOLD_MS);
     }
 
@@ -83,7 +92,7 @@
 
         ParallelSensorOperation op = new ParallelSensorOperation();
         for (int i = 0; i < subOpCount; i++) {
-            ISensorOperation subOp = new FakeSensorOperation(subOpDurationMs,
+            SensorOperation subOp = new FakeSensorOperation(subOpDurationMs,
                     TimeUnit.MILLISECONDS);
             op.add(subOp);
         }
@@ -92,7 +101,7 @@
         assertEquals(0, statsKeys.size());
 
         long start = System.currentTimeMillis();
-        op.execute();
+        op.execute(mTestNode);
         long durationMs = System.currentTimeMillis() - start;
         long durationDeltaMs = Math.abs(subOpDurationMs - durationMs);
         String message = String.format(
@@ -124,7 +133,7 @@
         ParallelSensorOperation op = new ParallelSensorOperation();
         for (int i = 0; i < subOpCount; i++) {
             // Trigger failures in the 5th, 55th operations at t=5ms, t=55ms
-            ISensorOperation subOp = new FakeSensorOperation(i % 50 == 5, i, TimeUnit.MILLISECONDS);
+            SensorOperation subOp = new FakeSensorOperation(i % 50 == 5, i, TimeUnit.MILLISECONDS);
             op.add(subOp);
         }
 
@@ -132,7 +141,7 @@
         assertEquals(0, statsKeys.size());
 
         try {
-            op.execute();
+            op.execute(mTestNode);
             fail("AssertionError expected");
         } catch (AssertionError e) {
             // Expected
@@ -164,7 +173,7 @@
         ParallelSensorOperation op = new ParallelSensorOperation(1, TimeUnit.SECONDS);
         for (int i = 0; i < subOpCount; i++) {
             // Trigger timeouts in the 5th, 55th operations (5 seconds vs 1 seconds)
-            ISensorOperation subOp = new FakeSensorOperation(i % 50 == 5 ? 5 : 0, TimeUnit.SECONDS);
+            SensorOperation subOp = new FakeSensorOperation(i % 50 == 5 ? 5 : 0, TimeUnit.SECONDS);
             op.add(subOp);
         }
 
@@ -172,7 +181,7 @@
         assertEquals(0, statsKeys.size());
 
         try {
-            op.execute();
+            op.execute(mTestNode);
             fail("AssertionError expected");
         } catch (AssertionError e) {
             // Expected
@@ -196,14 +205,14 @@
         final int iterations = 10;
         final int subOpDurationMs = 100;
 
-        ISensorOperation subOp = new FakeSensorOperation(subOpDurationMs, TimeUnit.MILLISECONDS);
-        ISensorOperation op = new RepeatingSensorOperation(subOp, iterations);
+        SensorOperation subOp = new FakeSensorOperation(subOpDurationMs, TimeUnit.MILLISECONDS);
+        SensorOperation op = new RepeatingSensorOperation(subOp, iterations);
 
         Set<String> statsKeys = op.getStats().flatten().keySet();
         assertEquals(0, statsKeys.size());
 
         long start = System.currentTimeMillis();
-        op.execute();
+        op.execute(mTestNode);
         long duration = System.currentTimeMillis() - start;
         assertTrue(Math.abs(subOpDurationMs * iterations - duration) < TEST_DURATION_THRESHOLD_MS);
 
@@ -223,13 +232,13 @@
         final int iterations = 100;
         final int failCount = 75;
 
-        ISensorOperation subOp = new FakeSensorOperation(0, TimeUnit.MILLISECONDS) {
+        SensorOperation subOp = new FakeSensorOperation(0, TimeUnit.MILLISECONDS) {
             private int mExecutedCount = 0;
             private SensorStats mFakeStats = new SensorStats();
 
             @Override
-            public void execute() {
-                super.execute();
+            public void execute(ISensorTestNode parent) {
+                super.execute(parent);
                 mExecutedCount++;
 
                 if (failCount == mExecutedCount) {
@@ -249,13 +258,13 @@
                 return mFakeStats;
             }
         };
-        ISensorOperation op = new RepeatingSensorOperation(subOp, iterations);
+        SensorOperation op = new RepeatingSensorOperation(subOp, iterations);
 
         Set<String> statsKeys = op.getStats().flatten().keySet();
         assertEquals(0, statsKeys.size());
 
         try {
-            op.execute();
+            op.execute(mTestNode);
             fail("AssertionError expected");
         } catch (AssertionError e) {
             // Expected
@@ -283,7 +292,7 @@
 
         SequentialSensorOperation op = new SequentialSensorOperation();
         for (int i = 0; i < subOpCount; i++) {
-            ISensorOperation subOp = new FakeSensorOperation(subOpDurationMs,
+            SensorOperation subOp = new FakeSensorOperation(subOpDurationMs,
                     TimeUnit.MILLISECONDS);
             op.add(subOp);
         }
@@ -292,7 +301,7 @@
         assertEquals(0, statsKeys.size());
 
         long start = System.currentTimeMillis();
-        op.execute();
+        op.execute(mTestNode);
         long duration = System.currentTimeMillis() - start;
         assertTrue(Math.abs(subOpDurationMs * subOpCount - duration) < TEST_DURATION_THRESHOLD_MS);
 
@@ -315,7 +324,7 @@
         SequentialSensorOperation op = new SequentialSensorOperation();
         for (int i = 0; i < subOpCount; i++) {
             // Trigger a failure in the 75th operation only
-            ISensorOperation subOp = new FakeSensorOperation(i + 1 == failCount, 0,
+            SensorOperation subOp = new FakeSensorOperation(i + 1 == failCount, 0,
                     TimeUnit.MILLISECONDS);
             op.add(subOp);
         }
@@ -324,7 +333,7 @@
         assertEquals(0, statsKeys.size());
 
         try {
-            op.execute();
+            op.execute(mTestNode);
             fail("AssertionError expected");
         } catch (AssertionError e) {
             // Expected
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 2ed0ca6..847c0f2 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
@@ -17,25 +17,25 @@
 package android.hardware.cts.helpers.sensoroperations;
 
 import android.hardware.cts.helpers.SensorStats;
+import android.hardware.cts.helpers.reporting.ISensorTestNode;
 
-import java.util.LinkedList;
-import java.util.List;
+import java.util.ArrayList;
 
 /**
- * A {@link ISensorOperation} that executes a set of children {@link ISensorOperation}s in a
+ * A {@link SensorOperation} that executes a set of children {@link SensorOperation}s in a
  * sequence. The children are executed in the order they are added. This class can be combined to
- * compose complex {@link ISensorOperation}s.
+ * compose complex {@link SensorOperation}s.
  */
-public class SequentialSensorOperation extends AbstractSensorOperation {
+public class SequentialSensorOperation extends SensorOperation {
     public static final String STATS_TAG = "sequential";
 
-    private final List<ISensorOperation> mOperations = new LinkedList<ISensorOperation>();
+    private final ArrayList<SensorOperation> mOperations = new ArrayList<>();
 
     /**
-     * Add a set of {@link ISensorOperation}s.
+     * Add a set of {@link SensorOperation}s.
      */
-    public void add(ISensorOperation ... operations) {
-        for (ISensorOperation operation : operations) {
+    public void add(SensorOperation ... operations) {
+        for (SensorOperation operation : operations) {
             if (operation == null) {
                 throw new IllegalArgumentException("Arguments cannot be null");
             }
@@ -44,15 +44,16 @@
     }
 
     /**
-     * Executes the {@link ISensorOperation}s in the order they were added. If an exception occurs
+     * Executes the {@link SensorOperation}s in the order they were added. If an exception occurs
      * in one operation, it is thrown and all subsequent operations will not run.
      */
     @Override
-    public void execute() throws InterruptedException {
+    public void execute(ISensorTestNode parent) throws InterruptedException {
+        ISensorTestNode currentNode = asTestNode(parent);
         for (int i = 0; i < mOperations.size(); i++) {
-            ISensorOperation operation = mOperations.get(i);
+            SensorOperation operation = mOperations.get(i);
             try {
-                operation.execute();
+                operation.execute(currentNode);
             } catch (AssertionError e) {
                 String msg = String.format("Operation %d failed: \"%s\"", i, e.getMessage());
                 getStats().addValue(SensorStats.ERROR, msg);
@@ -69,7 +70,7 @@
     @Override
     public SequentialSensorOperation clone() {
         SequentialSensorOperation operation = new SequentialSensorOperation();
-        for (ISensorOperation subOperation : mOperations) {
+        for (SensorOperation subOperation : mOperations) {
             operation.add(subOperation.clone());
         }
         return operation;
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
deleted file mode 100644
index d5aa4b9..0000000
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/TestSensorFlushOperation.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright (C) 2014 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 android.hardware.cts.helpers.sensoroperations;
-
-import android.hardware.cts.helpers.TestSensorEnvironment;
-import android.hardware.cts.helpers.TestSensorEventListener;
-
-import java.util.concurrent.TimeUnit;
-
-/**
- * A {@link ISensorOperation} used to verify that sensor events and sensor values are correct.
- * <p>
- * Provides methods to set test expectations as well as providing a set of default expectations
- * depending on sensor type.  When {{@link #execute()} is called, the sensor will collect the
- * events, call flush, and then run all the tests.
- * </p>
- */
-public class TestSensorFlushOperation extends VerifiableSensorOperation {
-    private final Long mDuration;
-    private final TimeUnit mTimeUnit;
-
-    /**
-     * Create a {@link TestSensorOperation}.
-     *
-     * @param environment the test environment
-     * @param duration the duration to gather events before calling {@code SensorManager.flush()}
-     * @param timeUnit the time unit of the duration
-     */
-    public TestSensorFlushOperation(
-            TestSensorEnvironment environment,
-            long duration,
-            TimeUnit timeUnit) {
-        super(environment);
-        mDuration = duration;
-        mTimeUnit = timeUnit;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    protected void doExecute(TestSensorEventListener listener) throws InterruptedException {
-        mSensorManager.runSensorAndFlush(listener, mDuration, mTimeUnit);
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    protected VerifiableSensorOperation doClone() {
-        return new TestSensorFlushOperation(mEnvironment, 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 695e1a7..901216a 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
@@ -16,83 +16,264 @@
 
 package android.hardware.cts.helpers.sensoroperations;
 
-import android.hardware.cts.helpers.TestSensorEnvironment;
-import android.hardware.cts.helpers.TestSensorEventListener;
+import junit.framework.Assert;
 
+import android.hardware.cts.helpers.SensorCtsHelper;
+import android.hardware.cts.helpers.SensorStats;
+import android.hardware.cts.helpers.SensorTestPlatformException;
+import android.hardware.cts.helpers.TestSensorEnvironment;
+import android.hardware.cts.helpers.TestSensorEvent;
+import android.hardware.cts.helpers.TestSensorEventListener;
+import android.hardware.cts.helpers.TestSensorManager;
+import android.hardware.cts.helpers.reporting.ISensorTestNode;
+import android.hardware.cts.helpers.sensorverification.EventGapVerification;
+import android.hardware.cts.helpers.sensorverification.EventOrderingVerification;
+import android.hardware.cts.helpers.sensorverification.EventTimestampSynchronizationVerification;
+import android.hardware.cts.helpers.sensorverification.FrequencyVerification;
+import android.hardware.cts.helpers.sensorverification.ISensorVerification;
+import android.hardware.cts.helpers.sensorverification.JitterVerification;
+import android.hardware.cts.helpers.sensorverification.MagnitudeVerification;
+import android.hardware.cts.helpers.sensorverification.MeanVerification;
+import android.hardware.cts.helpers.sensorverification.StandardDeviationVerification;
+import android.os.Handler;
+import android.util.Log;
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.List;
 import java.util.concurrent.TimeUnit;
 
 /**
- * A {@link ISensorOperation} used to verify that sensor events and sensor values are correct.
+ * A {@link SensorOperation} used to verify that sensor events and sensor values are correct.
  * <p>
  * Provides methods to set test expectations as well as providing a set of default expectations
- * depending on sensor type.  When {{@link #execute()} is called, the sensor will collect the
- * events and then run all the tests.
+ * depending on sensor type.  When {{@link #execute(ISensorTestNode)} is called, the sensor will
+ * collect the events and then run all the tests.
  * </p>
  */
-public class TestSensorOperation extends VerifiableSensorOperation {
-    private final Integer mEventCount;
-    private final Long mDuration;
-    private final TimeUnit mTimeUnit;
+public class TestSensorOperation extends SensorOperation {
+    private static final String TAG = "TestSensorOperation";
+
+    private final HashSet<ISensorVerification> mVerifications = new HashSet<>();
+
+    private final TestSensorManager mSensorManager;
+    private final TestSensorEnvironment mEnvironment;
+    private final Executor mExecutor;
+    private final Handler mHandler;
 
     /**
-     * Create a {@link TestSensorOperation}.
-     *
-     * @param environment the test environment
-     * @param eventCount the number of events to gather
+     * An interface that defines an abstraction for operations to be performed by the
+     * {@link TestSensorOperation}.
      */
-    public TestSensorOperation(TestSensorEnvironment environment, int eventCount) {
-        this(environment, eventCount, null /* duration */, null /* timeUnit */);
+    public interface Executor {
+        void execute(TestSensorManager sensorManager, TestSensorEventListener listener)
+                throws InterruptedException;
     }
 
     /**
      * Create a {@link TestSensorOperation}.
-     *
-     * @param environment the test environment
-     * @param duration the duration to gather events for
-     * @param timeUnit the time unit of the duration
+     */
+    public TestSensorOperation(TestSensorEnvironment environment, Executor executor) {
+        this(environment, executor, null /* handler */);
+    }
+
+    /**
+     * Create a {@link TestSensorOperation}.
      */
     public TestSensorOperation(
             TestSensorEnvironment environment,
-            long duration,
-            TimeUnit timeUnit) {
-        this(environment, null /* eventCount */, duration, timeUnit);
+            Executor executor,
+            Handler handler) {
+        mEnvironment = environment;
+        mExecutor = executor;
+        mHandler = handler;
+        mSensorManager = new TestSensorManager(mEnvironment);
     }
 
     /**
-     * Private helper constructor.
+     * Set all of the default test expectations.
      */
-    private TestSensorOperation(
+    public void addDefaultVerifications() {
+        addVerification(EventGapVerification.getDefault(mEnvironment));
+        addVerification(EventOrderingVerification.getDefault(mEnvironment));
+        addVerification(FrequencyVerification.getDefault(mEnvironment));
+        addVerification(JitterVerification.getDefault(mEnvironment));
+        addVerification(MagnitudeVerification.getDefault(mEnvironment));
+        addVerification(MeanVerification.getDefault(mEnvironment));
+        addVerification(StandardDeviationVerification.getDefault(mEnvironment));
+        addVerification(EventTimestampSynchronizationVerification.getDefault(mEnvironment));
+    }
+
+    public void addVerification(ISensorVerification verification) {
+        if (verification != null) {
+            mVerifications.add(verification);
+        }
+    }
+
+    /**
+     * Collect the specified number of events from the sensor and run all enabled verifications.
+     */
+    @Override
+    public void execute(ISensorTestNode parent) throws InterruptedException {
+        getStats().addValue("sensor_name", mEnvironment.getSensor().getName());
+        TestSensorEventListener listener = new TestSensorEventListener(mEnvironment, mHandler);
+        mExecutor.execute(mSensorManager, listener);
+
+        boolean failed = false;
+        StringBuilder sb = new StringBuilder();
+        List<TestSensorEvent> collectedEvents = listener.getCollectedEvents();
+        for (ISensorVerification verification : mVerifications) {
+            failed |= evaluateResults(collectedEvents, verification, sb);
+        }
+
+        if (failed) {
+            trySaveCollectedEvents(parent, listener);
+
+            String msg = SensorCtsHelper
+                    .formatAssertionMessage("VerifySensorOperation", mEnvironment, sb.toString());
+            getStats().addValue(SensorStats.ERROR, msg);
+            Assert.fail(msg);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public TestSensorOperation clone() {
+        TestSensorOperation operation = new TestSensorOperation(mEnvironment, mExecutor);
+        for (ISensorVerification verification : mVerifications) {
+            operation.addVerification(verification.clone());
+        }
+        return operation;
+    }
+
+    /**
+     * Evaluate the results of a test, aggregate the stats, and build the error message.
+     */
+    private boolean evaluateResults(
+            List<TestSensorEvent> events,
+            ISensorVerification verification,
+            StringBuilder sb) {
+        try {
+            // this is an intermediate state in refactoring, at some point verifications might
+            // become stateless
+            verification.addSensorEvents(events);
+            verification.verify(mEnvironment, getStats());
+        } catch (AssertionError e) {
+            if (sb.length() > 0) {
+                sb.append(", ");
+            }
+            sb.append(e.getMessage());
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Tries to save collected {@link TestSensorEvent}s to a file.
+     *
+     * NOTE: it is more important to handle verifications and its results, than failing if the file
+     * cannot be created. So we silently fail if necessary.
+     */
+    private void trySaveCollectedEvents(ISensorTestNode parent, TestSensorEventListener listener) {
+        String sanitizedFileName;
+        try {
+            String fileName = asTestNode(parent).getName();
+            sanitizedFileName = String.format(
+                    "%s-%s-%s_%dus.txt",
+                    SensorCtsHelper.sanitizeStringForFileName(fileName),
+                    SensorStats.getSanitizedSensorName(mEnvironment.getSensor()),
+                    mEnvironment.getFrequencyString(),
+                    mEnvironment.getMaxReportLatencyUs());
+        } catch (SensorTestPlatformException e) {
+            Log.w(TAG, "Unable to generate file name to save collected events", e);
+            return;
+        }
+
+        try {
+            listener.logCollectedEventsToFile(sanitizedFileName);
+        } catch (IOException e) {
+            Log.w(TAG, "Unable to save collected events to file: " + sanitizedFileName, e);
+        }
+    }
+
+    /**
+     * Creates an operation that will wait for a given amount of events to arrive.
+     *
+     * @param environment The test environment.
+     * @param eventCount The number of events to wait for.
+     */
+    public static TestSensorOperation createOperation(
             TestSensorEnvironment environment,
-            Integer eventCount,
-            Long duration,
-            TimeUnit timeUnit) {
-        super(environment);
-        mEventCount = eventCount;
-        mDuration = duration;
-        mTimeUnit = timeUnit;
+            final int eventCount) {
+        Executor executor = new Executor() {
+            @Override
+            public void execute(TestSensorManager sensorManager, TestSensorEventListener listener)
+                    throws InterruptedException {
+                try {
+                    sensorManager.registerListener(listener);
+                    listener.waitForEvents(eventCount);
+                } finally {
+                    sensorManager.unregisterListener();
+                }
+            }
+        };
+        return new TestSensorOperation(environment, executor);
     }
 
     /**
-     * {@inheritDoc}
+     * Creates an operation that will wait for a given amount of time to collect events.
+     *
+     * @param environment The test environment.
+     * @param duration The duration to wait for events.
+     * @param timeUnit The time unit for {@code duration}.
      */
-    @Override
-    protected void doExecute(TestSensorEventListener listener) throws InterruptedException {
-        if (mEventCount != null) {
-            mSensorManager.runSensor(listener, mEventCount);
-        } else {
-            mSensorManager.runSensor(listener, mDuration, mTimeUnit);
-        }
+    public static TestSensorOperation createOperation(
+            TestSensorEnvironment environment,
+            final long duration,
+            final TimeUnit timeUnit) {
+        Executor executor = new Executor() {
+            @Override
+            public void execute(TestSensorManager sensorManager, TestSensorEventListener listener)
+                    throws InterruptedException {
+                try {
+                    sensorManager.registerListener(listener);
+                    listener.waitForEvents(duration, timeUnit);
+                } finally {
+                    sensorManager.unregisterListener();
+                }
+            }
+        };
+        return new TestSensorOperation(environment, executor);
     }
 
     /**
-     * {@inheritDoc}
+     * Creates an operation that will wait for a given amount of time before calling
+     * {@link TestSensorManager#requestFlush()}.
+     *
+     * @param environment The test environment.
+     * @param duration The duration to wait before calling {@link TestSensorManager#requestFlush()}.
+     * @param timeUnit The time unit for {@code duration}.
      */
-    @Override
-    protected VerifiableSensorOperation doClone() {
-        if (mEventCount != null) {
-            return new TestSensorOperation(mEnvironment, mEventCount);
-        } else {
-            return new TestSensorOperation(mEnvironment, mDuration, mTimeUnit);
-        }
+    public static TestSensorOperation createFlushOperation(
+            TestSensorEnvironment environment,
+            final long duration,
+            final TimeUnit timeUnit) {
+        Executor executor = new Executor() {
+            @Override
+            public void execute(TestSensorManager sensorManager, TestSensorEventListener listener)
+                    throws InterruptedException {
+                try {
+                    sensorManager.registerListener(listener);
+                    SensorCtsHelper.sleep(duration, timeUnit);
+                    sensorManager.requestFlush();
+                    listener.waitForFlushComplete();
+                } finally {
+                    sensorManager.unregisterListener();
+                }
+            }
+        };
+        return new TestSensorOperation(environment, executor);
     }
 }
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
deleted file mode 100644
index 57018eb..0000000
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/VerifiableSensorOperation.java
+++ /dev/null
@@ -1,156 +0,0 @@
-/*
- * Copyright (C) 2014 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 android.hardware.cts.helpers.sensoroperations;
-
-import junit.framework.Assert;
-
-import android.hardware.cts.helpers.SensorCtsHelper;
-import android.hardware.cts.helpers.SensorStats;
-import android.hardware.cts.helpers.TestSensorEnvironment;
-import android.hardware.cts.helpers.TestSensorEventListener;
-import android.hardware.cts.helpers.TestSensorManager;
-import android.hardware.cts.helpers.ValidatingSensorEventListener;
-import android.hardware.cts.helpers.sensorverification.EventGapVerification;
-import android.hardware.cts.helpers.sensorverification.EventOrderingVerification;
-import android.hardware.cts.helpers.sensorverification.FrequencyVerification;
-import android.hardware.cts.helpers.sensorverification.ISensorVerification;
-import android.hardware.cts.helpers.sensorverification.JitterVerification;
-import android.hardware.cts.helpers.sensorverification.MagnitudeVerification;
-import android.hardware.cts.helpers.sensorverification.MeanVerification;
-import android.hardware.cts.helpers.sensorverification.StandardDeviationVerification;
-
-import java.util.Collection;
-import java.util.HashSet;
-
-/**
- * A {@link ISensorOperation} used to verify that sensor events and sensor values are correct.
- * <p>
- * Provides methods to set test expectations as well as providing a set of default expectations
- * depending on sensor type.  When {{@link #execute()} is called, the sensor will collect the
- * events and then run all the tests.
- * </p>
- */
-public abstract class VerifiableSensorOperation extends AbstractSensorOperation {
-    protected final TestSensorManager mSensorManager;
-    protected final TestSensorEnvironment mEnvironment;
-
-    private final Collection<ISensorVerification> mVerifications =
-            new HashSet<ISensorVerification>();
-
-    private boolean mLogEvents = false;
-
-    /**
-     * Create a {@link TestSensorOperation}.
-     *
-     * @param environment the test environment
-     */
-    public VerifiableSensorOperation(TestSensorEnvironment environment) {
-        mEnvironment = environment;
-        mSensorManager = new TestSensorManager(mEnvironment);
-    }
-
-    /**
-     * Set whether to log events.
-     */
-    public void setLogEvents(boolean logEvents) {
-        mLogEvents = logEvents;
-    }
-
-    /**
-     * Set all of the default test expectations.
-     */
-    public void addDefaultVerifications() {
-        addVerification(EventGapVerification.getDefault(mEnvironment));
-        addVerification(EventOrderingVerification.getDefault(mEnvironment));
-        addVerification(FrequencyVerification.getDefault(mEnvironment));
-        addVerification(JitterVerification.getDefault(mEnvironment));
-        addVerification(MagnitudeVerification.getDefault(mEnvironment));
-        addVerification(MeanVerification.getDefault(mEnvironment));
-        // Skip SigNumVerification since it has no default
-        addVerification(StandardDeviationVerification.getDefault(mEnvironment));
-    }
-
-    public void addVerification(ISensorVerification verification) {
-        if (verification != null) {
-            mVerifications.add(verification);
-        }
-    }
-
-    /**
-     * Collect the specified number of events from the sensor and run all enabled verifications.
-     */
-    @Override
-    public void execute() throws InterruptedException {
-        getStats().addValue("sensor_name", mEnvironment.getSensor().getName());
-
-        ValidatingSensorEventListener listener = new ValidatingSensorEventListener(mVerifications);
-        listener.setLogEvents(mLogEvents);
-
-        doExecute(listener);
-
-        boolean failed = false;
-        StringBuilder sb = new StringBuilder();
-        for (ISensorVerification verification : mVerifications) {
-            failed |= evaluateResults(verification, sb);
-        }
-
-        if (failed) {
-            String msg = SensorCtsHelper
-                    .formatAssertionMessage("VerifySensorOperation", mEnvironment, sb.toString());
-            getStats().addValue(SensorStats.ERROR, msg);
-            Assert.fail(msg);
-        }
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public VerifiableSensorOperation clone() {
-        VerifiableSensorOperation operation = doClone();
-        for (ISensorVerification verification : mVerifications) {
-            operation.addVerification(verification.clone());
-        }
-        return operation;
-    }
-
-    /**
-     * Execute operations in a {@link TestSensorManager}.
-     */
-    protected abstract void doExecute(TestSensorEventListener listener) throws InterruptedException;
-
-    /**
-     * Clone the subclass operation.
-     */
-    protected abstract VerifiableSensorOperation doClone();
-
-    /**
-     * Evaluate the results of a test, aggregate the stats, and build the error message.
-     */
-    private boolean evaluateResults(ISensorVerification verification, StringBuilder sb) {
-        try {
-            verification.verify(mEnvironment, getStats());
-        } catch (AssertionError e) {
-            if (sb.length() > 0) {
-                sb.append(", ");
-            }
-            sb.append(e.getMessage());
-            return true;
-        }
-        return false;
-    }
-}
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 b500ea7..9f03f31 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
@@ -17,41 +17,42 @@
 package android.hardware.cts.helpers.sensoroperations;
 
 import android.content.Context;
-import android.hardware.cts.helpers.SensorStats;
+import android.hardware.cts.helpers.reporting.ISensorTestNode;
 import android.os.PowerManager;
 import android.os.PowerManager.WakeLock;
 
 /**
- * An {@link ISensorOperation} which holds a wakelock while performing another
- * {@link ISensorOperation}.
+ * An {@link SensorOperation} which holds a wake-lock while performing another
+ * {@link SensorOperation}.
  */
-public class WakeLockOperation extends AbstractSensorOperation {
+public class WakeLockOperation extends SensorOperation {
     private static final String TAG = "WakeLockOperation";
 
-    private final ISensorOperation mOperation;
+    private final SensorOperation mOperation;
     private final Context mContext;
-    private final int mWakelockFlags;
+    private final int mWakeLockFlags;
 
     /**
      * Constructor for {@link WakeLockOperation}.
      *
-     * @param operation the child {@link ISensorOperation} to perform after the delay
+     * @param operation the child {@link SensorOperation} to perform after the delay
      * @param context the context used to access the power manager
-     * @param wakelockFlags the flags used when acquiring the wakelock
+     * @param wakeLockFlags the flags used when acquiring the wake-lock
      */
-    public WakeLockOperation(ISensorOperation operation, Context context, int wakelockFlags) {
+    public WakeLockOperation(SensorOperation operation, Context context, int wakeLockFlags) {
+        super(operation.getStats());
         mOperation = operation;
         mContext = context;
-        mWakelockFlags = wakelockFlags;
+        mWakeLockFlags = wakeLockFlags;
     }
 
     /**
      * Constructor for {@link WakeLockOperation}.
      *
-     * @param operation the child {@link ISensorOperation} to perform after the delay
+     * @param operation the child {@link SensorOperation} to perform after the delay
      * @param context the context used to access the power manager
      */
-    public WakeLockOperation(ISensorOperation operation, Context context) {
+    public WakeLockOperation(SensorOperation operation, Context context) {
         this(operation, context, PowerManager.PARTIAL_WAKE_LOCK);
     }
 
@@ -59,13 +60,12 @@
      * {@inheritDoc}
      */
     @Override
-    public void execute() throws InterruptedException {
+    public void execute(ISensorTestNode parent) throws InterruptedException {
         PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
-        WakeLock wakeLock = pm.newWakeLock(mWakelockFlags, TAG);
-
+        WakeLock wakeLock = pm.newWakeLock(mWakeLockFlags, TAG);
         wakeLock.acquire();
         try {
-            mOperation.execute();
+            mOperation.execute(asTestNode(parent));
         } finally {
             wakeLock.release();
         }
@@ -75,15 +75,7 @@
      * {@inheritDoc}
      */
     @Override
-    public SensorStats getStats() {
-        return mOperation.getStats();
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public ISensorOperation clone() {
-        return new WakeLockOperation(mOperation, mContext, mWakelockFlags);
+    public SensorOperation clone() {
+        return new WakeLockOperation(mOperation.clone(), mContext, mWakeLockFlags);
     }
 }
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/AbstractSensorVerification.java b/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/AbstractSensorVerification.java
index 911ae3a..1e775e3 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/AbstractSensorVerification.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/AbstractSensorVerification.java
@@ -18,6 +18,9 @@
 
 import android.hardware.cts.helpers.TestSensorEvent;
 
+import java.util.Collection;
+import java.util.List;
+
 /**
  * Abstract class that deals with the synchronization of the sensor verifications.
  */
@@ -27,7 +30,7 @@
      * {@inheritDoc}
      */
     @Override
-    public synchronized void addSensorEvents(TestSensorEvent ... events) {
+    public synchronized void addSensorEvents(Collection<TestSensorEvent> events) {
         for (TestSensorEvent event : events) {
             addSensorEventInternal(event);
         }
@@ -37,14 +40,6 @@
      * {@inheritDoc}
      */
     @Override
-    public synchronized void addSensorEvent(TestSensorEvent event) {
-        addSensorEventInternal(event);
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
     public abstract ISensorVerification clone();
 
     /**
@@ -52,18 +47,38 @@
      */
     protected abstract void addSensorEventInternal(TestSensorEvent event);
 
+    protected <TEvent extends IndexedEvent> int[] getIndexArray(List<TEvent> indexedEvents) {
+        int eventsCount = indexedEvents.size();
+        int[] indices = new int[eventsCount];
+        for (int i = 0; i < eventsCount; i++) {
+            indices[i] = indexedEvents.get(i).index;
+        }
+        return indices;
+    }
+
+    /**
+     * Helper class to store the index and current event.
+     * Events are added to the verification in the order they are generated, the index represents
+     * the position of the given event, in the list of added events.
+     */
+    protected class IndexedEvent {
+        public final int index;
+        public final TestSensorEvent event;
+
+        public IndexedEvent(int index, TestSensorEvent event) {
+            this.index = index;
+            this.event = event;
+        }
+    }
+
     /**
      * Helper class to store the index, previous event, and current event.
      */
-    protected class IndexedEventPair {
-        public final int index;
-        public final TestSensorEvent event;
+    protected class IndexedEventPair extends IndexedEvent {
         public final TestSensorEvent previousEvent;
 
-        public IndexedEventPair(int index, TestSensorEvent event,
-                TestSensorEvent previousEvent) {
-            this.index = index;
-            this.event = event;
+        public IndexedEventPair(int index, TestSensorEvent event, TestSensorEvent previousEvent) {
+            super(index, event);
             this.previousEvent = previousEvent;
         }
     }
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/EventGapVerification.java b/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/EventGapVerification.java
index 156afa8..b692f0f 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/EventGapVerification.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/EventGapVerification.java
@@ -63,19 +63,14 @@
     public void verify(TestSensorEnvironment environment, SensorStats stats) {
         if (environment.isSensorSamplingRateOverloaded()) {
             // the verification is not reliable on environments under load
-            stats.addValue(PASSED_KEY, true);
+            stats.addValue(PASSED_KEY, "skipped (under load)");
             return;
         }
 
         final int count = mEventGaps.size();
         stats.addValue(PASSED_KEY, count == 0);
         stats.addValue(SensorStats.EVENT_GAP_COUNT_KEY, count);
-
-        final int[] indices = new int[count];
-        for (int i = 0; i < indices.length; i++) {
-            indices[i] = mEventGaps.get(i).index;
-        }
-        stats.addValue(SensorStats.EVENT_GAP_POSITIONS_KEY, indices);
+        stats.addValue(SensorStats.EVENT_GAP_POSITIONS_KEY, getIndexArray(mEventGaps));
 
         if (count > 0) {
             StringBuilder sb = new StringBuilder();
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/EventGapVerificationTest.java b/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/EventGapVerificationTest.java
index d01e108..6f17e7b 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/EventGapVerificationTest.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/EventGapVerificationTest.java
@@ -22,6 +22,9 @@
 import android.hardware.cts.helpers.TestSensorEnvironment;
 import android.hardware.cts.helpers.TestSensorEvent;
 
+import java.util.ArrayList;
+import java.util.Collection;
+
 /**
  * Tests for {@link EventGapVerification}.
  */
@@ -90,11 +93,13 @@
         }
     }
 
-    private ISensorVerification getVerification(int expected, long ... timestamps) {
-        ISensorVerification verification = new EventGapVerification(expected);
+    private static EventGapVerification getVerification(int expected, long ... timestamps) {
+        Collection<TestSensorEvent> events = new ArrayList<>(timestamps.length);
         for (long timestamp : timestamps) {
-            verification.addSensorEvent(new TestSensorEvent(null, timestamp, 0, null));
+            events.add(new TestSensorEvent(null, timestamp, 0, null));
         }
+        EventGapVerification verification = new EventGapVerification(expected);
+        verification.addSensorEvents(events);
         return verification;
     }
 }
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/EventOrderingVerification.java b/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/EventOrderingVerification.java
index 6598725..7b77ead 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/EventOrderingVerification.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/EventOrderingVerification.java
@@ -75,12 +75,9 @@
         final int count = mOutOfOrderEvents.size();
         stats.addValue(PASSED_KEY, count == 0);
         stats.addValue(SensorStats.EVENT_OUT_OF_ORDER_COUNT_KEY, count);
-
-        final int[] indices = new int[count];
-        for (int i = 0; i < indices.length; i++) {
-            indices[i] = mOutOfOrderEvents.get(i).index;
-        }
-        stats.addValue(SensorStats.EVENT_OUT_OF_ORDER_POSITIONS_KEY, indices);
+        stats.addValue(
+                SensorStats.EVENT_OUT_OF_ORDER_POSITIONS_KEY,
+                getIndexArray(mOutOfOrderEvents));
 
         if (count > 0) {
             StringBuilder sb = new StringBuilder();
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/EventOrderingVerificationTest.java b/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/EventOrderingVerificationTest.java
index 88d5c19..b9848fa 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/EventOrderingVerificationTest.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/EventOrderingVerificationTest.java
@@ -22,6 +22,7 @@
 import android.hardware.cts.helpers.TestSensorEvent;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
 
 /**
@@ -96,11 +97,13 @@
         assertTrue(indices.contains(4));
     }
 
-    private EventOrderingVerification getVerification(long ... timestamps) {
-        EventOrderingVerification verification = new EventOrderingVerification();
+    private static EventOrderingVerification getVerification(long ... timestamps) {
+        Collection<TestSensorEvent> events = new ArrayList<>(timestamps.length);
         for (long timestamp : timestamps) {
-            verification.addSensorEvent(new TestSensorEvent(null, timestamp, 0, null));
+            events.add(new TestSensorEvent(null, timestamp, 0, null));
         }
+        EventOrderingVerification verification = new EventOrderingVerification();
+        verification.addSensorEvents(events);
         return verification;
     }
 
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/EventTimestampSynchronizationVerification.java b/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/EventTimestampSynchronizationVerification.java
new file mode 100644
index 0000000..d4a1f38
--- /dev/null
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/EventTimestampSynchronizationVerification.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2014 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 android.hardware.cts.helpers.sensorverification;
+
+import junit.framework.Assert;
+
+import android.hardware.SensorEvent;
+import android.hardware.cts.helpers.SensorStats;
+import android.hardware.cts.helpers.TestSensorEnvironment;
+import android.hardware.cts.helpers.TestSensorEvent;
+import android.os.SystemClock;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A {@link ISensorVerification} which verifies that the timestamp of the {@link SensorEvent} is
+ * synchronized with {@link SystemClock#elapsedRealtimeNanos()}, based on a given threshold.
+ */
+public class EventTimestampSynchronizationVerification extends AbstractSensorVerification {
+    public static final String PASSED_KEY = "timestamp_synchronization_passed";
+
+    // number of indices to print in assertion message before truncating
+    private static final int TRUNCATE_MESSAGE_LENGTH = 3;
+
+    private static final long DEFAULT_THRESHOLD_NS = TimeUnit.MILLISECONDS.toNanos(500);
+
+    private final ArrayList<TestSensorEvent> mCollectedEvents = new ArrayList<TestSensorEvent>();
+
+    private final long mMaximumSynchronizationErrorNs;
+    private final long mReportLatencyNs;
+
+    /**
+     * Constructs an instance of {@link EventTimestampSynchronizationVerification}.
+     *
+     * @param maximumSynchronizationErrorNs The valid threshold for timestamp synchronization.
+     * @param reportLatencyNs The latency on which batching events are received
+     */
+    public EventTimestampSynchronizationVerification(
+            long maximumSynchronizationErrorNs,
+            long reportLatencyNs) {
+        mMaximumSynchronizationErrorNs = maximumSynchronizationErrorNs;
+        mReportLatencyNs = reportLatencyNs;
+    }
+
+    /**
+     * Gets a default {@link EventTimestampSynchronizationVerification}.
+     *
+     * @param environment The test environment
+     * @return The verification or null if the verification is not supported in the given
+     *         environment.
+     */
+    public static EventTimestampSynchronizationVerification getDefault(
+            TestSensorEnvironment environment) {
+        int reportLatencyUs = environment.getMaxReportLatencyUs();
+        int fifoMaxEventCount = environment.getSensor().getFifoMaxEventCount();
+        if (fifoMaxEventCount > 0) {
+            int fifoBasedReportLatencyUs =
+                    fifoMaxEventCount * environment.getMaximumExpectedSamplingPeriodUs();
+            reportLatencyUs = Math.min(reportLatencyUs, fifoBasedReportLatencyUs);
+
+        }
+        long reportLatencyNs = TimeUnit.MICROSECONDS.toNanos(reportLatencyUs);
+        return new EventTimestampSynchronizationVerification(DEFAULT_THRESHOLD_NS, reportLatencyNs);
+    }
+
+    @Override
+    public void verify(TestSensorEnvironment environment, SensorStats stats) {
+        StringBuilder errorMessageBuilder =
+                new StringBuilder(" event timestamp synchronization failures: ");
+        List<IndexedEvent> failures = verifyTimestampSynchronization(errorMessageBuilder);
+
+        int failuresCount = failures.size();
+        stats.addValue(SensorStats.EVENT_TIME_SYNCHRONIZATION_COUNT_KEY, failuresCount);
+        stats.addValue(
+                SensorStats.EVENT_TIME_SYNCHRONIZATION_POSITIONS_KEY,
+                getIndexArray(failures));
+
+        boolean success = failures.isEmpty();
+        stats.addValue(PASSED_KEY, success);
+        errorMessageBuilder.insert(0, failuresCount);
+        Assert.assertTrue(errorMessageBuilder.toString(), success);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public EventTimestampSynchronizationVerification clone() {
+        return new EventTimestampSynchronizationVerification(
+                mMaximumSynchronizationErrorNs,
+                mReportLatencyNs);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected void addSensorEventInternal(TestSensorEvent event) {
+        mCollectedEvents.add(event);
+    }
+
+    /**
+     * Verifies timestamp synchronization for all sensor events.
+     * The verification accounts for a lower and upper threshold, such thresholds are adjusted for
+     * batching cases.
+     *
+     * @param builder A string builder to store error messaged found in the collected sensor events.
+     * @return A list of events tha failed the verification.
+     */
+    private List<IndexedEvent> verifyTimestampSynchronization(StringBuilder builder) {
+        int collectedEventsCount = mCollectedEvents.size();
+        ArrayList<IndexedEvent> failures = new ArrayList<IndexedEvent>();
+
+        for (int i = 0; i < collectedEventsCount; ++i) {
+            TestSensorEvent event = mCollectedEvents.get(i);
+            long eventTimestampNs = event.timestamp;
+            long receivedTimestampNs = event.receivedTimestamp;
+            long upperThresholdNs = receivedTimestampNs;
+            long lowerThresholdNs = receivedTimestampNs - mMaximumSynchronizationErrorNs
+                    - mReportLatencyNs;
+
+            if (eventTimestampNs < lowerThresholdNs || eventTimestampNs > upperThresholdNs) {
+                if (failures.size() < TRUNCATE_MESSAGE_LENGTH) {
+                    builder.append("position=").append(i);
+                    builder.append(", timestamp=").append(eventTimestampNs).append("ns");
+                    builder.append(", expected=[").append(lowerThresholdNs);
+                    builder.append(", ").append(upperThresholdNs).append("]ns; ");
+                }
+                failures.add(new IndexedEvent(i, event));
+            }
+        }
+        if (failures.size() >= TRUNCATE_MESSAGE_LENGTH) {
+            builder.append("more; ");
+        }
+        return failures;
+    }
+}
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/FrequencyVerificationTest.java b/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/FrequencyVerificationTest.java
index 24349ce..bbf022a 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/FrequencyVerificationTest.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/FrequencyVerificationTest.java
@@ -22,6 +22,9 @@
 import android.hardware.cts.helpers.TestSensorEnvironment;
 import android.hardware.cts.helpers.TestSensorEvent;
 
+import java.util.ArrayList;
+import java.util.Collection;
+
 /**
  * Tests for {@link EventOrderingVerification}.
  */
@@ -73,15 +76,17 @@
         return new TestSensorEnvironment(getContext(), Sensor.TYPE_ALL, rateUs);
     }
 
-    private ISensorVerification getVerification(
+    private static FrequencyVerification getVerification(
             double lowerThreshold,
             double upperThreshold,
             long ... timestamps) {
-        ISensorVerification verification =
-                new FrequencyVerification(lowerThreshold, upperThreshold);
+        Collection<TestSensorEvent> events = new ArrayList<>(timestamps.length);
         for (long timestamp : timestamps) {
-            verification.addSensorEvent(new TestSensorEvent(null, timestamp, 0, null));
+            events.add(new TestSensorEvent(null, timestamp, 0, null));
         }
+        FrequencyVerification verification =
+                new FrequencyVerification(lowerThreshold, upperThreshold);
+        verification.addSensorEvents(events);
         return verification;
     }
 
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/ISensorVerification.java b/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/ISensorVerification.java
index 4f7b65a..2027f0f 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/ISensorVerification.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/ISensorVerification.java
@@ -20,24 +20,25 @@
 import android.hardware.cts.helpers.TestSensorEnvironment;
 import android.hardware.cts.helpers.TestSensorEvent;
 
+import java.util.Collection;
+
 /**
- * Interface describing the sensor verification. This class was designed for to handle streaming
- * events. The methods {@link #addSensorEvent(TestSensorEvent)} and
- * {@link #addSensorEvents(TestSensorEvent...)} should be called in the order that the events are
- * received. The method {@link #verify(TestSensorEnvironment, SensorStats)} should be called after
- * all events are added.
+ * Interface describing the sensor verification.
+ * This class was designed to handle streaming of events.
+ *
+ * The method {@link #addSensorEvents(Collection)} should be called in the order that the events are
+ * received.
+ *
+ * The method {@link #verify(TestSensorEnvironment, SensorStats)} should be called after all events
+ * are added.
  */
 public interface ISensorVerification {
 
     /**
-     * Add a single {@link TestSensorEvent} to be evaluated.
+     * Add a list of {@link TestSensorEvent}s to be evaluated.
      */
-    public void addSensorEvent(TestSensorEvent event);
-
-    /**
-     * Add multiple {@link TestSensorEvent}s to be evaluated.
-     */
-    public void addSensorEvents(TestSensorEvent ... events);
+    // TODO: refactor verifications to be stateless, and pass the list of events in verify()
+    void addSensorEvents(Collection<TestSensorEvent> events);
 
     /**
      * Evaluate all added {@link TestSensorEvent}s and update stats.
@@ -45,10 +46,10 @@
      * @param stats a {@link SensorStats} object used to keep track of the stats.
      * @throws AssertionError if the verification fails.
      */
-    public void verify(TestSensorEnvironment environment, SensorStats stats);
+    void verify(TestSensorEnvironment environment, SensorStats stats);
 
     /**
      * Clones the {@link ISensorVerification}
      */
-    public ISensorVerification clone();
+    ISensorVerification clone();
 }
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/JitterVerificationTest.java b/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/JitterVerificationTest.java
index 0c85b63..50e288c 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/JitterVerificationTest.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/JitterVerificationTest.java
@@ -22,6 +22,8 @@
 import android.hardware.cts.helpers.TestSensorEnvironment;
 import android.hardware.cts.helpers.TestSensorEvent;
 
+import java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
 
 /**
@@ -98,11 +100,13 @@
         assertEquals(3.0, jitterValues.get(3));
     }
 
-    private JitterVerification getVerification(int threshold, long ... timestamps) {
-        JitterVerification verification = new JitterVerification(threshold);
+    private static JitterVerification getVerification(int threshold, long ... timestamps) {
+        Collection<TestSensorEvent> events = new ArrayList<>(timestamps.length);
         for (long timestamp : timestamps) {
-            verification.addSensorEvent(new TestSensorEvent(null, timestamp, 0, null));
+            events.add(new TestSensorEvent(null, timestamp, 0, null));
         }
+        JitterVerification verification = new JitterVerification(threshold);
+        verification.addSensorEvents(events);
         return verification;
     }
 
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/MagnitudeVerificationTest.java b/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/MagnitudeVerificationTest.java
index bb29330..ac873c1 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/MagnitudeVerificationTest.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/MagnitudeVerificationTest.java
@@ -22,6 +22,9 @@
 import android.hardware.cts.helpers.TestSensorEnvironment;
 import android.hardware.cts.helpers.TestSensorEvent;
 
+import java.util.ArrayList;
+import java.util.Collection;
+
 /**
  * Tests for {@link MagnitudeVerification}.
  */
@@ -63,12 +66,14 @@
         assertEquals(magnitude, (Float) stats.getValue(SensorStats.MAGNITUDE_KEY), 0.01);
     }
 
-    private MagnitudeVerification getVerification(float expected, float threshold,
+    private static MagnitudeVerification getVerification(float expected, float threshold,
             float[] ... values) {
-        MagnitudeVerification verification = new MagnitudeVerification(expected, threshold);
+        Collection<TestSensorEvent> events = new ArrayList<>(values.length);
         for (float[] value : values) {
-            verification.addSensorEvent(new TestSensorEvent(null, 0, 0, value));
+            events.add(new TestSensorEvent(null, 0, 0, value));
         }
+        MagnitudeVerification verification = new MagnitudeVerification(expected, threshold);
+        verification.addSensorEvents(events);
         return verification;
     }
 }
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/MeanVerificationTest.java b/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/MeanVerificationTest.java
index b07ea50..d7fcf9f 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/MeanVerificationTest.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/MeanVerificationTest.java
@@ -22,6 +22,9 @@
 import android.hardware.cts.helpers.TestSensorEnvironment;
 import android.hardware.cts.helpers.TestSensorEvent;
 
+import java.util.ArrayList;
+import java.util.Collection;
+
 /**
  * Tests for {@link MeanVerification}.
  */
@@ -89,12 +92,14 @@
         verifyStats(stats, false, new float[]{2.0f, 3.0f, 6.0f});
     }
 
-    private MeanVerification getVerification(float[] expected, float[] threshold,
+    private static MeanVerification getVerification(float[] expected, float[] threshold,
             float[] ... values) {
-        MeanVerification verification = new MeanVerification(expected, threshold);
+        Collection<TestSensorEvent> events = new ArrayList<>(values.length);
         for (float[] value : values) {
-            verification.addSensorEvent(new TestSensorEvent(null, 0, 0, value));
+            events.add(new TestSensorEvent(null, 0, 0, value));
         }
+        MeanVerification verification = new MeanVerification(expected, threshold);
+        verification.addSensorEvents(events);
         return verification;
     }
 
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/StandardDeviationVerificationTest.java b/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/StandardDeviationVerificationTest.java
index 5d958f5..617a438 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/StandardDeviationVerificationTest.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensorverification/StandardDeviationVerificationTest.java
@@ -22,6 +22,9 @@
 import android.hardware.cts.helpers.TestSensorEnvironment;
 import android.hardware.cts.helpers.TestSensorEvent;
 
+import java.util.ArrayList;
+import java.util.Collection;
+
 /**
  * Tests for {@link StandardDeviationVerification}.
  */
@@ -79,11 +82,15 @@
         }
     }
 
-    private StandardDeviationVerification getVerification(float[] threshold, float[] ... values) {
-        StandardDeviationVerification verification = new StandardDeviationVerification(threshold);
+    private static StandardDeviationVerification getVerification(
+            float[] threshold,
+            float[] ... values) {
+        Collection<TestSensorEvent> events = new ArrayList<>(values.length);
         for (float[] value : values) {
-            verification.addSensorEvent(new TestSensorEvent(null, 0, 0, value));
+            events.add(new TestSensorEvent(null, 0, 0, value));
         }
+        StandardDeviationVerification verification = new StandardDeviationVerification(threshold);
+        verification.addSensorEvents(events);
         return verification;
     }
 }