Refactor sensor operations.

Change-Id: I633a5d3f1857e5a0dc766f0bef7385d8a2fc45dd
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 ee26831..44bed6c 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/AccelerometerMeasurementTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/AccelerometerMeasurementTestActivity.java
@@ -18,7 +18,7 @@
 
 import android.hardware.Sensor;
 import android.hardware.SensorManager;
-import android.hardware.cts.helpers.sensorTestOperations.VerifySensorOperation;
+import android.hardware.cts.helpers.sensoroperations.VerifySensorOperation;
 
 import java.util.concurrent.TimeUnit;
 
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 d148fb9..cdb5b3f 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/sensors/GyroscopeMeasurementTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/sensors/GyroscopeMeasurementTestActivity.java
@@ -18,7 +18,7 @@
 
 import android.hardware.Sensor;
 import android.hardware.SensorManager;
-import android.hardware.cts.helpers.sensorTestOperations.VerifySensorOperation;
+import android.hardware.cts.helpers.sensoroperations.VerifySensorOperation;
 
 /**
  * Semi-automated test that focuses on characteristics associated with Accelerometer measurements.
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 7a089bc..115f20a 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,7 @@
 import android.hardware.SensorEvent;
 import android.hardware.SensorManager;
 import android.hardware.cts.helpers.SensorManagerTestVerifier;
-import android.hardware.cts.helpers.sensorTestOperations.VerifySensorOperation;
+import android.hardware.cts.helpers.sensoroperations.VerifySensorOperation;
 
 /**
  * Semi-automated test that focuses characteristics associated with Accelerometer measurements.
diff --git a/tests/tests/hardware/src/android/hardware/cts/SensorIntegrationTests.java b/tests/tests/hardware/src/android/hardware/cts/SensorIntegrationTests.java
index 64acce9..bcf9755 100644
--- a/tests/tests/hardware/src/android/hardware/cts/SensorIntegrationTests.java
+++ b/tests/tests/hardware/src/android/hardware/cts/SensorIntegrationTests.java
@@ -19,12 +19,13 @@
 import android.hardware.Sensor;
 import android.hardware.SensorManager;
 import android.hardware.cts.helpers.SensorCtsHelper;
+import android.hardware.cts.helpers.SensorStats;
 import android.hardware.cts.helpers.SensorTestCase;
 import android.hardware.cts.helpers.SensorTestInformation;
-import android.hardware.cts.helpers.sensorTestOperations.ParallelCompositeSensorTestOperation;
-import android.hardware.cts.helpers.sensorTestOperations.RepeatingSensorTestOperation;
-import android.hardware.cts.helpers.sensorTestOperations.SequentialCompositeSensorTestOperation;
-import android.hardware.cts.helpers.sensorTestOperations.VerifySensorOperation;
+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.VerifySensorOperation;
 
 import junit.framework.Test;
 import junit.framework.TestSuite;
@@ -39,6 +40,8 @@
  *          -w com.android.cts.hardware/android.test.InstrumentationCtsTestRunner
  */
 public class SensorIntegrationTests extends SensorTestCase {
+    private static final String TAG = "SensorIntegrationTests";
+
     /**
      * Builder for the test suite.
      * This is the method that will build dynamically the set of test cases to execute.
@@ -91,7 +94,7 @@
                 Sensor.TYPE_MAGNETIC_FIELD,
                 Sensor.TYPE_GYROSCOPE };
 
-        ParallelCompositeSensorTestOperation operation = new ParallelCompositeSensorTestOperation();
+        ParallelSensorOperation operation = new ParallelSensorOperation();
         for(int sensorType : sensorTypes) {
             VerifySensorOperation continuousOperation = new VerifySensorOperation(
                     context,
@@ -100,7 +103,7 @@
                     0 /* reportLatencyInUs */,
                     100 /* event count */);
             continuousOperation.verifyEventOrdering();
-            operation.add(new RepeatingSensorTestOperation(continuousOperation, ITERATIONS));
+            operation.add(new RepeatingSensorOperation(continuousOperation, ITERATIONS));
 
             VerifySensorOperation batchingOperation = new VerifySensorOperation(
                     context,
@@ -109,9 +112,10 @@
                     SensorCtsHelper.getSecondsAsMicroSeconds(BATCHING_RATE_IN_SECONDS),
                     100);
             batchingOperation.verifyEventOrdering();
-            operation.add(new RepeatingSensorTestOperation(batchingOperation, ITERATIONS));
+            operation.add(new RepeatingSensorOperation(batchingOperation, ITERATIONS));
         }
         operation.execute();
+        SensorStats.logStats(TAG, operation.getStats());
     }
 
     /**
@@ -140,7 +144,7 @@
         final int INSTANCES_TO_USE = 5;
         final int ITERATIONS_TO_EXECUTE = 100;
 
-        ParallelCompositeSensorTestOperation operation = new ParallelCompositeSensorTestOperation();
+        ParallelSensorOperation operation = new ParallelSensorOperation();
         int sensorTypes[] = {
                 Sensor.TYPE_ACCELEROMETER,
                 Sensor.TYPE_MAGNETIC_FIELD,
@@ -148,8 +152,7 @@
 
         for(int sensorType : sensorTypes) {
             for(int instance = 0; instance < INSTANCES_TO_USE; ++instance) {
-                SequentialCompositeSensorTestOperation sequentialOperation =
-                        new SequentialCompositeSensorTestOperation();
+                SequentialSensorOperation sequentialOperation = new SequentialSensorOperation();
                 for(int iteration = 0; iteration < ITERATIONS_TO_EXECUTE; ++iteration) {
                     VerifySensorOperation sensorOperation = new VerifySensorOperation(
                             this.getContext(),
@@ -165,6 +168,7 @@
         }
 
         operation.execute();
+        SensorStats.logStats(TAG, operation.getStats());
     }
 
     /**
@@ -221,7 +225,6 @@
                 0 /*reportLatencyInUs*/,
                 100 /* event count */);
         tester.verifyEventOrdering();
-        tester.start();
 
         VerifySensorOperation testee = new VerifySensorOperation(
                 context,
@@ -230,12 +233,15 @@
                 0 /*reportLatencyInUs*/,
                 100 /* event count */);
         testee.verifyEventOrdering();
-        testee.start();
 
-        testee.waitForCompletion();
-        tester.waitForCompletion();
+        ParallelSensorOperation operation = new ParallelSensorOperation();
+        operation.add(tester, testee);
+        operation.execute();
+        SensorStats.logStats(TAG, operation.getStats());
 
+        testee = testee.clone();
         testee.execute();
+        SensorStats.logStats(TAG, testee.getStats());
     }
 
     /**
diff --git a/tests/tests/hardware/src/android/hardware/cts/SingleSensorTests.java b/tests/tests/hardware/src/android/hardware/cts/SingleSensorTests.java
index 6898dd1..27e7b02 100644
--- a/tests/tests/hardware/src/android/hardware/cts/SingleSensorTests.java
+++ b/tests/tests/hardware/src/android/hardware/cts/SingleSensorTests.java
@@ -19,9 +19,10 @@
 import android.hardware.Sensor;
 import android.hardware.SensorManager;
 import android.hardware.cts.helpers.SensorCtsHelper;
+import android.hardware.cts.helpers.SensorStats;
 import android.hardware.cts.helpers.SensorTestCase;
 import android.hardware.cts.helpers.SensorTestInformation;
-import android.hardware.cts.helpers.sensorTestOperations.VerifySensorOperation;
+import android.hardware.cts.helpers.sensoroperations.VerifySensorOperation;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -83,6 +84,7 @@
  * </p>
  */
 public class SingleSensorTests extends SensorTestCase {
+    private static final String TAG = "SingleSensorTests";
 
     /**
      * This test verifies that the sensor's properties complies with the required properites set in
@@ -206,5 +208,6 @@
                 SensorManager.SENSOR_DELAY_FASTEST, 0, 100);
         op.setDefaultVerifications();
         op.execute();
+        SensorStats.logStats(TAG, op.getStats());
     }
 }
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 cd5a9c6..fd221a9 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/SensorCtsHelper.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/SensorCtsHelper.java
@@ -18,6 +18,7 @@
 import android.content.Context;
 import android.hardware.Sensor;
 import android.hardware.SensorManager;
+import android.hardware.cts.helpers.sensoroperations.ISensorOperation;
 import android.os.Environment;
 import android.util.Log;
 
@@ -359,7 +360,7 @@
      */
     public static String formatAssertionMessage(
             String verificationName,
-            SensorTestOperation test,
+            ISensorOperation test,
             Sensor sensor,
             String format,
             Object ... params) {
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/SensorManagerTestVerifier.java b/tests/tests/hardware/src/android/hardware/cts/helpers/SensorManagerTestVerifier.java
index f5ec118..e9e89d0 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/SensorManagerTestVerifier.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/SensorManagerTestVerifier.java
@@ -158,6 +158,11 @@
         return this.collectEvents(eventCount, "");
     }
 
+    public TestSensorEvent[] collectEvents(long duration, TimeUnit timeUnit) {
+        // TODO: Allow this class to support duration as well as event count
+        throw new UnsupportedOperationException();
+    }
+
     public void startFlush() {
         String message = SensorCtsHelper.formatAssertionMessage(
                 "Flush",
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/SensorStats.java b/tests/tests/hardware/src/android/hardware/cts/helpers/SensorStats.java
new file mode 100644
index 0000000..48f8136
--- /dev/null
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/SensorStats.java
@@ -0,0 +1,101 @@
+/*
+ * 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.cts.helpers.sensoroperations.ISensorOperation;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * Class used to store stats related to {@link ISensorOperation}s.  Sensor stats may be linked
+ * together so that they form a tree.
+ */
+public class SensorStats {
+    public static final String DELIMITER = "__";
+
+    private final Map<String, Object> mValues = new HashMap<String, Object>();
+    private final Map<String, SensorStats> mSensorStats = new HashMap<String, SensorStats>();
+
+    /**
+     * Add a value.
+     *
+     * @param key the key.
+     * @param value the value as an {@link Object}.
+     */
+    public synchronized void addValue(String key, Object value) {
+        if (value == null) {
+            return;
+        }
+        mValues.put(key, value);
+    }
+
+    /**
+     * Add a nested {@link SensorStats}. This is useful for keeping track of stats in a
+     * {@link ISensorOperation} tree.
+     *
+     * @param key the key
+     * @param stats the sub {@link SensorStats} object.
+     */
+    public synchronized void addSensorStats(String key, SensorStats stats) {
+        if (stats == null) {
+            return;
+        }
+        mSensorStats.put(key, stats);
+    }
+
+    /**
+     * 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")}.
+     *
+     * @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);
+        for (Entry<String, SensorStats> statsEntry : mSensorStats.entrySet()) {
+            for (Entry<String, Object> valueEntry : statsEntry.getValue().flatten().entrySet()) {
+                String key = statsEntry.getKey() + DELIMITER + valueEntry.getKey();
+                flattenedMap.put(key, valueEntry.getValue());
+            }
+        }
+        return flattenedMap;
+    }
+
+    /**
+     * Utility method to log the stats to the logcat.
+     */
+    public static void logStats(String tag, SensorStats stats) {
+        final Map<String, Object> flattened = stats.flatten();
+        final List<String> keys = new ArrayList<String>(flattened.keySet());
+        Collections.sort(keys);
+        for (String key : keys) {
+            Object value = flattened.get(key);
+            if (value instanceof Double || value instanceof Float) {
+                Log.v(tag, String.format("%s: %.4f", key, value));
+            } else {
+                Log.v(tag, String.format("%s: %s", key, value.toString()));
+            }
+        }
+    }
+}
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/SensorStatsTest.java b/tests/tests/hardware/src/android/hardware/cts/helpers/SensorStatsTest.java
new file mode 100644
index 0000000..8ba0f41
--- /dev/null
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/SensorStatsTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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 junit.framework.TestCase;
+
+import java.util.Map;
+
+/**
+ * Unit tests for the {@link SensorStats} class.
+ */
+public class SensorStatsTest extends TestCase {
+
+    /**
+     * Test that {@link SensorStats#flatten()} works correctly.
+     */
+    public void testFlatten() {
+        SensorStats stats = new SensorStats();
+        stats.addValue("value0", 0);
+        stats.addValue("value1", 1);
+
+        SensorStats subStats = new SensorStats();
+        subStats.addValue("value2", 2);
+        subStats.addValue("value3", 3);
+
+        SensorStats subSubStats = new SensorStats();
+        subSubStats.addValue("value4", 4);
+        subSubStats.addValue("value5", 5);
+
+        subStats.addSensorStats("stats1", subSubStats);
+        stats.addSensorStats("stats0", subStats);
+
+        // Add empty stats, expect no value in flattened map
+        stats.addSensorStats("stats2", new SensorStats());
+
+        // Add null values, expect no value in flattened map
+        stats.addSensorStats("stats3", null);
+        stats.addValue("value6", null);
+
+        Map<String, Object> flattened = stats.flatten();
+
+        assertEquals(6, flattened.size());
+        assertEquals(0, (int) (Integer) flattened.get("value0"));
+        assertEquals(1, (int) (Integer) flattened.get("value1"));
+        assertEquals(2, (int) (Integer) flattened.get("stats0__value2"));
+        assertEquals(3, (int) (Integer) flattened.get("stats0__value3"));
+        assertEquals(4, (int) (Integer) flattened.get("stats0__stats1__value4"));
+        assertEquals(5, (int) (Integer) flattened.get("stats0__stats1__value5"));
+    }
+}
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/SensorTestOperation.java b/tests/tests/hardware/src/android/hardware/cts/helpers/SensorTestOperation.java
deleted file mode 100644
index 902c8024..0000000
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/SensorTestOperation.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * Copyright (C) 2013 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 junit.framework.Assert;
-
-import java.util.concurrent.TimeUnit;
-
-/**
- * Base test class that supports a basic test operation performed in a sensor.
- * The class follows a command patter as a base for its work.
- *
- * Remarks:
- * - The class wraps verifications and test checks that are needed to verify the operation.
- * - The operation runs in a background thread where it performs the bulk of its work.
- */
-public abstract class SensorTestOperation {
-    private final SensorTestExceptionHandler mExceptionHandler = new SensorTestExceptionHandler();
-
-    protected final String LOG_TAG = "TestRunner";
-    protected final long WAIT_TIMEOUT_IN_MILLISECONDS =
-            TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES);
-
-    private Thread mThread;
-
-    protected int mIterationCount;
-
-    /**
-     * Public API definition.
-     */
-    public synchronized void start() throws Throwable {
-        if(mThread != null) {
-            throw new IllegalStateException("The operation has already been started.");
-        }
-
-        mThread = new Thread() {
-            @Override
-            public void run() {
-                try {
-                    doWork();
-                } catch (Throwable e) {
-                    // log the exception so it can be sent back to the appropriate test thread
-                    this.getUncaughtExceptionHandler().uncaughtException(this, e);
-                }
-            }
-        };
-
-        ++mIterationCount;
-        mThread.setUncaughtExceptionHandler(mExceptionHandler);
-        mThread.start();
-    }
-
-    public synchronized void waitForCompletion() throws Throwable {
-        if(mThread == null) {
-            // let a wait on a stopped operation to be no-op
-            return;
-        }
-        mThread.join(WAIT_TIMEOUT_IN_MILLISECONDS);
-        if(mThread.isAlive()) {
-            // the test is hung so collect the state of the system and fail
-            String operationName = this.getClass().getSimpleName();
-            String message = String.format(
-                    "%s hung. %s. BugReport collected at: %s",
-                    operationName,
-                    this.toString(),
-                    SensorCtsHelper.collectBugreport(operationName));
-            Assert.fail(message);
-        }
-        mThread = null;
-        mExceptionHandler.rethrow();
-    }
-
-    public void execute() throws Throwable {
-        this.start();
-        this.waitForCompletion();
-    }
-
-    @Override
-    public String toString() {
-        return String.format("ThreadId:%d, Iteration:%d", mThread.getId(), mIterationCount);
-    }
-
-    /**
-     * Subclasses implement this method to perform the work associated with the operation they
-     * represent.
-     */
-    protected abstract void doWork() throws Throwable;
-
-    /**
-     * Private helpers.
-     */
-    private class SensorTestExceptionHandler implements Thread.UncaughtExceptionHandler {
-        private final Object mLock = new Object();
-
-        private Throwable mThrowable;
-
-        @Override
-        public void uncaughtException(Thread thread, Throwable throwable) {
-            synchronized(mLock) {
-                // the fist exception is in general the one that is more interesting
-                if(mThrowable != null) {
-                    return;
-                }
-                mThrowable = throwable;
-            }
-        }
-
-        public void rethrow() throws Throwable {
-            Throwable throwable;
-            synchronized(mLock) {
-                throwable = mThrowable;
-                mThrowable = null;
-            }
-            if(throwable != null) {
-                throw throwable;
-            }
-        }
-    }
-}
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/sensorTestOperations/ParallelCompositeSensorTestOperation.java b/tests/tests/hardware/src/android/hardware/cts/helpers/sensorTestOperations/ParallelCompositeSensorTestOperation.java
deleted file mode 100644
index 3730f4b..0000000
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/sensorTestOperations/ParallelCompositeSensorTestOperation.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright (C) 2013 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.sensorTestOperations;
-
-import android.hardware.cts.helpers.SensorTestOperation;
-
-import java.util.ArrayList;
-
-/**
- * A test operation that groups a set of SensorTestOperations and allows to execute them all in
- * parallel.
- * This class can be combined to compose other primitive SensorTestOperations.
- */
-public class ParallelCompositeSensorTestOperation extends SensorTestOperation {
-    private final ArrayList<SensorTestOperation> mOperations = new ArrayList<SensorTestOperation>();
-
-    /**
-     * There is no synchronization
-     * @param operations
-     */
-    public void add(SensorTestOperation ... operations) {
-        synchronized (mOperations) {
-            for(SensorTestOperation operation : operations) {
-                mOperations.add(operation);
-            }
-        }
-    }
-
-    @Override
-    protected void doWork() throws Throwable {
-        synchronized (mOperations) {
-            for(SensorTestOperation operation : mOperations) {
-                operation.start();
-            }
-            for(SensorTestOperation operation : mOperations) {
-                operation.waitForCompletion();
-            }
-        }
-    }
-}
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/sensorTestOperations/RepeatingSensorTestOperation.java b/tests/tests/hardware/src/android/hardware/cts/helpers/sensorTestOperations/RepeatingSensorTestOperation.java
deleted file mode 100644
index 7a3c450..0000000
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/sensorTestOperations/RepeatingSensorTestOperation.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright (C) 2013 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.sensorTestOperations;
-
-import android.hardware.cts.helpers.SensorTestOperation;
-
-/**
- * High level SensorTestOperation that executes the inner operation in a loop.
- */
-public class RepeatingSensorTestOperation extends SensorTestOperation {
-    private final SensorTestOperation mSensorTestOperation;
-    private final int mRepetitionCount;
-
-    public RepeatingSensorTestOperation(SensorTestOperation operation, int repetitionCount) {
-        mSensorTestOperation = operation;
-        mRepetitionCount = repetitionCount;
-    }
-
-    @Override
-    protected void doWork() throws Throwable {
-        for(int i = 0; i < mRepetitionCount; ++i) {
-            mSensorTestOperation.execute();
-        }
-    }
-}
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/sensorTestOperations/SequentialCompositeSensorTestOperation.java b/tests/tests/hardware/src/android/hardware/cts/helpers/sensorTestOperations/SequentialCompositeSensorTestOperation.java
deleted file mode 100644
index 4b921680..0000000
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/sensorTestOperations/SequentialCompositeSensorTestOperation.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (C) 2013 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.sensorTestOperations;
-
-import android.hardware.cts.helpers.SensorTestOperation;
-
-import java.util.ArrayList;
-
-/**
- * A test operation that groups a set of SensorTestOperations and allows to execute them in a
- * sequence, each operation executes in the order they are added to the composite container.
- * This class can be combined to compose other primitive SensorTestOperations.
- */
-public class SequentialCompositeSensorTestOperation extends SensorTestOperation {
-    private final ArrayList<SensorTestOperation> mOperations = new ArrayList<SensorTestOperation>();
-
-    /**
-     * There is no synchronization
-     * @param operations
-     */
-    public void add(SensorTestOperation ... operations) {
-        synchronized (mOperations) {
-            for(SensorTestOperation operation : operations) {
-                mOperations.add(operation);
-            }
-        }
-    }
-
-    @Override
-    protected void doWork() throws Throwable {
-        synchronized (mOperations) {
-            for(SensorTestOperation operation : mOperations) {
-                operation.execute();
-            }
-        }
-    }
-}
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
new file mode 100644
index 0000000..5f6e558
--- /dev/null
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/AbstractSensorOperation.java
@@ -0,0 +1,51 @@
+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#addValue(String, Object)}
+     */
+    protected void addValue(String key, Object value) {
+        mStats.addValue(key, value);
+    }
+
+    /**
+     * 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/DelaySensorOperation.java b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/DelaySensorOperation.java
new file mode 100644
index 0000000..ad0ce94
--- /dev/null
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/DelaySensorOperation.java
@@ -0,0 +1,85 @@
+/*
+ * 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 java.util.concurrent.TimeUnit;
+
+/**
+ * An {@link ISensorOperation} which delays for a specified period of time before performing another
+ * {@link ISensorOperation}.
+ */
+public class DelaySensorOperation implements ISensorOperation {
+    private static final int NANOS_PER_MILLI = 1000000;
+
+    private final ISensorOperation 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 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");
+        }
+        mOperation = operation;
+        mDelay = delay;
+        mTimeUnit = timeUnit;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void execute() {
+        sleep(TimeUnit.NANOSECONDS.convert(mDelay, mTimeUnit));
+        mOperation.execute();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public SensorStats getStats() {
+        return mOperation.getStats();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public DelaySensorOperation clone() {
+        return new DelaySensorOperation(mOperation.clone(), mDelay, mTimeUnit);
+    }
+
+    /**
+     * Helper method to sleep for a given number of ns. Exposed for unit testing.
+     */
+    void sleep(long delayNs) {
+        try {
+            Thread.sleep(delayNs / NANOS_PER_MILLI, (int) (delayNs % NANOS_PER_MILLI));
+        } catch (InterruptedException e) {
+            // Ignore
+        }
+    }
+}
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
new file mode 100644
index 0000000..0c7e771
--- /dev/null
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/FakeSensorOperation.java
@@ -0,0 +1,77 @@
+/*
+ * 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 java.util.concurrent.TimeUnit;
+
+/**
+ * A fake {@ISensorOperation} that will run for a specified time and then pass or fail. Useful when
+ * debugging the framework.
+ */
+public class FakeSensorOperation extends AbstractSensorOperation {
+    private static final int NANOS_PER_MILLI = 1000000;
+
+    private final boolean mFail;
+    private final long mDelay;
+    private final TimeUnit mTimeUnit;
+
+    /**
+     * Constructor for {@link FakeSensorOperation} that passes
+     */
+    public FakeSensorOperation(long delay, TimeUnit timeUnit) {
+        this(false, delay, timeUnit);
+    }
+
+    /**
+     * Constructor for {@link FakeSensorOperation}
+     */
+    public FakeSensorOperation(boolean fail, long delay, TimeUnit timeUnit) {
+        if (timeUnit == null) {
+            throw new IllegalArgumentException("Arguments cannot be null");
+        }
+        mFail = fail;
+        mDelay = delay;
+        mTimeUnit = timeUnit;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void execute() {
+        long delayNs = TimeUnit.NANOSECONDS.convert(mDelay, mTimeUnit);
+        try {
+            Thread.sleep(delayNs / NANOS_PER_MILLI, (int) (delayNs % NANOS_PER_MILLI));
+            addValue("executed", new Boolean(true));
+            if (mFail) {
+                Assert.fail("FakeSensorOperation failed");
+            }
+        }catch (InterruptedException e) {
+            // Ignore
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public FakeSensorOperation clone() {
+        return new FakeSensorOperation(mFail, mDelay, mTimeUnit);
+    }
+}
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
new file mode 100644
index 0000000..4ae56ea
--- /dev/null
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/ISensorOperation.java
@@ -0,0 +1,59 @@
+/*
+ * 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.
+     */
+    public void execute();
+
+    /**
+     * 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
new file mode 100644
index 0000000..9dda510
--- /dev/null
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/ParallelSensorOperation.java
@@ -0,0 +1,255 @@
+/*
+ * Copyright (C) 2013 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.util.Log;
+
+import junit.framework.Assert;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A {@link ISensorOperation} that executes a set of children {@link ISensorOperation}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.
+ */
+public class ParallelSensorOperation extends AbstractSensorOperation {
+    public static final String STATS_TAG = "parallel";
+
+    private static final String TAG = "ParallelSensorOperation";
+    private static final int NANOS_PER_MILLI = 1000000;
+
+    private final List<ISensorOperation> mOperations = new LinkedList<ISensorOperation>();
+    private final Long mTimeout;
+    private final TimeUnit mTimeUnit;
+
+    /**
+     * Constructor for the {@link ParallelSensorOperation} without a timeout.
+     */
+    public ParallelSensorOperation() {
+        mTimeout = null;
+        mTimeUnit = null;
+    }
+
+    /**
+     * Constructor for the {@link ParallelSensorOperation} with a timeout.
+     */
+    public ParallelSensorOperation(long timeout, TimeUnit timeUnit) {
+        if (timeUnit == null) {
+            throw new IllegalArgumentException("Arguments cannot be null");
+        }
+        mTimeout = timeout;
+        mTimeUnit = timeUnit;
+    }
+
+    /**
+     * Add a set of {@link ISensorOperation}s.
+     */
+    public void add(ISensorOperation ... operations) {
+        for (ISensorOperation operation : operations) {
+            if (operation == null) {
+                throw new IllegalArgumentException("Arguments cannot be null");
+            }
+            mOperations.add(operation);
+        }
+    }
+
+    /**
+     * Executes the {@link ISensorOperation}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() {
+        Long timeoutTimeNs = null;
+        if (mTimeout != null && mTimeUnit != null) {
+            timeoutTimeNs = System.nanoTime() + TimeUnit.NANOSECONDS.convert(mTimeout, mTimeUnit);
+        }
+
+        List<OperationThread> threadPool = new ArrayList<OperationThread>(mOperations.size());
+        for (final ISensorOperation operation : mOperations) {
+            OperationThread thread = new OperationThread(operation);
+            thread.start();
+            threadPool.add(thread);
+        }
+
+        List<Integer> timeoutIndices = new ArrayList<Integer>();
+        List<OperationExceptionInfo> exceptions = new ArrayList<OperationExceptionInfo>();
+        Throwable earliestException = null;
+        Long earliestExceptionTime = null;
+
+        for (int i = 0; i < threadPool.size(); i++) {
+            OperationThread thread = threadPool.get(i);
+            join(thread, timeoutTimeNs);
+            if (thread.isAlive()) {
+                timeoutIndices.add(i);
+                thread.interrupt();
+            }
+
+            Throwable exception = thread.getException();
+            Long exceptionTime = thread.getExceptionTime();
+            if (exception != null && exceptionTime != null) {
+                if (exception instanceof AssertionError) {
+                    exceptions.add(new OperationExceptionInfo(i, (AssertionError) exception));
+                }
+                if (earliestExceptionTime == null || exceptionTime < earliestExceptionTime) {
+                    earliestException = exception;
+                    earliestExceptionTime = exceptionTime;
+                }
+            }
+
+            addSensorStats(STATS_TAG, i, thread.getSensorOperation().getStats());
+        }
+
+        if (earliestException == null) {
+            if (timeoutIndices.size() > 0) {
+                Assert.fail(getTimeoutMessage(timeoutIndices));
+            }
+        } else if (earliestException instanceof AssertionError) {
+            String msg = getExceptionMessage(exceptions, timeoutIndices);
+            throw new AssertionError(msg, earliestException);
+        } else if (earliestException instanceof RuntimeException) {
+            throw (RuntimeException) earliestException;
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public ParallelSensorOperation clone() {
+        ParallelSensorOperation operation = new ParallelSensorOperation();
+        for (ISensorOperation subOperation : mOperations) {
+            operation.add(subOperation.clone());
+        }
+        return operation;
+    }
+
+    /**
+     * Helper method that joins a thread at a given time in the future.
+     */
+    private void join(Thread thread, Long timeoutTimeNs) {
+        try {
+            if (timeoutTimeNs == null) {
+                thread.join();
+            } else {
+                // Cap wait time to 1ns so that join doesn't block indefinitely.
+                long waitTimeNs = Math.max(timeoutTimeNs - System.nanoTime(), 1);
+                thread.join(waitTimeNs / NANOS_PER_MILLI, (int) waitTimeNs % NANOS_PER_MILLI);
+            }
+        } catch (InterruptedException e) {
+            // Log and ignore
+            Log.w(TAG, "Thread interrupted during join, operations may timeout before expected"
+                    + " time");
+        }
+    }
+
+    /**
+     * Helper method for joining the exception messages used in assertions.
+     */
+    private String getExceptionMessage(List<OperationExceptionInfo> exceptions,
+            List<Integer> timeoutIndices) {
+        StringBuilder sb = new StringBuilder();
+        sb.append(exceptions.get(0).toString());
+        for (int i = 1; i < exceptions.size(); i++) {
+            sb.append(", ").append(exceptions.get(i).toString());
+        }
+        if (timeoutIndices.size() > 0) {
+            sb.append(", ").append(getTimeoutMessage(timeoutIndices));
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Helper method for formatting the operation timed out message used in assertions
+     */
+    private String getTimeoutMessage(List<Integer> indices) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("Operation");
+        if (indices.size() != 1) {
+            sb.append("s");
+        }
+        sb.append(" ").append(indices.get(0));
+        for (int i = 1; i < indices.size(); i++) {
+            sb.append(", ").append(indices.get(i));
+        }
+        sb.append(" timed out");
+        return sb.toString();
+    }
+
+    /**
+     * Helper class for holding operation index and exception
+     */
+    private class OperationExceptionInfo {
+        private final int mIndex;
+        private final AssertionError mException;
+
+        public OperationExceptionInfo(int index, AssertionError exception) {
+            mIndex = index;
+            mException = exception;
+        }
+
+        @Override
+        public String toString() {
+            return String.format("Operation %d failed: \"%s\"", mIndex, mException.getMessage());
+        }
+    }
+
+    /**
+     * Helper class to run the {@link ISensorOperation} in its own thread.
+     */
+    private class OperationThread extends Thread {
+        final private ISensorOperation mOperation;
+        private Throwable mException = null;
+        private Long mExceptionTime = null;
+
+        public OperationThread(ISensorOperation operation) {
+            mOperation = operation;
+        }
+
+        /**
+         * Run the thread catching {@link RuntimeException}s and {@link AssertionError}s and
+         * the time it happened.
+         */
+        @Override
+        public void run() {
+            try {
+                mOperation.execute();
+            } catch (AssertionError e) {
+                mExceptionTime = System.nanoTime();
+                mException = e;
+            } catch (RuntimeException e) {
+                mExceptionTime = System.nanoTime();
+                mException = e;
+            }
+        }
+
+        public ISensorOperation getSensorOperation() {
+            return mOperation;
+        }
+
+        public Throwable getException() {
+            return mException;
+        }
+
+        public Long getExceptionTime() {
+            return mExceptionTime;
+        }
+    }
+}
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
new file mode 100644
index 0000000..28884cd
--- /dev/null
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/RepeatingSensorOperation.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2013 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;
+
+/**
+ * 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.
+ */
+public class RepeatingSensorOperation extends AbstractSensorOperation {
+    public static final String STATS_TAG = "repeating";
+
+    private final ISensorOperation mOperation;
+    private final int mIterations;
+
+    /**
+     * Constructor for {@link RepeatingSensorOperation}.
+     *
+     * @param operation the {@link ISensorOperation} to run.
+     * @param iterations the number of iterations to run the operation for.
+     */
+    public RepeatingSensorOperation(ISensorOperation 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.
+     */
+    @Override
+    public void execute() {
+        for(int i = 0; i < mIterations; ++i) {
+            ISensorOperation operation = mOperation.clone();
+            try {
+                operation.execute();
+            } catch (AssertionError e) {
+                String msg = String.format("Iteration %d failed: \"%s\"", i, e.getMessage());
+                throw new AssertionError(msg, e);
+            } finally {
+                addSensorStats(STATS_TAG, i, operation.getStats());
+            }
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public RepeatingSensorOperation clone() {
+        return new RepeatingSensorOperation(mOperation.clone(), mIterations);
+    }
+}
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
new file mode 100644
index 0000000..244a974
--- /dev/null
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/SensorOperationTest.java
@@ -0,0 +1,313 @@
+/*
+ * 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 junit.framework.TestCase;
+
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Tests for the primitive {@link ISensorOperation}s including {@link DelaySensorOperation},
+ * {@link ParallelSensorOperation}, {@link RepeatingSensorOperation} and
+ * {@link SequentialSensorOperation}.
+ */
+public class SensorOperationTest extends TestCase {
+    private static final int THRESHOLD_MS = 50;
+
+    /**
+     * Test that the {@link FakeSensorOperation} functions correctly. Other tests in this class
+     * rely on this operation.
+     */
+    public void testFakeSensorOperation() {
+        final int opDurationMs = 100;
+
+        ISensorOperation op = new FakeSensorOperation(opDurationMs, TimeUnit.MILLISECONDS);
+
+        assertFalse(op.getStats().flatten().containsKey("executed"));
+        long start = System.currentTimeMillis();
+        op.execute();
+        long duration = System.currentTimeMillis() - start;
+        assertTrue(Math.abs(opDurationMs - duration) < THRESHOLD_MS);
+        assertTrue(op.getStats().flatten().containsKey("executed"));
+
+        op = new FakeSensorOperation(true, 0, TimeUnit.MILLISECONDS);
+        try {
+            op.execute();
+            fail("AssertionError expected");
+        } catch (AssertionError e) {
+            // Expected
+        }
+    }
+
+    /**
+     * Test that the {@link DelaySensorOperation} functions correctly.
+     */
+    public void testDelaySensorOperation() {
+        final int opDurationMs = 500;
+        final int subOpDurationMs = 100;
+
+        FakeSensorOperation subOp = new FakeSensorOperation(subOpDurationMs, TimeUnit.MILLISECONDS);
+        ISensorOperation op = new DelaySensorOperation(subOp, opDurationMs, TimeUnit.MILLISECONDS);
+
+        long start = System.currentTimeMillis();
+        op.execute();
+        long duration = System.currentTimeMillis() - start;
+        assertTrue(Math.abs(opDurationMs + subOpDurationMs - duration) < THRESHOLD_MS);
+    }
+
+    /**
+     * Test that the {@link ParallelSensorOperation} functions correctly.
+     */
+    public void testParallelSensorOperation() {
+        final int subOpCount = 100;
+        final int subOpDurationMs = 500;
+
+        ParallelSensorOperation op = new ParallelSensorOperation();
+        for (int i = 0; i < subOpCount; i++) {
+            ISensorOperation subOp = new FakeSensorOperation(subOpDurationMs,
+                    TimeUnit.MILLISECONDS);
+            op.add(subOp);
+        }
+
+        Set<String> statsKeys = op.getStats().flatten().keySet();
+        assertEquals(0, statsKeys.size());
+
+        long start = System.currentTimeMillis();
+        op.execute();
+        long duration = System.currentTimeMillis() - start;
+        assertTrue(Math.abs(subOpDurationMs - duration) < THRESHOLD_MS);
+
+        statsKeys = op.getStats().flatten().keySet();
+        assertEquals(subOpCount, statsKeys.size());
+        for (int i = 0; i < subOpCount; i++) {
+            assertTrue(statsKeys.contains(String.format("%s_%03d%sexecuted",
+                    ParallelSensorOperation.STATS_TAG, i, SensorStats.DELIMITER)));
+        }
+    }
+
+    /**
+     * Test that the {@link ParallelSensorOperation} functions correctly if there is a failure in
+     * a child operation.
+     */
+    public void testParallelSensorOperation_fail() {
+        final int subOpCount = 100;
+
+        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);
+            op.add(subOp);
+        }
+
+        Set<String> statsKeys = op.getStats().flatten().keySet();
+        assertEquals(0, statsKeys.size());
+
+        try {
+            op.execute();
+            fail("AssertionError expected");
+        } catch (AssertionError e) {
+            // Expected
+            System.out.println(e.getMessage());
+            // TODO: Verify that the exception rethrown was at t=5ms.
+        }
+
+        statsKeys = op.getStats().flatten().keySet();
+        assertEquals(subOpCount, statsKeys.size());
+        for (int i = 0; i < subOpCount; i++) {
+            assertTrue(statsKeys.contains(String.format("%s_%03d%sexecuted",
+                    ParallelSensorOperation.STATS_TAG, i, SensorStats.DELIMITER)));
+        }
+    }
+
+    /**
+     * Test that the {@link ParallelSensorOperation} functions correctly if a child exceeds the
+     * timeout.
+     */
+    public void testParallelSensorOperation_timeout() {
+        final int subOpCount = 100;
+
+        ParallelSensorOperation op = new ParallelSensorOperation(100, TimeUnit.MILLISECONDS);
+        for (int i = 0; i < subOpCount; i++) {
+            // Trigger timeouts in the 5th, 55th operations (5 seconds vs 0 seconds)
+            ISensorOperation subOp = new FakeSensorOperation(i % 50 == 5 ? 5 : 0, TimeUnit.SECONDS);
+            op.add(subOp);
+        }
+
+        Set<String> statsKeys = op.getStats().flatten().keySet();
+        assertEquals(0, statsKeys.size());
+
+        try {
+            op.execute();
+            fail("AssertionError expected");
+        } catch (AssertionError e) {
+            // Expected
+            System.out.println(e.getMessage());
+            // TODO: Verify that the exception rethrown was at t=5ms.
+        }
+
+        statsKeys = op.getStats().flatten().keySet();
+        assertEquals(subOpCount - 2, statsKeys.size());
+        for (int i = 0; i < subOpCount; i++) {
+            if (i % 50 != 5) {
+                assertTrue(statsKeys.contains(String.format("%s_%03d%sexecuted",
+                        ParallelSensorOperation.STATS_TAG, i, SensorStats.DELIMITER)));
+            }
+        }
+    }
+
+    /**
+     * Test that the {@link RepeatingSensorOperation} functions correctly.
+     */
+    public void testRepeatingSensorOperation() {
+        final int iterations = 10;
+        final int subOpDurationMs = 100;
+
+        ISensorOperation subOp = new FakeSensorOperation(subOpDurationMs, TimeUnit.MILLISECONDS);
+        ISensorOperation op = new RepeatingSensorOperation(subOp, iterations);
+
+        Set<String> statsKeys = op.getStats().flatten().keySet();
+        assertEquals(0, statsKeys.size());
+
+        long start = System.currentTimeMillis();
+        op.execute();
+        long duration = System.currentTimeMillis() - start;
+        assertTrue(Math.abs(subOpDurationMs * iterations - duration) < THRESHOLD_MS);
+
+        statsKeys = op.getStats().flatten().keySet();
+        assertEquals(iterations, statsKeys.size());
+        for (int i = 0; i < iterations; i++) {
+            assertTrue(statsKeys.contains(String.format("%s_%03d%sexecuted",
+                    RepeatingSensorOperation.STATS_TAG, i, SensorStats.DELIMITER)));
+        }
+    }
+
+    /**
+     * Test that the {@link RepeatingSensorOperation} functions correctly if there is a failure in
+     * a child operation.
+     */
+    public void testRepeatingSensorOperation_fail() {
+        final int iterations = 100;
+        final int failCount = 75;
+
+        ISensorOperation subOp = new FakeSensorOperation(0, TimeUnit.MILLISECONDS) {
+            private int mExecutedCount = 0;
+
+            @Override
+            public void execute() {
+                super.execute();
+                mExecutedCount++;
+
+                if (failCount == mExecutedCount) {
+                    fail("FakeSensorOperation failed");
+                }
+            }
+
+            @Override
+            public FakeSensorOperation clone() {
+                // Don't clone
+                return this;
+            }
+        };
+        ISensorOperation op = new RepeatingSensorOperation(subOp, iterations);
+
+        Set<String> statsKeys = op.getStats().flatten().keySet();
+        assertEquals(0, statsKeys.size());
+
+        try {
+            op.execute();
+            fail("AssertionError expected");
+        } catch (AssertionError e) {
+            // Expected
+            System.out.println(e.getMessage());
+        }
+
+        statsKeys = op.getStats().flatten().keySet();
+        assertEquals(failCount, statsKeys.size());
+        for (int i = 0; i < failCount; i++) {
+            assertTrue(statsKeys.contains(String.format("%s_%03d%sexecuted",
+                    RepeatingSensorOperation.STATS_TAG, i, SensorStats.DELIMITER)));
+        }
+    }
+
+    /**
+     * Test that the {@link SequentialSensorOperation} functions correctly.
+     */
+    public void testSequentialSensorOperation() {
+        final int subOpCount = 10;
+        final int subOpDurationMs = 100;
+
+        SequentialSensorOperation op = new SequentialSensorOperation();
+        for (int i = 0; i < subOpCount; i++) {
+            ISensorOperation subOp = new FakeSensorOperation(subOpDurationMs,
+                    TimeUnit.MILLISECONDS);
+            op.add(subOp);
+        }
+
+        Set<String> statsKeys = op.getStats().flatten().keySet();
+        assertEquals(0, statsKeys.size());
+
+        long start = System.currentTimeMillis();
+        op.execute();
+        long duration = System.currentTimeMillis() - start;
+        assertTrue(Math.abs(subOpDurationMs * subOpCount - duration) < THRESHOLD_MS);
+
+        statsKeys = op.getStats().flatten().keySet();
+        assertEquals(subOpCount, statsKeys.size());
+        for (int i = 0; i < subOpCount; i++) {
+            assertTrue(statsKeys.contains(String.format("%s_%03d%sexecuted",
+                    SequentialSensorOperation.STATS_TAG, i, SensorStats.DELIMITER)));
+        }
+    }
+
+    /**
+     * Test that the {@link SequentialSensorOperation} functions correctly if there is a failure in
+     * a child operation.
+     */
+    public void testSequentialSensorOperation_fail() {
+        final int subOpCount = 100;
+        final int failCount = 75;
+
+        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,
+                    TimeUnit.MILLISECONDS);
+            op.add(subOp);
+        }
+
+        Set<String> statsKeys = op.getStats().flatten().keySet();
+        assertEquals(0, statsKeys.size());
+
+        try {
+            op.execute();
+            fail("AssertionError expected");
+        } catch (AssertionError e) {
+            // Expected
+            System.out.println(e.getMessage());
+        }
+
+        statsKeys = op.getStats().flatten().keySet();
+        assertEquals(failCount, statsKeys.size());
+        for (int i = 0; i < failCount; i++) {
+            assertTrue(statsKeys.contains(String.format("%s_%03d%sexecuted",
+                    SequentialSensorOperation.STATS_TAG, i, SensorStats.DELIMITER)));
+        }
+    }
+}
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
new file mode 100644
index 0000000..b62b867
--- /dev/null
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/SequentialSensorOperation.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2013 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 java.util.LinkedList;
+import java.util.List;
+
+/**
+ * A {@link ISensorOperation} that executes a set of children {@link ISensorOperation}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.
+ */
+public class SequentialSensorOperation extends AbstractSensorOperation {
+    public static final String STATS_TAG = "sequential";
+
+    private final List<ISensorOperation> mOperations = new LinkedList<ISensorOperation>();
+
+    /**
+     * Add a set of {@link ISensorOperation}s.
+     */
+    public void add(ISensorOperation ... operations) {
+        for (ISensorOperation operation : operations) {
+            if (operation == null) {
+                throw new IllegalArgumentException("Arguments cannot be null");
+            }
+            mOperations.add(operation);
+        }
+    }
+
+    /**
+     * Executes the {@link ISensorOperation}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() {
+        for (int i = 0; i < mOperations.size(); i++) {
+            ISensorOperation operation = mOperations.get(i);
+            try {
+                operation.execute();
+            } catch (AssertionError e) {
+                String msg = String.format("Operation %d failed: \"%s\"", i, e.getMessage());
+                throw new AssertionError(msg, e);
+            } finally {
+                addSensorStats(STATS_TAG, i, operation.getStats());
+            }
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public SequentialSensorOperation clone() {
+        SequentialSensorOperation operation = new SequentialSensorOperation();
+        for (ISensorOperation subOperation : mOperations) {
+            operation.add(subOperation.clone());
+        }
+        return operation;
+    }
+}
diff --git a/tests/tests/hardware/src/android/hardware/cts/helpers/sensorTestOperations/VerifySensorOperation.java b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/VerifySensorOperation.java
similarity index 84%
rename from tests/tests/hardware/src/android/hardware/cts/helpers/sensorTestOperations/VerifySensorOperation.java
rename to tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/VerifySensorOperation.java
index b2f268a..de83a94 100644
--- a/tests/tests/hardware/src/android/hardware/cts/helpers/sensorTestOperations/VerifySensorOperation.java
+++ b/tests/tests/hardware/src/android/hardware/cts/helpers/sensoroperations/VerifySensorOperation.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package android.hardware.cts.helpers.sensorTestOperations;
+package android.hardware.cts.helpers.sensoroperations;
 
 import android.content.Context;
 import android.hardware.Sensor;
@@ -22,7 +22,6 @@
 import android.hardware.cts.helpers.SensorCtsHelper;
 import android.hardware.cts.helpers.SensorManagerTestVerifier;
 import android.hardware.cts.helpers.SensorTestInformation;
-import android.hardware.cts.helpers.SensorTestOperation;
 import android.hardware.cts.helpers.SensorVerificationHelper;
 import android.hardware.cts.helpers.SensorVerificationHelper.VerificationResult;
 import android.hardware.cts.helpers.TestSensorEvent;
@@ -30,22 +29,20 @@
 
 import junit.framework.Assert;
 
-import java.util.ArrayList;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 
 /**
- * A {@link SensorTestOperation} used to verify that sensor events and sensor values are correct.
+ * 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 class VerifySensorOperation extends SensorTestOperation {
+public class VerifySensorOperation extends AbstractSensorOperation {
     private static final String TAG = "VerifySensorOperation";
 
     private static final boolean DEBUG = false;
@@ -55,7 +52,9 @@
     private int mSensorType = 0;
     private int mRateUs = 0;
     private int mMaxBatchReportLatencyUs = 0;
-    private int mEventCount = 0;
+    private Integer mEventCount = null;
+    private Long mDuration = null;
+    private TimeUnit mTimeUnit = null;
 
     private boolean mVerifyEventOrdering = false;
 
@@ -103,6 +102,28 @@
     }
 
     /**
+     * Create a {@link VerifySensorOperation}.
+     *
+     * @param context the {@link Context}.
+     * @param sensorType the sensor type
+     * @param rateUs the rate that
+     * @param maxBatchReportLatencyUs the max batch report latency
+     * @param duration the duration to gather events for
+     * @param timeUnit the time unit of the duration
+     */
+    public VerifySensorOperation(Context context, int sensorType, int rateUs,
+            int maxBatchReportLatencyUs, long duration, TimeUnit timeUnit) {
+        mContext = context;
+        mSensorType = sensorType;
+        mRateUs = rateUs;
+        mMaxBatchReportLatencyUs = maxBatchReportLatencyUs;
+        mDuration = duration;
+        mTimeUnit = timeUnit;
+        mSensor = new SensorManagerTestVerifier(mContext, mSensorType, mRateUs,
+                mMaxBatchReportLatencyUs);
+    }
+
+    /**
      * Set all of the default test expectations.
      */
     public void setDefaultVerifications() {
@@ -434,56 +455,65 @@
      * Collect the specified number of events from the sensor and run all enabled verifications.
      */
     @Override
-    public void doWork() {
-        TestSensorEvent[] events = mSensor.collectEvents(mEventCount);
+    public void execute() {
+        addValue("sensor_name", SensorTestInformation.getSensorName(mSensorType));
+        addValue("sensor_handle", mSensor.getUnderlyingSensor().getHandle());
+
+        TestSensorEvent[] events;
+        if (mEventCount != null) {
+            events = mSensor.collectEvents(mEventCount);
+        } else {
+            events = mSensor.collectEvents(mDuration, mTimeUnit);
+        }
 
         boolean failed = false;
         StringBuilder sb = new StringBuilder();
         VerificationResult result = null;
-        Map<String, Object> stats = new HashMap<String, Object>();
 
         if (mVerifyEventOrdering) {
             result = SensorVerificationHelper.verifyEventOrdering(events);
             // evaluateResults first so it is always called.
-            failed = evaluateResults(result, sb, stats) || failed;
+            failed |= evaluateResults(result, sb);
         }
 
         if (mVerifyFrequency) {
             result = SensorVerificationHelper.verifyFrequency(events, mFrequencyExpected,
                     mFrequencyThreshold);
-            failed = evaluateResults(result, sb, stats) || failed;
+            failed |= evaluateResults(result, sb);
         }
 
         if (mVerifyJitter) {
             result = SensorVerificationHelper.verifyJitter(events, mJitterExpected,
                     mJitterThreshold);
-            failed = evaluateResults(result, sb, stats) || failed;
+            failed |= evaluateResults(result, sb);
         }
 
         if (mVerifyMean) {
             result = SensorVerificationHelper.verifyMean(events, mMeanExpected, mMeanThreshold);
-            failed = evaluateResults(result, sb, stats) || failed;
+            failed |= evaluateResults(result, sb);
         }
 
         if (mVerifyMagnitude) {
             result = SensorVerificationHelper.verifyMagnitude(events, mMagnitudeExpected,
                     mMagnitudeThreshold);
-            failed = evaluateResults(result, sb, stats) || failed;
+            failed |= evaluateResults(result, sb);
         }
 
         if (mVerifySignum) {
             result = SensorVerificationHelper.verifySignum(events, mSignumExpected,
                     mSignumThreshold);
-            failed = evaluateResults(result, sb, stats) || failed;
+            failed |= evaluateResults(result, sb);
         }
 
         if (mVerifyStandardDeviation) {
             result = SensorVerificationHelper.verifyStandardDeviation(events,
                     mStandardDeviationThreshold);
-            failed = evaluateResults(result, sb, stats) || failed;
+            failed |= evaluateResults(result, sb);
         }
 
-        logStats(events, stats);
+        if (DEBUG) {
+            logStats(events);
+        }
 
         if (failed) {
             Assert.fail(String.format("%s, handle %d: %s",
@@ -493,6 +523,43 @@
     }
 
     /**
+     * {@inheritDoc}
+     */
+    @Override
+    public VerifySensorOperation clone() {
+        VerifySensorOperation operation;
+        if (mEventCount != null) {
+            operation = new VerifySensorOperation(mContext, mSensorType, mRateUs,
+                    mMaxBatchReportLatencyUs, mEventCount);
+        } else {
+            operation = new VerifySensorOperation(mContext, mSensorType, mRateUs,
+                    mMaxBatchReportLatencyUs, mDuration, mTimeUnit);
+        }
+        if (mVerifyEventOrdering) {
+            operation.verifyEventOrdering();
+        }
+        if (mVerifyFrequency) {
+            operation.verifyFrequency(mFrequencyExpected, mFrequencyThreshold);
+        }
+        if (mVerifyJitter) {
+            operation.verifyJitter(mJitterExpected, mJitterThreshold);
+        }
+        if (mVerifyMean) {
+            operation.verifyMean(mMeanExpected, mMeanThreshold);
+        }
+        if (mVerifyMagnitude) {
+            operation.verifyMagnitude(mMagnitudeExpected, mMagnitudeThreshold);
+        }
+        if (mVerifySignum) {
+            operation.verifySignum(mSignumExpected, mSignumThreshold);
+        }
+        if (mVerifyStandardDeviation) {
+            operation.verifyStandardDeviation(mStandardDeviationThreshold);
+        }
+        return operation;
+    }
+
+    /**
      * Return true if the operation rate is not one of {@link SensorManager#SENSOR_DELAY_GAME},
      * {@link SensorManager#SENSOR_DELAY_UI}, or {@link SensorManager#SENSOR_DELAY_NORMAL}.
      */
@@ -505,10 +572,9 @@
     /**
      * Evaluate the results of a test, aggregate the stats, and build the error message.
      */
-    private boolean evaluateResults(VerificationResult result, StringBuilder sb,
-            Map<String, Object> stats) {
+    private boolean evaluateResults(VerificationResult result, StringBuilder sb) {
         for (String key : result.getKeys()) {
-            stats.put(key, result.getValue(key));
+            addValue(key, result.getValue(key));
         }
 
         if (result.isFailed()) {
@@ -522,33 +588,22 @@
     }
 
     /**
-     * Log the stats to the logcat.
+     * Log the events to the logcat.
      */
-    private void logStats(TestSensorEvent[] events, Map<String, Object> stats) {
+    private void logStats(TestSensorEvent[] events) {
         if (events.length <= 0) {
             return;
         }
 
-        if (DEBUG) {
-            List<Double> jitterValues = null;
-            if (events.length > 1) {
-                jitterValues = SensorCtsHelper.getJitterValues(events);
-            }
-
-            logTestSensorEvent(0, events[0], null, null);
-            for (int i = 1; i < events.length; i++) {
-                Double jitter = jitterValues == null ? null : jitterValues.get(i - 1);
-                logTestSensorEvent(i, events[i], events[i - 1], jitter);
-            }
+        List<Double> jitterValues = null;
+        if (events.length > 1) {
+            jitterValues = SensorCtsHelper.getJitterValues(events);
         }
 
-        List<String> keys = new ArrayList<String>(stats.keySet());
-        Collections.sort(keys);
-        Log.v(TAG, String.format("Stats for %s, handle %d:",
-                SensorTestInformation.getSensorName(mSensorType),
-                mSensor.getUnderlyingSensor().getHandle()));
-        for (String key : keys) {
-            Log.v(TAG, String.format("%s: %s", key, stats.get(key).toString()));
+        logTestSensorEvent(0, events[0], null, null);
+        for (int i = 1; i < events.length; i++) {
+            Double jitter = jitterValues == null ? null : jitterValues.get(i - 1);
+            logTestSensorEvent(i, events[i], events[i - 1], jitter);
         }
     }