Merge "Migration of hermetic startup test to CrystalBall."
diff --git a/build/tasks/platform_tests.mk b/build/tasks/platform_tests.mk
index 9c88a6c..1bed4de 100644
--- a/build/tasks/platform_tests.mk
+++ b/build/tasks/platform_tests.mk
@@ -16,7 +16,8 @@
 # based on the configuration.
 #
 
-include $(call my-dir)/tests/platform_test_list.mk
+LOCAL_PATH := $(call my-dir)
+include $(LOCAL_PATH)/tests/platform_test_list.mk
 -include $(wildcard vendor/*/build/tasks/tests/platform_test_list.mk)
 
 my_modules := $(platform_tests)
diff --git a/build/tasks/tests/instrumentation_test_list.mk b/build/tasks/tests/instrumentation_test_list.mk
index f6761d9..5ef9952 100644
--- a/build/tasks/tests/instrumentation_test_list.mk
+++ b/build/tasks/tests/instrumentation_test_list.mk
@@ -70,9 +70,6 @@
     FrameworksPrivacyLibraryTests \
     SettingsUITests \
     ExtServicesUnitTests\
-    NexusLauncherOutOfProcTests\
-    NexusLauncherDebug\
-    NexusLauncherTests\
     FrameworksNetSmokeTests\
 
 
diff --git a/build/tasks/tests/native_test_list.mk b/build/tasks/tests/native_test_list.mk
index 0ff1395..093ccb4 100644
--- a/build/tasks/tests/native_test_list.mk
+++ b/build/tasks/tests/native_test_list.mk
@@ -14,9 +14,7 @@
 
 native_tests := \
     adbd_test \
-    apex_file_test \
-    apex_manifest_test \
-    apexservice_test \
+    audio_health_tests \
     backtrace_test \
     bionic-unit-tests \
     bionic-unit-tests-static \
@@ -41,6 +39,7 @@
     dvr_api-test \
     dvr_buffer_queue-test \
     dvr_display-test \
+    gwp_asan_unittest \
     hello_world_test \
     hwui_unit_tests \
     incident_helper_test \
@@ -51,7 +50,6 @@
     installd_otapreopt_test \
     installd_service_test \
     installd_utils_test \
-    JniInvocation_test \
     libandroidfw_tests \
     libappfuse_test \
     libbase_test \
@@ -64,6 +62,7 @@
     libjavacore-unit-tests \
     liblog-unit-tests \
     libminijail_unittest_gtest \
+    libnativehelper_tests \
     libnetdbpf_test \
     libperfmgr_test \
     libprocinfo_test \
@@ -130,7 +129,6 @@
     NeuralNetworksTest_mt_static \
     NeuralNetworksTest_operations \
     NeuralNetworksTest_static \
-    NeuralNetworksTest_static_asan \
     SurfaceFlinger_test \
     lmkd_unit_test \
     vrflinger_test
diff --git a/build/tasks/tests/platform_test_list.mk b/build/tasks/tests/platform_test_list.mk
index d77a7d1..58aa694 100644
--- a/build/tasks/tests/platform_test_list.mk
+++ b/build/tasks/tests/platform_test_list.mk
@@ -1,7 +1,4 @@
 platform_tests += \
-    apex_file_test \
-    apex_manifest_test \
-    apexservice_test \
     ActivityManagerPerfTests \
     ActivityManagerPerfTestsTestApp \
     AndroidTVJankTests \
@@ -20,8 +17,6 @@
     BandwidthEnforcementTest \
     BandwidthTests \
     BluetoothTests \
-    bluetooth_cert_stack \
-    bluetooth_stack_with_facade \
     BootHelperApp \
     BusinessCard \
     CalculatorFunctionalTests \
@@ -58,9 +53,7 @@
     FrameworksUtilTests \
     InternalLocTestApp \
     JankMicroBenchmarkTests \
-    libbluetooth_gd \
     long_trace_config.textproto \
-    libgrpc++_unsecure \
     MemoryUsage \
     MultiDexLegacyTestApp \
     MultiDexLegacyTestApp2 \
@@ -86,9 +79,8 @@
     PermissionFunctionalTests \
     PermissionTestAppMV1 \
     PermissionUtils \
-    PlatformScenarioTests \
+    PlatformCommonScenarioTests \
     PowerPerfTest \
-    root-canal \
     SettingsUITests \
     SimpleTestApp \
     skia_dm \
@@ -98,7 +90,9 @@
     SmokeTestApp \
     SysAppJankTestsWear \
     TouchLatencyJankTestWear \
+    trace_config.textproto \
     trace_config_detailed.textproto \
+    trace_config_experimental.textproto \
     UbSystemUiJankTests \
     UbWebViewJankTests \
     UiBench \
@@ -125,3 +119,7 @@
     CuttlefishRilTests \
     CuttlefishWifiTests
 endif
+
+ifeq ($(HOST_OS),linux)
+platform_tests += root-canal
+endif
diff --git a/libraries/aoa-helper/src/com/android/helper/aoa/AoaDevice.java b/libraries/aoa-helper/src/com/android/helper/aoa/AoaDevice.java
index e56c4ac..b43690f 100644
--- a/libraries/aoa-helper/src/com/android/helper/aoa/AoaDevice.java
+++ b/libraries/aoa-helper/src/com/android/helper/aoa/AoaDevice.java
@@ -22,10 +22,12 @@
 import com.google.common.primitives.Bytes;
 import com.google.common.util.concurrent.Uninterruptibles;
 
-import java.awt.*;
+import java.awt.Point;
 import java.time.Duration;
+import java.time.Instant;
 import java.util.Arrays;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Objects;
 import java.util.concurrent.TimeUnit;
 
@@ -80,8 +82,6 @@
     private static final Duration ACTION_DELAY = Duration.ofSeconds(3L);
     private static final Duration STEP_DELAY = Duration.ofMillis(10L);
     static final Duration LONG_CLICK = Duration.ofSeconds(1L);
-    static final int SCROLL_STEPS = 40;
-    static final int FLING_STEPS = 10;
 
     private final UsbHelper mHelper;
     private UsbDevice mDelegate;
@@ -183,6 +183,12 @@
                 && ADB_PID.contains(mDelegate.getProductId());
     }
 
+    /** Get current time. */
+    @VisibleForTesting
+    Instant now() {
+        return Instant.now();
+    }
+
     /** Wait for a specified duration. */
     public void sleep(@Nonnull Duration duration) {
         Uninterruptibles.sleepUninterruptibly(duration.toNanos(), TimeUnit.NANOSECONDS);
@@ -204,30 +210,26 @@
         touch(TOUCH_UP, point, ACTION_DELAY);
     }
 
-    /** Scroll from one location to another. */
-    public void scroll(@Nonnull Point from, @Nonnull Point to) {
-        swipe(from, to, SCROLL_STEPS);
-    }
-
-    /** Fling from one location to another. */
-    public void fling(@Nonnull Point from, @Nonnull Point to) {
-        swipe(from, to, FLING_STEPS);
-    }
-
-    /** Drag from one location to another. */
-    public void drag(@Nonnull Point from, @Nonnull Point to) {
-        touch(TOUCH_DOWN, from, LONG_CLICK);
-        scroll(from, to);
-    }
-
-    // Move from one location to another using discrete steps
-    private void swipe(Point from, Point to, int steps) {
-        steps = Math.max(steps, 1);
-        float xStep = ((float) (to.x - from.x)) / steps;
-        float yStep = ((float) (to.y - from.y)) / steps;
-
-        for (int i = 0; i <= steps; i++) {
-            Point point = new Point((int) (from.x + xStep * i), (int) (from.y + yStep * i));
+    /**
+     * Swipe from one position to another in the specified duration.
+     *
+     * @param from starting position
+     * @param to final position
+     * @param duration swipe motion duration
+     */
+    public void swipe(@Nonnull Point from, @Nonnull Point to, @Nonnull Duration duration) {
+        Instant start = now();
+        touch(TOUCH_DOWN, from, STEP_DELAY);
+        while (true) {
+            Duration elapsed = Duration.between(start, now());
+            if (duration.compareTo(elapsed) < 0) {
+                break;
+            }
+            double progress = (double) elapsed.toMillis() / duration.toMillis();
+            Point point =
+                    new Point(
+                            (int) (progress * to.x + (1 - progress) * from.x),
+                            (int) (progress * to.y + (1 - progress) * from.y));
             touch(TOUCH_DOWN, point, STEP_DELAY);
         }
         touch(TOUCH_UP, to, ACTION_DELAY);
@@ -242,38 +244,23 @@
     }
 
     /**
-     * Write a string by pressing keys. Only alphanumeric characters and whitespace is supported.
+     * Press a combination of keys.
      *
-     * @param value string to write
+     * @param keys key HID usages, see <a
+     *     https://source.android.com/devices/input/keyboard-devices">Keyboard devices</a>
      */
-    public void write(@Nonnull String value) {
-        // map characters to HID usages
-        Integer[] keyCodes =
-                value.codePoints()
-                        .mapToObj(
-                                c -> {
-                                    if (Character.isSpaceChar(c)) {
-                                        return 0x2C;
-                                    } else if (Character.isAlphabetic(c)) {
-                                        return Character.toLowerCase(c) - 'a' + 0x04;
-                                    } else if (Character.isDigit(c)) {
-                                        return c == '0' ? 0x27 : c - '1' + 0x1E;
-                                    }
-                                    return null;
-                                })
-                        .toArray(Integer[]::new);
-        // press the keys
-        key(keyCodes);
+    public void pressKeys(Integer... keys) {
+        pressKeys(Arrays.asList(keys));
     }
 
     /**
-     * Press a key.
+     * Press a combination of keys.
      *
-     * @param keyCodes key HID usages, see <a
+     * @param keys list of key HID usages, see <a
      *     https://source.android.com/devices/input/keyboard-devices">Keyboard devices</a>
      */
-    public void key(Integer... keyCodes) {
-        Iterator<Integer> it = Arrays.stream(keyCodes).filter(Objects::nonNull).iterator();
+    public void pressKeys(@Nonnull List<Integer> keys) {
+        Iterator<Integer> it = keys.stream().filter(Objects::nonNull).iterator();
         while (it.hasNext()) {
             Integer keyCode = it.next();
             send(HID.KEYBOARD, new byte[] {keyCode.byteValue()}, STEP_DELAY);
diff --git a/libraries/aoa-helper/src/com/android/helper/aoa/UsbDevice.java b/libraries/aoa-helper/src/com/android/helper/aoa/UsbDevice.java
index b7a2755..62d7a30 100644
--- a/libraries/aoa-helper/src/com/android/helper/aoa/UsbDevice.java
+++ b/libraries/aoa-helper/src/com/android/helper/aoa/UsbDevice.java
@@ -30,12 +30,14 @@
 /** Connected USB device. */
 public class UsbDevice implements AutoCloseable {
 
+    private final UsbHelper mHelper;
     private final IUsbNative mUsb;
     private final byte[] mDescriptor = new byte[18];
     private Pointer mHandle;
 
-    UsbDevice(@Nonnull IUsbNative usb, @Nonnull Pointer devicePointer) {
-        mUsb = usb;
+    UsbDevice(@Nonnull UsbHelper helper, @Nonnull Pointer devicePointer) {
+        mHelper = helper;
+        mUsb = helper.getUsb();
 
         // retrieve device descriptor
         mUsb.libusb_get_device_descriptor(devicePointer, mDescriptor);
@@ -47,11 +49,22 @@
     }
 
     /**
-     * Performs a synchronous control transaction with unlimited timeout.
+     * Performs a synchronous control transaction with the default timeout.
      *
      * @return number of bytes transferred, or an error code
      */
     public int controlTransfer(byte requestType, byte request, int value, int index, byte[] data) {
+        int timeout = (int) mHelper.getTransferTimeout().toMillis();
+        return controlTransfer(requestType, request, value, index, data, timeout);
+    }
+
+    /**
+     * Performs a synchronous control transaction.
+     *
+     * @return number of bytes transferred, or an error code
+     */
+    public int controlTransfer(
+            byte requestType, byte request, int value, int index, byte[] data, int timeout) {
         return mUsb.libusb_control_transfer(
                 checkNotNull(mHandle),
                 requestType,
@@ -60,7 +73,7 @@
                 (short) index,
                 data,
                 (short) data.length,
-                0);
+                timeout);
     }
 
     /**
diff --git a/libraries/aoa-helper/src/com/android/helper/aoa/UsbHelper.java b/libraries/aoa-helper/src/com/android/helper/aoa/UsbHelper.java
index 1a3b86d..18e7c67 100644
--- a/libraries/aoa-helper/src/com/android/helper/aoa/UsbHelper.java
+++ b/libraries/aoa-helper/src/com/android/helper/aoa/UsbHelper.java
@@ -42,11 +42,16 @@
  */
 public class UsbHelper implements AutoCloseable {
 
+    // Interval used when waiting for a device
     private static final Duration POLL_INTERVAL = Duration.ofSeconds(1L);
+    // Default transfer timeout used by all managed devices
+    private static final Duration DEFAULT_TRANSFER_TIMEOUT = Duration.ofSeconds(10L);
 
     private final IUsbNative mUsb;
     private Pointer mContext;
 
+    private Duration mTransferTimeout = DEFAULT_TRANSFER_TIMEOUT;
+
     public UsbHelper() {
         this((IUsbNative) Native.loadLibrary("usb-1.0", IUsbNative.class));
     }
@@ -60,6 +65,21 @@
         mContext = context.getValue();
     }
 
+    /** @return native libusb adapter */
+    IUsbNative getUsb() {
+        return mUsb;
+    }
+
+    /** @return default transfer timeout (Duration.ZERO indicates unlimited timeout). */
+    public Duration getTransferTimeout() {
+        return mTransferTimeout;
+    }
+
+    /** Sets the default transfer timeout used by all managed devices. */
+    public void setTransferTimeout(@Nonnull Duration transferTimeout) {
+        mTransferTimeout = transferTimeout;
+    }
+
     /**
      * Verifies a USB response, throwing an exception if it corresponds to an error.
      *
@@ -122,7 +142,7 @@
 
     @VisibleForTesting
     UsbDevice connect(@Nonnull Pointer devicePointer) {
-        return new UsbDevice(mUsb, devicePointer);
+        return new UsbDevice(this, devicePointer);
     }
 
     /**
diff --git a/libraries/aoa-helper/tests/src/com/android/helper/aoa/AoaDeviceTest.java b/libraries/aoa-helper/tests/src/com/android/helper/aoa/AoaDeviceTest.java
index 8368cf7..f40c153 100644
--- a/libraries/aoa-helper/tests/src/com/android/helper/aoa/AoaDeviceTest.java
+++ b/libraries/aoa-helper/tests/src/com/android/helper/aoa/AoaDeviceTest.java
@@ -22,10 +22,8 @@
 import static com.android.helper.aoa.AoaDevice.ACCESSORY_START_MAX_RETRIES;
 import static com.android.helper.aoa.AoaDevice.ACCESSORY_UNREGISTER_HID;
 import static com.android.helper.aoa.AoaDevice.DEVICE_NOT_FOUND;
-import static com.android.helper.aoa.AoaDevice.FLING_STEPS;
 import static com.android.helper.aoa.AoaDevice.GOOGLE_VID;
 import static com.android.helper.aoa.AoaDevice.LONG_CLICK;
-import static com.android.helper.aoa.AoaDevice.SCROLL_STEPS;
 import static com.android.helper.aoa.AoaDevice.SYSTEM_BACK;
 import static com.android.helper.aoa.AoaDevice.SYSTEM_HOME;
 import static com.android.helper.aoa.AoaDevice.SYSTEM_WAKE;
@@ -63,11 +61,11 @@
 
 import java.awt.*;
 import java.time.Duration;
+import java.time.Instant;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Objects;
 import java.util.stream.Collectors;
-import java.util.stream.Stream;
 
 import javax.annotation.Nonnull;
 
@@ -230,67 +228,26 @@
     }
 
     @Test
-    public void testScroll() {
+    public void testSwipe() {
         mDevice = createDevice();
-        mDevice.scroll(new Point(0, 0), new Point(0, SCROLL_STEPS));
+        mDevice.swipe(new Point(20, 0), new Point(70, 100), Duration.ofMillis(50));
 
-        // generate an event for each step, spaced one pixel apart
         List<Touch> events =
-                Stream.iterate(0, i -> i + 1)
-                        .limit(SCROLL_STEPS + 1)
-                        .map(i -> new Touch(TOUCH_DOWN, 0, i))
-                        .collect(Collectors.toList());
-        events.add(new Touch(TOUCH_UP, 0, SCROLL_STEPS));
-
+                List.of(
+                        new Touch(TOUCH_DOWN, 20, 0),
+                        new Touch(TOUCH_DOWN, 30, 20),
+                        new Touch(TOUCH_DOWN, 40, 40),
+                        new Touch(TOUCH_DOWN, 50, 60),
+                        new Touch(TOUCH_DOWN, 60, 80),
+                        new Touch(TOUCH_DOWN, 70, 100),
+                        new Touch(TOUCH_UP, 70, 100));
         verifyTouches(events);
     }
 
     @Test
-    public void testFling() {
+    public void testPressKeys() {
         mDevice = createDevice();
-        mDevice.fling(new Point(0, 0), new Point(FLING_STEPS, 0));
-
-        // generate an event for each step, spaced one pixel apart
-        List<Touch> events =
-                Stream.iterate(0, i -> i + 1)
-                        .limit(FLING_STEPS + 1)
-                        .map(i -> new Touch(TOUCH_DOWN, i, 0))
-                        .collect(Collectors.toList());
-        events.add(new Touch(TOUCH_UP, FLING_STEPS, 0));
-
-        verifyTouches(events);
-    }
-
-    @Test
-    public void testDrag() {
-        mDevice = createDevice();
-        mDevice.drag(new Point(0, 0), new Point(SCROLL_STEPS, 0));
-
-        // generate an event for each step, spaced one pixel apart
-        List<Touch> events =
-                Stream.iterate(0, i -> i + 1)
-                        .limit(SCROLL_STEPS + 1)
-                        .map(i -> new Touch(TOUCH_DOWN, i, 0))
-                        .collect(Collectors.toList());
-        // drag is a long click followed by a scroll
-        events.add(0, new Touch(TOUCH_DOWN, 0, 0));
-        events.add(new Touch(TOUCH_UP, SCROLL_STEPS, 0));
-
-        verifyTouches(events);
-    }
-
-    @Test
-    public void testWrite() {
-        mDevice = spy(createDevice());
-        mDevice.write("Test #0123!");
-
-        verify(mDevice).key(0x17, 0x08, 0x16, 0x17, 0x2C, null, 0x27, 0x1E, 0x1F, 0x20, null);
-    }
-
-    @Test
-    public void testKey() {
-        mDevice = createDevice();
-        mDevice.key(1, null, 2);
+        mDevice.pressKeys(1, null, 2);
 
         InOrder order = inOrder(mDelegate);
         // press and release 1
@@ -347,11 +304,25 @@
 
     // Helpers
 
+    /** Creates a mock device with predictable timestamps. */
     private AoaDevice createDevice() {
-        AoaDevice device = new AoaDevice(mHelper, mDelegate) {
-            @Override
-            public void sleep(@Nonnull Duration duration) {}
-        };
+        AoaDevice device =
+                new AoaDevice(mHelper, mDelegate) {
+                    private Instant mInstant;
+
+                    @Override
+                    Instant now() {
+                        if (mInstant == null) {
+                            mInstant = Instant.MIN;
+                        }
+                        return mInstant;
+                    }
+
+                    @Override
+                    public void sleep(@Nonnull Duration duration) {
+                        mInstant = now().plus(duration);
+                    }
+                };
         return spy(device);
     }
 
diff --git a/libraries/aoa-helper/tests/src/com/android/helper/aoa/UsbDeviceTest.java b/libraries/aoa-helper/tests/src/com/android/helper/aoa/UsbDeviceTest.java
index dfb3067..7a85915 100644
--- a/libraries/aoa-helper/tests/src/com/android/helper/aoa/UsbDeviceTest.java
+++ b/libraries/aoa-helper/tests/src/com/android/helper/aoa/UsbDeviceTest.java
@@ -35,6 +35,8 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
+import java.time.Duration;
+
 /** Unit tests for {@link UsbDevice} */
 @RunWith(JUnit4.class)
 public class UsbDeviceTest {
@@ -43,6 +45,7 @@
 
     private Pointer mHandle;
     private IUsbNative mUsb;
+    private UsbHelper mHelper;
 
     @Before
     public void setUp() {
@@ -50,6 +53,7 @@
         mHandle = new Memory(1);
 
         mUsb = mock(IUsbNative.class);
+        mHelper = new UsbHelper(mUsb);
         // return device handle when opening connection
         when(mUsb.libusb_open(any(), any()))
                 .then(
@@ -60,7 +64,7 @@
                             return 0;
                         });
 
-        mDevice = new UsbDevice(mUsb, mock(Pointer.class));
+        mDevice = new UsbDevice(mHelper, mock(Pointer.class));
     }
 
     @Test
@@ -75,6 +79,8 @@
 
     @Test
     public void testControlTransfer() {
+        mHelper.setTransferTimeout(Duration.ofMillis(123L));
+
         byte[] data = new byte[]{1, 2, 3, 4};
         mDevice.controlTransfer((byte) 1, (byte) 2, 3, 4, data);
 
@@ -82,7 +88,7 @@
         verify(mUsb).libusb_control_transfer(eq(mHandle),
                 eq((byte) 1), eq((byte) 2), eq((short) 3), eq((short) 4),
                 eq(data), eq((short) 4), // data and length
-                eq(0)); // timeout
+                eq(123)); // default timeout
     }
 
     @Test(expected = NullPointerException.class)
diff --git a/libraries/app-helpers/core/src/android/platform/helpers/AbstractStandardAppHelper.java b/libraries/app-helpers/core/src/android/platform/helpers/AbstractStandardAppHelper.java
index fe94bd3..3310915 100644
--- a/libraries/app-helpers/core/src/android/platform/helpers/AbstractStandardAppHelper.java
+++ b/libraries/app-helpers/core/src/android/platform/helpers/AbstractStandardAppHelper.java
@@ -53,6 +53,7 @@
     private static final String SCREENSHOT_DIR = "apphelper-screenshots";
     private static final String FAVOR_CMD = "favor-shell-commands";
     private static final String USE_HOME_CMD = "press-home-to-exit";
+    private static final String APP_IDLE_OPTION = "app-idle_ms";
     private static final String LAUNCH_TIMEOUT_OPTION = "app-launch-timeout_ms";
     private static final String ERROR_NOT_FOUND =
         "Element %s %s is not found in the application %s";
@@ -68,6 +69,7 @@
             KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
     private final boolean mFavorShellCommands;
     private final boolean mPressHomeToExit;
+    private final long mAppIdle;
     private final long mLaunchTimeout;
 
     public AbstractStandardAppHelper(Instrumentation instr) {
@@ -79,6 +81,12 @@
         mPressHomeToExit =
                 Boolean.valueOf(
                         InstrumentationRegistry.getArguments().getString(USE_HOME_CMD, "false"));
+        mAppIdle =
+                Long.valueOf(
+                        InstrumentationRegistry.getArguments()
+                                .getString(
+                                        APP_IDLE_OPTION,
+                                        String.valueOf(TimeUnit.SECONDS.toMillis(0))));
         //TODO(b/127356533): Choose a sensible default for app launch timeout after b/125356281.
         mLaunchTimeout =
                 Long.valueOf(
@@ -115,12 +123,7 @@
             String output = null;
             try {
                 Log.i(LOG_TAG, String.format("Sending command to launch: %s", pkg));
-                Intent intent =
-                        mInstrumentation
-                                .getContext()
-                                .getPackageManager()
-                                .getLaunchIntentForPackage(pkg);
-                mInstrumentation.getContext().startActivity(intent);
+                mInstrumentation.getContext().startActivity(getOpenAppIntent());
             } catch (ActivityNotFoundException e) {
                 removeDialogWatchers();
                 throw new TestHelperException(String.format("Failed to find package: %s", pkg), e);
@@ -143,6 +146,33 @@
                             pkg, System.currentTimeMillis() - launchInitiationTimeMs));
         }
         removeDialogWatchers();
+        // Idle for specified time after app launch
+        idleApp();
+    }
+
+    private void idleApp() {
+        if (mAppIdle != 0) {
+            Log.v(LOG_TAG, String.format("Idle app for %d ms", mAppIdle));
+            SystemClock.sleep(mAppIdle);
+        }
+    }
+
+    /**
+     * Returns the {@code Intent} used by {@code open()} to launch an {@code Activity}. The default
+     * implementation launches the default {@code Activity} of the package. Override this method to
+     * launch a different {@code Activity}.
+     */
+    public Intent getOpenAppIntent() {
+        Intent intent =
+                mInstrumentation
+                        .getContext()
+                        .getPackageManager()
+                        .getLaunchIntentForPackage(getPackage());
+        if (intent == null) {
+            throw new IllegalStateException(
+                    String.format("Failed to get intent of package: %s", getPackage()));
+        }
+        return intent;
     }
 
     /**
diff --git a/libraries/app-helpers/core/src/android/platform/helpers/HelperAccessor.java b/libraries/app-helpers/core/src/android/platform/helpers/HelperAccessor.java
index b95baa9..c1f0c88 100644
--- a/libraries/app-helpers/core/src/android/platform/helpers/HelperAccessor.java
+++ b/libraries/app-helpers/core/src/android/platform/helpers/HelperAccessor.java
@@ -20,38 +20,47 @@
 /**
  * A {@code HelperAccessor} can be included in any test to access an App Helper implementation.
  *
- * <p>For example:
- * <code>
+ * <p>For example: <code>
  *     HelperAccessor<IXHelper> accessor = new HelperAccessor(IXHelper.class);
  *     accessor.get().performSomeAction();
- * </code>
+ * </code> To target a specific helper implementation by prefix, build this object and call, <code>
+ * withPrefix</code> on it.
  */
 public class HelperAccessor<T extends IAppHelper> {
     private final Class<T> mInterfaceClass;
+
     private T mHelper;
+    private String mPrefix;
 
     public HelperAccessor(Class<T> klass) {
         mInterfaceClass = klass;
     }
 
+    /** Selects only helpers that begin with the prefix, {@code prefix}. */
+    public HelperAccessor<T> withPrefix(String prefix) {
+        mPrefix = prefix;
+        // Unset the helper, in case this was changed after first use.
+        mHelper = null;
+        // Return self to follow a pseudo-builder initialization pattern.
+        return this;
+    }
+
     public T get() {
         if (mHelper == null) {
-            mHelper = HelperManager.getInstance(
-                    InstrumentationRegistry.getContext(),
-                    InstrumentationRegistry.getInstrumentation())
-                        .get(mInterfaceClass);
+            if (mPrefix == null || mPrefix.isEmpty()) {
+                mHelper =
+                        HelperManager.getInstance(
+                                        InstrumentationRegistry.getContext(),
+                                        InstrumentationRegistry.getInstrumentation())
+                                .get(mInterfaceClass);
+            } else {
+                mHelper =
+                        HelperManager.getInstance(
+                                        InstrumentationRegistry.getContext(),
+                                        InstrumentationRegistry.getInstrumentation())
+                                .get(mInterfaceClass, mPrefix);
+            }
         }
         return mHelper;
     }
-
-    public T get(String prefix) {
-        if (mHelper == null) {
-            mHelper = HelperManager.getInstance(
-                InstrumentationRegistry.getContext(),
-                InstrumentationRegistry.getInstrumentation())
-                .get(mInterfaceClass, prefix);
-        }
-        return mHelper;
-    }
-
 }
diff --git a/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoDateTimeSettingsHelper.java b/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoDateTimeSettingsHelper.java
index 6882b92..2866fe5 100644
--- a/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoDateTimeSettingsHelper.java
+++ b/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoDateTimeSettingsHelper.java
@@ -26,7 +26,7 @@
     /**
      * Setup expectation: Date & time setting is open
      *
-     * Set the device date
+     * <p>Set the device date.
      *
      * @param date - input LocalDate object
      */
@@ -35,33 +35,44 @@
     /**
      * Setup expectation: Date & time setting is open
      *
-     * Get the current date displayed on the UI in LocalDate object
+     * <p>Get the current date displayed on the UI in LocalDate object.
      */
     LocalDate getDate();
 
     /**
      * Setup expectation: Date & time setting is open
      *
-     * Set the device time
+     * <p>Set the device time in 12-hour format
      *
      * @param hour - input hour
      * @param minute - input minute
-     * @param AM_PM - input am/pm
+     * @param is_am - input am/pm
      */
-    void setTime(int hour, int minute, boolean is_am);
+    void setTimeInTwelveHourFormat(int hour, int minute, boolean is_am);
 
     /**
      * Setup expectation: Date & time setting is open
      *
-     * Get the current time displayed on the UI
-     * The return string format will match the UI format exactly
+     * <p>Set the device time in 24-hour format
+     *
+     * @param hour - input hour
+     * @param minute - input minute
+     */
+    void setTimeInTwentyFourHourFormat(int hour, int minute);
+
+    /**
+     * Setup expectation: Date & time setting is open
+     *
+     * <p>Get the current time displayed on the UI.
+     *
+     * @return returned time format will match the UI format exactly
      */
     String getTime();
 
     /**
      * Setup expectation: Date & time setting is open
      *
-     * Set the device time zone
+     * <p>Set the device time zone.
      *
      * @param timezone - city selected for timezone
      */
@@ -70,21 +81,21 @@
     /**
      * Setup expectation: Date & time setting is open
      *
-     * Get the current timezone displayed on the UI
+     * <p>Get the current timezone displayed on the UI.
      */
     String getTimeZone();
 
     /**
      * Setup expectation: Date & time setting is open
      *
-     * Check if the 24 hour format menu switch widget is toggoled on
+     * <p>Check if the 24 hour format is enabled
      */
-    boolean isUseTwentyFourHourFormatSwitchWidgetOn();
+    boolean isTwentyFourHourFormatEnabled();
 
     /**
      * Setup expectation: Date & time setting is open
      *
-     * Toggle on/off 24 hour format widget switch
+     * <p>Toggle on/off 24 hour format widget switch.
      */
     boolean toggleTwentyFourHourFormatSwitch();
 }
diff --git a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IPhotosHelper.java b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IPhotosHelper.java
index 85b243d..15684fd 100644
--- a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IPhotosHelper.java
+++ b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IPhotosHelper.java
@@ -112,22 +112,22 @@
     public void openPicture(int index);
 
     /**
-     * Setup expectations: Photos is open and a picture album is open.
+     * Setup expectations: Photos is open and a picture is open.
      *
-     * This method will scroll the picture album in the specified direction.
+     * <p>This method will scroll to next or previous picture in the specified direction.
      *
      * @param direction The direction to scroll, must be LEFT or RIGHT.
-     * @return Returns whether album can be still scrolled in the given direction
+     * @return Returns whether picture can be still scrolled in the given direction
      */
-    public boolean scrollAlbum(Direction direction);
+    public boolean scrollPicture(Direction direction);
 
     /**
-     * Setup expectations: Photos is open and a picture folder is open.
+     * Setup expectations: Photos is open and a page contains pictures or albums is open.
      *
-     * This method will scroll the Photos grid view in the specified direction.
+     * <p>This method will scroll the page in the specified direction.
      *
      * @param direction The direction of the scroll, must be UP or DOWN.
      * @return Returns whether the object can still scroll in the given direction
      */
-    public boolean scrollGridView(Direction direction);
+    public boolean scrollPage(Direction direction);
 }
diff --git a/libraries/collectors-helper/jank/src/com/android/helpers/BinderCollectionHelper.java b/libraries/collectors-helper/jank/src/com/android/helpers/BinderCollectionHelper.java
index b1e71dc..5c94b75 100644
--- a/libraries/collectors-helper/jank/src/com/android/helpers/BinderCollectionHelper.java
+++ b/libraries/collectors-helper/jank/src/com/android/helpers/BinderCollectionHelper.java
@@ -169,7 +169,7 @@
                     while ((nextCount = getNextCounter(reader)) > 0) {
                         totalCount += nextCount;
                     }
-                    result.put(currentProcess, totalCount);
+                    result.put("binder_count_" + currentProcess, totalCount);
                 }
             }
         }
diff --git a/libraries/collectors-helper/jank/src/com/android/helpers/SfStatsCollectionHelper.java b/libraries/collectors-helper/jank/src/com/android/helpers/SfStatsCollectionHelper.java
index aaa5d2d..b2c0948 100644
--- a/libraries/collectors-helper/jank/src/com/android/helpers/SfStatsCollectionHelper.java
+++ b/libraries/collectors-helper/jank/src/com/android/helpers/SfStatsCollectionHelper.java
@@ -37,7 +37,8 @@
 
     private static final String LOG_TAG = SfStatsCollectionHelper.class.getSimpleName();
 
-    private static final Pattern KEY_VALUE_PATTERN = Pattern.compile("^(\\w+)\\s+=\\s+(\\S+)");
+    private static final Pattern KEY_VALUE_PATTERN =
+            Pattern.compile("^(\\w+)\\s+=\\s+(\\d+\\.?\\d*|.*).*");
     private static final Pattern HISTOGRAM_PATTERN =
             Pattern.compile("([^\\n]+)\\n((\\d+ms=\\d+\\s+)+)");
 
diff --git a/libraries/collectors-helper/jank/test/src/com/android/helpers/BinderCollectionHelperTest.java b/libraries/collectors-helper/jank/test/src/com/android/helpers/BinderCollectionHelperTest.java
index ca0f1df..8358dec 100644
--- a/libraries/collectors-helper/jank/test/src/com/android/helpers/BinderCollectionHelperTest.java
+++ b/libraries/collectors-helper/jank/test/src/com/android/helpers/BinderCollectionHelperTest.java
@@ -100,7 +100,7 @@
         mHelper.addTrackedProcesses(process);
         Map<String, Integer> result = new HashMap<>();
         mHelper.parseMetrics(getBufferedReader(), result);
-        assertTrue(result.get(process) == 0);
+        assertTrue(result.get("binder_count_" + process) == 0);
     }
 
     @Test
@@ -109,7 +109,7 @@
         mHelper.addTrackedProcesses(process);
         Map<String, Integer> result = new HashMap<>();
         mHelper.parseMetrics(getBufferedReader(), result);
-        assertTrue(result.get(process) == 20);
+        assertTrue(result.get("binder_count_" + process) == 20);
     }
 
     private BufferedReader getBufferedReader() throws Exception {
diff --git a/libraries/collectors-helper/jank/test/src/com/android/helpers/SfStatsCollectionHelperTest.java b/libraries/collectors-helper/jank/test/src/com/android/helpers/SfStatsCollectionHelperTest.java
index 3d851ea..c986a67 100644
--- a/libraries/collectors-helper/jank/test/src/com/android/helpers/SfStatsCollectionHelperTest.java
+++ b/libraries/collectors-helper/jank/test/src/com/android/helpers/SfStatsCollectionHelperTest.java
@@ -85,7 +85,25 @@
                     + "post2present histogram is as below:\n"
                     + "0ms=0 1ms=0 2ms=0 3ms=0 4ms=0 5ms=0 6ms=0 7ms=0 8ms=264 9ms=0\n"
                     + "post2acquire histogram is as below:\n"
-                    + "0ms=0 1ms=0 2ms=0 3ms=0 4ms=0 5ms=0 6ms=0 7ms=0 8ms=264 9ms=0";
+                    + "0ms=0 1ms=0 2ms=0 3ms=0 4ms=0 5ms=0 6ms=0 7ms=0 8ms=264 9ms=0\n"
+                    + "\n"
+                    + "layerName = SurfaceView - com.mxtech.videoplayer.ad/com.mxtech.videoplayer.ad.ActivityScreen#0\n"
+                    + "packageName = \n"
+                    + "totalFrames = 2352\n"
+                    + "droppedFrames = 0\n"
+                    + "averageFPS = 59.999\n"
+                    + "present2present histogram is as below:\n"
+                    + "0ms=0 1ms=0 2ms=0 3ms=0 4ms=0 5ms=0 6ms=0 7ms=0 8ms=2352 9ms=0\n"
+                    + "latch2present histogram is as below:\n"
+                    + "0ms=0 1ms=0 2ms=0 3ms=0 4ms=0 5ms=0 6ms=0 7ms=0 8ms=2352 9ms=0\n"
+                    + "desired2present histogram is as below:\n"
+                    + "0ms=0 1ms=0 2ms=0 3ms=0 4ms=0 5ms=0 6ms=0 7ms=0 8ms=2352 9ms=0\n"
+                    + "acquire2present histogram is as below:\n"
+                    + "0ms=0 1ms=0 2ms=0 3ms=0 4ms=0 5ms=0 6ms=0 7ms=0 8ms=2352 9ms=0\n"
+                    + "post2present histogram is as below:\n"
+                    + "0ms=0 1ms=0 2ms=0 3ms=0 4ms=0 5ms=0 6ms=0 7ms=0 8ms=2352 9ms=0\n"
+                    + "post2acquire histogram is as below:\n"
+                    + "0ms=0 1ms=0 2ms=0 3ms=0 4ms=0 5ms=0 6ms=0 7ms=0 8ms=2352 9ms=0";
 
     private static final String LOG_TAG = SfStatsCollectionHelperTest.class.getSimpleName();
 
@@ -140,6 +158,27 @@
                                 constructKey(
                                         SFSTATS_METRICS_PREFIX,
                                         "GLOBAL",
+                                        "clientCompositionFrames".toUpperCase())))
+                .isEqualTo(Double.valueOf(0));
+        assertThat(
+                        metrics.get(
+                                constructKey(
+                                        SFSTATS_METRICS_PREFIX,
+                                        "GLOBAL",
+                                        "displayOnTime".toUpperCase())))
+                .isEqualTo(Double.valueOf(2485421));
+        assertThat(
+                        metrics.get(
+                                constructKey(
+                                        SFSTATS_METRICS_PREFIX,
+                                        "GLOBAL",
+                                        "totalP2PTime".toUpperCase())))
+                .isEqualTo(Double.valueOf(2674034));
+        assertThat(
+                        metrics.get(
+                                constructKey(
+                                        SFSTATS_METRICS_PREFIX,
+                                        "GLOBAL",
                                         "FRAME_CPU_DURATION_AVG")))
                 .isEqualTo(Double.valueOf(5.5));
         assertThat(
@@ -191,6 +230,27 @@
                                         "com.google.android.nexuslauncher.NexusLauncherActivity#0",
                                         "AVERAGE_FPS")))
                 .isEqualTo(84.318);
+        assertThat(
+                        metrics.get(
+                                constructKey(
+                                        SFSTATS_METRICS_PREFIX,
+                                        "SurfaceView - com.mxtech.videoplayer.ad/com.mxtech.videoplayer.ad.ActivityScreen#0",
+                                        "TOTAL_FRAMES")))
+                .isEqualTo(Double.valueOf(2352));
+        assertThat(
+                        metrics.get(
+                                constructKey(
+                                        SFSTATS_METRICS_PREFIX,
+                                        "SurfaceView - com.mxtech.videoplayer.ad/com.mxtech.videoplayer.ad.ActivityScreen#0",
+                                        "DROPPED_FRAMES")))
+                .isEqualTo(Double.valueOf(0));
+        assertThat(
+                        metrics.get(
+                                constructKey(
+                                        SFSTATS_METRICS_PREFIX,
+                                        "SurfaceView - com.mxtech.videoplayer.ad/com.mxtech.videoplayer.ad.ActivityScreen#0",
+                                        "AVERAGE_FPS")))
+                .isEqualTo(59.999);
         mHelper.stopCollecting();
     }
 
diff --git a/libraries/collectors-helper/memory/src/com/android/helpers/FreeMemHelper.java b/libraries/collectors-helper/memory/src/com/android/helpers/FreeMemHelper.java
index da0ff2c..b2f1f8c 100644
--- a/libraries/collectors-helper/memory/src/com/android/helpers/FreeMemHelper.java
+++ b/libraries/collectors-helper/memory/src/com/android/helpers/FreeMemHelper.java
@@ -50,6 +50,7 @@
     private static final String PROC_MEMINFO = "cat /proc/meminfo";
     private static final String LINE_SEPARATOR = "\\n";
     private static final String MEM_AVAILABLE_PATTERN = "^MemAvailable.*";
+    private static final String MEM_FREE_PATTERN = "^MemFree.*";
     private static final Pattern CACHE_PROC_START_PATTERN = Pattern.compile(".*: Cached$");
     private static final Pattern PID_PATTERN = Pattern.compile("^.*pid(?<processid> [0-9]*).*$");
     private static final String DUMPSYS_PROCESS = "dumpsys meminfo %s";
@@ -57,6 +58,7 @@
     private static final String PROCESS_ID = "processid";
     public static final String MEM_AVAILABLE_CACHE_PROC_DIRTY = "MemAvailable_CacheProcDirty_bytes";
     public static final String PROC_MEMINFO_MEM_AVAILABLE= "proc_meminfo_memavailable_bytes";
+    public static final String PROC_MEMINFO_MEM_FREE= "proc_meminfo_memfree_bytes";
     public static final String DUMPSYS_CACHED_PROC_MEMORY= "dumpsys_cached_procs_memory_bytes";
 
     private UiDevice mUiDevice;
@@ -77,27 +79,35 @@
         String memInfo;
         try {
             memInfo = mUiDevice.executeShellCommand(PROC_MEMINFO);
+            Log.i(TAG, "cat proc/meminfo :" + memInfo);
         } catch (IOException ioe) {
             Log.e(TAG, "Failed to read " + PROC_MEMINFO + ".", ioe);
             return null;
         }
 
         Pattern memAvailablePattern = Pattern.compile(MEM_AVAILABLE_PATTERN, Pattern.MULTILINE);
+        Pattern memFreePattern = Pattern.compile(MEM_FREE_PATTERN, Pattern.MULTILINE);
         Matcher memAvailableMatcher = memAvailablePattern.matcher(memInfo);
+        Matcher memFreeMatcher = memFreePattern.matcher(memInfo);
 
         String[] memAvailable = null;
-        if (memAvailableMatcher.find()) {
+        String[] memFree = null;
+        if (memAvailableMatcher.find() && memFreeMatcher.find()) {
             memAvailable = memAvailableMatcher.group(0).split(SEPARATOR);
+            memFree = memFreeMatcher.group(0).split(SEPARATOR);
         }
 
-        if (memAvailable == null) {
-            Log.e(TAG, "MemAvailable is null.");
+        if (memAvailable == null || memFree == null) {
+            Log.e(TAG, "MemAvailable or MemFree is null.");
             return null;
         }
         Map<String, Long> results = new HashMap<>();
         long memAvailableProc = Long.parseLong(memAvailable[1]);
         results.put(PROC_MEMINFO_MEM_AVAILABLE, (memAvailableProc * 1024));
 
+        long memFreeProc = Long.parseLong(memFree[1]);
+        results.put(PROC_MEMINFO_MEM_FREE, (memFreeProc * 1024));
+
         long cacheProcDirty = memAvailableProc;
         byte[] dumpsysMemInfoBytes = MetricUtility.executeCommandBlocking(DUMPSYS_MEMIFNO,
                 InstrumentationRegistry.getInstrumentation());
@@ -107,7 +117,7 @@
         for (String process : cachedProcList) {
             Log.i(TAG, "Cached Process" + process);
             Matcher match;
-            if (((match = matches(PID_PATTERN, process))) != null) {
+            if ((match = matches(PID_PATTERN, process)) != null) {
                 String processId = match.group(PROCESS_ID);
                 String processDumpSysMemInfo = String.format(DUMPSYS_PROCESS, processId);
                 String processInfoStr;
@@ -173,7 +183,7 @@
                 Log.i(TAG, currLine);
                 Matcher match;
                 if (!isCacheProcSection
-                        && ((match = matches(CACHE_PROC_START_PATTERN, currLine))) == null) {
+                        && (match = matches(CACHE_PROC_START_PATTERN, currLine)) == null) {
                     // Continue untill the start of cache proc section.
                     continue;
                 } else {
diff --git a/libraries/collectors-helper/memory/src/com/android/helpers/RssSnapshotHelper.java b/libraries/collectors-helper/memory/src/com/android/helpers/RssSnapshotHelper.java
deleted file mode 100644
index f73ee53..0000000
--- a/libraries/collectors-helper/memory/src/com/android/helpers/RssSnapshotHelper.java
+++ /dev/null
@@ -1,257 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.helpers;
-
-import static com.android.helpers.MetricUtility.constructKey;
-
-import android.icu.text.NumberFormat;
-import android.support.test.uiautomator.UiDevice;
-import android.util.Log;
-import androidx.test.InstrumentationRegistry;
-import java.io.File;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.text.ParseException;
-import java.util.HashMap;
-import java.util.InputMismatchException;
-import java.util.Map;
-import java.util.Scanner;
-import java.util.UUID;
-
-/**
- * Helper to collect rss snapshot for a list of processes.
- */
-public class RssSnapshotHelper implements ICollectorHelper<String> {
-  private static final String TAG = RssSnapshotHelper.class.getSimpleName();
-
-  private static final String DROP_CACHES_CMD = "echo %d > /proc/sys/vm/drop_caches";
-  private static final String PIDOF_CMD = "pidof %s";
-  private static final String SHOWMAP_CMD = "showmap -v %d";
-
-  public static final String RSS_METRIC_PREFIX = "showmap_rss_bytes";
-  public static final String OUTPUT_FILE_PATH_KEY = "showmap_output_file";
-
-  private String[] mProcessNames = null;
-  private String mTestOutputDir = null;
-  private String mTestOutputFile = null;
-
-  private int mDropCacheOption;
-  private UiDevice mUiDevice;
-
-  // Map to maintain per-process rss.
-  private Map<String, String> mRssMap = new HashMap<>();
-
-  public void setUp(String testOutputDir, String... processNames) {
-    mProcessNames = processNames;
-    mTestOutputDir = testOutputDir;
-    mDropCacheOption = 0;
-    mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
-  }
-
-  @Override
-  public boolean startCollecting() {
-    if (mTestOutputDir == null || mProcessNames == null) {
-      Log.e(TAG, String.format("Invalid test setup"));
-      return false;
-    }
-
-    File directory = new File(mTestOutputDir);
-    String filePath =
-        String.format("%s/rss_snapshot%d.txt", mTestOutputDir, UUID.randomUUID().hashCode());
-    File file = new File(filePath);
-
-    // Make sure directory exists and file does not
-    if (directory.exists()) {
-      if (file.exists() && !file.delete()) {
-        Log.e(TAG, String.format("Failed to delete result output file %s", filePath));
-        return false;
-      }
-    } else {
-      if (!directory.mkdirs()) {
-        Log.e(TAG, String.format("Failed to create result output directory %s", mTestOutputDir));
-        return false;
-      }
-    }
-
-    // Create an empty file to fail early in case there are no write permissions
-    try {
-      if (!file.createNewFile()) {
-        // This should not happen unless someone created the file right after we deleted it
-        Log.e(TAG, String.format("Race with another user of result output file %s", filePath));
-        return false;
-      }
-    } catch (IOException e) {
-      Log.e(TAG, String.format("Failed to create result output file %s", filePath), e);
-      return false;
-    }
-
-    mTestOutputFile = filePath;
-    return true;
-  }
-
-  @Override
-  public Map<String, String> getMetrics() {
-    try {
-      // Drop cache if requested
-      if (mDropCacheOption > 0) {
-        dropCache(mDropCacheOption);
-      }
-
-      if (mProcessNames.length == 0) {
-        // No processes specified, just return
-        return mRssMap;
-      }
-
-      FileWriter writer = new FileWriter(new File(mTestOutputFile), true);
-      for (String processName : mProcessNames) {
-        long pid, rss;
-        String showmapOutput;
-
-        // Collect required data
-        try {
-          pid = getPid(processName);
-          showmapOutput = execShowMap(processName, pid);
-          rss = extractTotalRss(processName, showmapOutput);
-        } catch (RuntimeException e) {
-          Log.e(TAG, e.getMessage(), e.getCause());
-          // Skip this process and continue with the next one
-          continue;
-        }
-
-        // Store showmap output into file
-        storeToFile(mTestOutputFile, processName, pid, showmapOutput, writer);
-
-        // Store metrics
-        mRssMap.put(constructKey(RSS_METRIC_PREFIX, processName), Long.toString(rss * 1024));
-      }
-      writer.close();
-      mRssMap.put(OUTPUT_FILE_PATH_KEY, mTestOutputFile);
-    } catch (RuntimeException e) {
-      Log.e(TAG, e.getMessage(), e.getCause());
-    } catch (IOException e) {
-      Log.e(TAG, String.format("Failed to write output file %s", mTestOutputFile), e);
-    }
-
-    return mRssMap;
-  }
-
-  @Override
-  public boolean stopCollecting() {
-    return true;
-  }
-
-  /**
-   * Set drop cache option.
-   *
-   * @param dropCacheOption drop pagecache (1), slab (2) or all (3) cache
-   * @return true on success, false if input option is invalid
-   */
-  public boolean setDropCacheOption(int dropCacheOption) {
-    // Valid values are 1..3
-    if (dropCacheOption < 1 || dropCacheOption > 3) {
-      return false;
-    }
-
-    mDropCacheOption = dropCacheOption;
-    return true;
-  }
-
-  /**
-   * Drops kernel memory cache.
-   *
-   * @param cacheOption drop pagecache (1), slab (2) or all (3) caches
-   */
-  private void dropCache(int cacheOption) throws RuntimeException {
-    try {
-      mUiDevice.executeShellCommand(String.format(DROP_CACHES_CMD, cacheOption));
-    } catch (IOException e) {
-      throw new RuntimeException("Unable to drop caches", e);
-    }
-  }
-
-  /**
-   * Get pid of the process with {@code processName} name.
-   *
-   * @param processName name of the process to get pid
-   * @return pid of the specified process
-   */
-  private int getPid(String processName) throws RuntimeException {
-    try {
-      // Note that only the first pid returned by "pidof" will be used.
-      String pidofOutput = mUiDevice.executeShellCommand(String.format(PIDOF_CMD, processName));
-      return NumberFormat.getInstance().parse(pidofOutput).intValue();
-    } catch (IOException | ParseException e) {
-      throw new RuntimeException(String.format("Unable to get pid of %s ", processName), e);
-    }
-  }
-
-  /**
-   * Executes showmap command for the process with {@code processName} name and {@code pid} pid.
-   *
-   * @param processName name of the process to run showmap for
-   * @param pid pid of the process to run showmap for
-   * @return the output of showmap command
-   */
-  private String execShowMap(String processName, long pid) throws IOException {
-    try {
-      return mUiDevice.executeShellCommand(String.format(SHOWMAP_CMD, pid));
-    } catch (IOException e) {
-      throw new RuntimeException(
-          String.format("Unable to execute showmap command for %s ", processName), e);
-    }
-  }
-
-  /**
-   * Extract total RSS from showmap command output for the process with {@code processName} name.
-   *
-   * @param processName name of the process to extract RSS for
-   * @param showmapOutput showmap command output
-   * @return total RSS of the process
-   */
-  private long extractTotalRss(String processName, String showmapOutput) throws RuntimeException {
-    try {
-      int pos = showmapOutput.lastIndexOf("----");
-      Scanner sc = new Scanner(showmapOutput.substring(pos));
-      sc.next();
-      sc.nextLong();
-      return sc.nextLong();
-    } catch (IndexOutOfBoundsException | InputMismatchException e) {
-      throw new RuntimeException(
-          String.format("Unexpected showmap format for %s ", processName), e);
-    }
-  }
-
-  /**
-   * Store test results for one process into file.
-   *
-   * @param fileName name of the file being written
-   * @param processName name of the process
-   * @param pid pid of the process
-   * @param showmapOutput showmap command output
-   * @param writer file writer to write the data
-   */
-  private void storeToFile(String fileName, String processName, long pid, String showmapOutput,
-      FileWriter writer) throws RuntimeException {
-    try {
-      writer.write(String.format(">>> %s (%d) <<<\n", processName, pid));
-      writer.write(showmapOutput);
-      writer.write('\n');
-    } catch (IOException e) {
-      throw new RuntimeException(String.format("Unable to write file %s ", fileName), e);
-    }
-  }
-}
diff --git a/libraries/collectors-helper/memory/src/com/android/helpers/ShowmapSnapshotHelper.java b/libraries/collectors-helper/memory/src/com/android/helpers/ShowmapSnapshotHelper.java
new file mode 100644
index 0000000..fba95b0
--- /dev/null
+++ b/libraries/collectors-helper/memory/src/com/android/helpers/ShowmapSnapshotHelper.java
@@ -0,0 +1,358 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.helpers;
+
+import static com.android.helpers.MetricUtility.constructKey;
+
+import android.support.test.uiautomator.UiDevice;
+import android.util.Log;
+import androidx.test.InstrumentationRegistry;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.InputMismatchException;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+
+/**
+ * Helper to collect memory information for a list of processes from showmap.
+ */
+public class ShowmapSnapshotHelper implements ICollectorHelper<String> {
+  private static final String TAG = ShowmapSnapshotHelper.class.getSimpleName();
+
+  private static final String DROP_CACHES_CMD = "echo %d > /proc/sys/vm/drop_caches";
+  private static final String PIDOF_CMD = "pidof %s";
+  public static final String ALL_PROCESSES_CMD = "ps -A";
+  private static final String SHOWMAP_CMD = "showmap -v %d";
+
+  public static final String OUTPUT_METRIC_PATTERN = "showmap_%s_bytes";
+  public static final String OUTPUT_FILE_PATH_KEY = "showmap_output_file";
+  public static final String PROCESS_COUNT = "process_count";
+
+  private String[] mProcessNames = null;
+  private String mTestOutputDir = null;
+  private String mTestOutputFile = null;
+
+  private int mDropCacheOption;
+  private boolean mCollectForAllProcesses = false;
+  private UiDevice mUiDevice;
+
+  // Map to maintain per-process memory info
+  private Map<String, String> mMemoryMap = new HashMap<>();
+  
+  // Maintain metric name and the index it corresponds to in the showmap output
+  // summary
+  private Map<Integer, String> mMetricNameIndexMap = new HashMap<>();
+
+  public void setUp(String testOutputDir, String... processNames) {
+    mProcessNames = processNames;
+    mTestOutputDir = testOutputDir;
+    mDropCacheOption = 0;
+    mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+  }
+
+  @Override
+  public boolean startCollecting() {
+    if (mTestOutputDir == null) {
+      Log.e(TAG, String.format("Invalid test setup"));
+      return false;
+    }
+
+    File directory = new File(mTestOutputDir);
+    String filePath =
+        String.format("%s/showmap_snapshot%d.txt", mTestOutputDir, UUID.randomUUID().hashCode());
+    File file = new File(filePath);
+
+    // Make sure directory exists and file does not
+    if (directory.exists()) {
+      if (file.exists() && !file.delete()) {
+        Log.e(TAG, String.format("Failed to delete result output file %s", filePath));
+        return false;
+      }
+    } else {
+      if (!directory.mkdirs()) {
+        Log.e(TAG, String.format("Failed to create result output directory %s", mTestOutputDir));
+        return false;
+      }
+    }
+
+    // Create an empty file to fail early in case there are no write permissions
+    try {
+      if (!file.createNewFile()) {
+        // This should not happen unless someone created the file right after we deleted it
+        Log.e(TAG, String.format("Race with another user of result output file %s", filePath));
+        return false;
+      }
+    } catch (IOException e) {
+      Log.e(TAG, String.format("Failed to create result output file %s", filePath), e);
+      return false;
+    }
+
+    mTestOutputFile = filePath;
+    return true;
+  }
+
+  @Override
+  public Map<String, String> getMetrics() {
+    try {
+      // Drop cache if requested
+      if (mDropCacheOption > 0) {
+        dropCache(mDropCacheOption);
+      }
+
+      if (mCollectForAllProcesses) {
+         Log.i(TAG, "Collecting memory metrics for all processes.");
+         mProcessNames = getAllProcessNames();
+      } else if (mProcessNames.length > 0) {
+         Log.i(TAG, "Collecting memory only for given list of process");
+      } else if (mProcessNames.length == 0) {
+        // No processes specified, just return empty map
+        return mMemoryMap;
+      }
+
+      FileWriter writer = new FileWriter(new File(mTestOutputFile), true);
+      for (String processName : mProcessNames) {
+        List<Integer> pids = new ArrayList<>();
+
+        // Collect required data
+        try {
+          pids = getPids(processName);
+          for (Integer pid: pids) {
+            String showmapOutput = execShowMap(processName, pid);
+            parseAndUpdateMemoryInfo(processName, showmapOutput);
+            // Store showmap output into file. If there are more than one process
+            // with same name write the individual showmap associated with pid.
+            storeToFile(mTestOutputFile, processName, pid, showmapOutput, writer);
+          }
+        } catch (RuntimeException e) {
+          Log.e(TAG, e.getMessage(), e.getCause());
+          // Skip this process and continue with the next one
+          continue;
+        }   
+      }
+      // Store the unique process count. -1 to exclude the "ps" process name.
+      mMemoryMap.put(PROCESS_COUNT, Integer.toString(mProcessNames.length - 1));
+      writer.close();
+      mMemoryMap.put(OUTPUT_FILE_PATH_KEY, mTestOutputFile);
+    } catch (RuntimeException e) {
+      Log.e(TAG, e.getMessage(), e.getCause());
+    } catch (IOException e) {
+      Log.e(TAG, String.format("Failed to write output file %s", mTestOutputFile), e);
+    }
+
+    return mMemoryMap;
+  }
+
+  @Override
+  public boolean stopCollecting() {
+    return true;
+  }
+
+  /**
+   * Set drop cache option.
+   *
+   * @param dropCacheOption drop pagecache (1), slab (2) or all (3) cache
+   * @return true on success, false if input option is invalid
+   */
+  public boolean setDropCacheOption(int dropCacheOption) {
+    // Valid values are 1..3
+    if (dropCacheOption < 1 || dropCacheOption > 3) {
+      return false;
+    }
+
+    mDropCacheOption = dropCacheOption;
+    return true;
+  }
+
+  /**
+   * Drops kernel memory cache.
+   *
+   * @param cacheOption drop pagecache (1), slab (2) or all (3) caches
+   */
+  private void dropCache(int cacheOption) throws RuntimeException {
+    try {
+      mUiDevice.executeShellCommand(String.format(DROP_CACHES_CMD, cacheOption));
+    } catch (IOException e) {
+      throw new RuntimeException("Unable to drop caches", e);
+    }
+  }
+
+  /**
+   * Get pid's of the process with {@code processName} name.
+   *
+   * @param processName name of the process to get pid
+   * @return pid's of the specified process
+   */
+  private List<Integer> getPids(String processName) throws RuntimeException {
+    try {
+      String pidofOutput = mUiDevice.executeShellCommand(String.format(PIDOF_CMD, processName));
+
+      // Sample output for the process with more than 1 pid.
+      // Sample command : "pidof init"
+      // Sample output : 1 559
+      String[] pids = pidofOutput.split("\\s+");
+      List<Integer> pidList = new ArrayList<>();
+      for (String pid: pids) {
+          pidList.add(Integer.parseInt(pid.trim()));
+      }
+      return pidList;
+    } catch (IOException e) {
+      throw new RuntimeException(String.format("Unable to get pid of %s ", processName), e);
+    }
+  }
+
+  /**
+   * Executes showmap command for the process with {@code processName} name and {@code pid} pid.
+   *
+   * @param processName name of the process to run showmap for
+   * @param pid pid of the process to run showmap for
+   * @return the output of showmap command
+   */
+  private String execShowMap(String processName, long pid) throws IOException {
+    try {
+      return mUiDevice.executeShellCommand(String.format(SHOWMAP_CMD, pid));
+    } catch (IOException e) {
+      throw new RuntimeException(
+          String.format("Unable to execute showmap command for %s ", processName), e);
+    }
+  }
+
+  /**
+   * Extract memory metrics from showmap command output for the process with {@code processName}
+   * name.
+   *
+   * @param processName name of the process to extract memory info for
+   * @param showmapOutput showmap command output
+   */
+  private void parseAndUpdateMemoryInfo(String processName, String showmapOutput)
+          throws RuntimeException {
+    try {
+      
+      // -------- -------- -------- -------- -------- -------- -------- -------- ----- ------ ----
+      // virtual                     shared   shared  private  private
+      //  size      RSS      PSS    clean    dirty    clean    dirty     swap  swapPSS flags object
+      // ------- -------- -------- -------- -------- -------- -------- -------- ------ -----  ----
+      //10810272     5400     1585     3800      168      264     1168        0        0      TOTAL
+      
+      int pos = showmapOutput.lastIndexOf("----");
+      String summarySplit[] = showmapOutput.substring(pos).trim().split("\\s+");
+
+      for (Map.Entry<Integer, String> entry : mMetricNameIndexMap.entrySet()) {
+          String metricKey = constructKey(String.format(OUTPUT_METRIC_PATTERN, entry.getValue()),
+                  processName);
+          // If there are multiple pids associated with the process name then update the
+          // existing entry in the map otherwise add new entry in the map.
+          if(mMemoryMap.containsKey(metricKey)) {
+              long currValue = Long.parseLong(mMemoryMap.get(metricKey));
+              mMemoryMap.put(metricKey, Long.toString(currValue +
+                      (Long.parseLong(summarySplit[entry.getKey() + 1]) * 1024)));
+          } else {
+              mMemoryMap.put(metricKey, Long.toString(Long.parseLong(
+                      summarySplit[entry.getKey() + 1]) * 1024));
+          }   
+      } 
+    } catch (IndexOutOfBoundsException | InputMismatchException e) {
+      throw new RuntimeException(
+          String.format("Unexpected showmap format for %s ", processName), e);
+    }
+  }
+
+  /**
+   * Store test results for one process into file.
+   *
+   * @param fileName name of the file being written
+   * @param processName name of the process
+   * @param pid pid of the process
+   * @param showmapOutput showmap command output
+   * @param writer file writer to write the data
+   */
+  private void storeToFile(String fileName, String processName, long pid, String showmapOutput,
+      FileWriter writer) throws RuntimeException {
+    try {
+      writer.write(String.format(">>> %s (%d) <<<\n", processName, pid));
+      writer.write(showmapOutput);
+      writer.write('\n');
+    } catch (IOException e) {
+      throw new RuntimeException(String.format("Unable to write file %s ", fileName), e);
+    }
+  }
+
+  /**
+   * Set the memory metric name and corresponding index to parse from the showmap output summary.
+   * @param metricNameIndexStr comma separated metric_name:index
+   *
+   * TODO: Pre-process the string into map and pass the map to this method.
+   */
+  public void setMetricNameIndex(String metricNameIndexStr) {
+      Log.i(TAG, String.format("Metric Name index %s", metricNameIndexStr));
+      String metricDetails[] = metricNameIndexStr.split(",");
+      for (String metricDetail : metricDetails) {
+          String metricDetailsSplit[] = metricDetail.split(":");
+          if (metricDetailsSplit.length == 2) {
+              mMetricNameIndexMap.put(Integer.parseInt(
+                      metricDetailsSplit[1]), metricDetailsSplit[0]);
+          }
+      }
+      Log.i(TAG, String.format("Metric Name index map size %s", mMetricNameIndexMap.size()));
+  }
+
+  /**
+   * Enables memory collection for all processes.
+   */
+  public void setAllProcesses() {
+      mCollectForAllProcesses = true;
+  }
+
+  /**
+   * Get all process names running in the system.
+   */
+  private String[] getAllProcessNames() {
+      Set<String> allProcessNames = new LinkedHashSet<>();
+      try {
+          String psOutput = mUiDevice.executeShellCommand(ALL_PROCESSES_CMD);
+          // Split the lines
+          String allProcesses[] = psOutput.split("\\n");
+          for (String invidualProcessDetails : allProcesses) {
+              Log.i(TAG, String.format("Process detail: %s", invidualProcessDetails));
+              // Sample process detail line
+              // system         603     1   41532   5396 SyS_epoll+          0 S servicemanager
+              String processSplit[] = invidualProcessDetails.split("\\s+");
+              // Parse process name
+              String processName = processSplit[processSplit.length - 1].trim();
+              // Include the process name which are not enclosed in [].
+              if (!processName.startsWith("[") && !processName.endsWith("]")) {
+                  // Skip the first (i.e header) line from "ps -A" output.
+                  if (processName.equalsIgnoreCase("NAME")) {
+                      continue;
+                  }
+                  Log.i(TAG, String.format("Including the process %s", processName));
+                  allProcessNames.add(processName);
+              }
+          }
+      } catch (IOException ioe) {
+          throw new RuntimeException(
+                  String.format("Unable execute all processes command %s ", ALL_PROCESSES_CMD),
+                  ioe);
+      }
+      return allProcessNames.toArray(new String[0]);
+  }
+}
diff --git a/libraries/collectors-helper/memory/test/src/com/android/helpers/tests/FreeMemHelperTest.java b/libraries/collectors-helper/memory/test/src/com/android/helpers/tests/FreeMemHelperTest.java
index 283ab36..9770908 100644
--- a/libraries/collectors-helper/memory/test/src/com/android/helpers/tests/FreeMemHelperTest.java
+++ b/libraries/collectors-helper/memory/test/src/com/android/helpers/tests/FreeMemHelperTest.java
@@ -54,6 +54,7 @@
         assertTrue(freeMemMetrics.containsKey(FreeMemHelper.DUMPSYS_CACHED_PROC_MEMORY));
         assertTrue(freeMemMetrics.get(FreeMemHelper.MEM_AVAILABLE_CACHE_PROC_DIRTY) > 0);
         assertTrue(freeMemMetrics.get(FreeMemHelper.PROC_MEMINFO_MEM_AVAILABLE) > 0);
+        assertTrue(freeMemMetrics.get(FreeMemHelper.PROC_MEMINFO_MEM_FREE) > 0);
         assertTrue(freeMemMetrics.get(FreeMemHelper.DUMPSYS_CACHED_PROC_MEMORY) > 0);
     }
 }
diff --git a/libraries/collectors-helper/memory/test/src/com/android/helpers/tests/RssSnapshotHelperTest.java b/libraries/collectors-helper/memory/test/src/com/android/helpers/tests/RssSnapshotHelperTest.java
deleted file mode 100644
index 33e49cc..0000000
--- a/libraries/collectors-helper/memory/test/src/com/android/helpers/tests/RssSnapshotHelperTest.java
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.helpers.tests;
-
-import static com.android.helpers.MetricUtility.constructKey;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-import androidx.test.runner.AndroidJUnit4;
-import com.android.helpers.RssSnapshotHelper;
-import java.util.Map;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Android Unit tests for {@link RssSnapshotHelper}.
- *
- * To run:
- * atest CollectorsHelperTest:com.android.helpers.tests.RssSnapshotHelperTest
- */
-@RunWith(AndroidJUnit4.class)
-public class RssSnapshotHelperTest {
-  private static final String TAG = RssSnapshotHelperTest.class.getSimpleName();
-
-  // Valid output file
-  private static final String VALID_OUTPUT_DIR = "/sdcard/test_results";
-  // Invalid output file (no permissions to write)
-  private static final String INVALID_OUTPUT_DIR = "/data/local/tmp";
-
-  // Lists of process names
-  private static final String[] EMPTY_PROCESS_LIST = {};
-  private static final String[] ONE_PROCESS_LIST = {"com.android.systemui"};
-  private static final String[] TWO_PROCESS_LIST = {"com.android.systemui", "system_server"};
-
-  private RssSnapshotHelper mRssSnapshotHelper;
-
-  @Before
-  public void setUp() {
-    mRssSnapshotHelper = new RssSnapshotHelper();
-  }
-
-  /**
-   * Test start collecting returns false if the helper has not been properly set up.
-   */
-  @Test
-  public void testSetUpNotCalled() {
-    assertFalse(mRssSnapshotHelper.startCollecting());
-  }
-
-  /**
-   * Test invalid options for drop cache flag.
-   */
-  @Test
-  public void testInvalidDropCacheOptions() {
-    assertFalse(mRssSnapshotHelper.setDropCacheOption(-1));
-    assertFalse(mRssSnapshotHelper.setDropCacheOption(0));
-    assertFalse(mRssSnapshotHelper.setDropCacheOption(4));
-  }
-
-  /**
-   * Test invalid options for drop cache flag.
-   */
-  @Test
-  public void testValidDropCacheOptions() {
-    assertTrue(mRssSnapshotHelper.setDropCacheOption(1));
-    assertTrue(mRssSnapshotHelper.setDropCacheOption(2));
-    assertTrue(mRssSnapshotHelper.setDropCacheOption(3));
-  }
-
-  /**
-   * Test no metrics are sampled if process name is empty.
-   */
-  @Test
-  public void testEmptyProcessName() {
-    mRssSnapshotHelper.setUp(VALID_OUTPUT_DIR, EMPTY_PROCESS_LIST);
-    Map<String, String> metrics = mRssSnapshotHelper.getMetrics();
-    assertTrue(metrics.isEmpty());
-  }
-
-  /**
-   * Test sampling on a valid and running process.
-   */
-  @Test
-  public void testValidFile() {
-    mRssSnapshotHelper.setUp(VALID_OUTPUT_DIR, ONE_PROCESS_LIST);
-    assertTrue(mRssSnapshotHelper.startCollecting());
-  }
-
-  /**
-   * Test sampling on using an invalid output file.
-   */
-  @Test
-  public void testInvalidFile() {
-    mRssSnapshotHelper.setUp(INVALID_OUTPUT_DIR, ONE_PROCESS_LIST);
-    assertFalse(mRssSnapshotHelper.startCollecting());
-  }
-
-  /**
-   * Test getting metrics from one process.
-   */
-  @Test
-  public void testGetMetrics_OneProcess() {
-    testProcessList(ONE_PROCESS_LIST);
-  }
-
-  /**
-   * Test getting metrics from multiple processes process.
-   */
-  @Test
-  public void testGetMetrics_MultipleProcesses() {
-    testProcessList(TWO_PROCESS_LIST);
-  }
-
-  private void testProcessList(String... processNames) {
-    mRssSnapshotHelper.setUp(VALID_OUTPUT_DIR, processNames);
-    assertTrue(mRssSnapshotHelper.startCollecting());
-    Map<String, String> metrics = mRssSnapshotHelper.getMetrics();
-    assertFalse(metrics.isEmpty());
-    for (String processName : processNames) {
-      assertTrue(
-          metrics.containsKey(constructKey(RssSnapshotHelper.RSS_METRIC_PREFIX, processName)));
-    }
-    assertTrue(metrics.containsKey(RssSnapshotHelper.OUTPUT_FILE_PATH_KEY));
-  }
-}
diff --git a/libraries/collectors-helper/memory/test/src/com/android/helpers/tests/ShowmapSnapshotHelperTest.java b/libraries/collectors-helper/memory/test/src/com/android/helpers/tests/ShowmapSnapshotHelperTest.java
new file mode 100644
index 0000000..ca1218a
--- /dev/null
+++ b/libraries/collectors-helper/memory/test/src/com/android/helpers/tests/ShowmapSnapshotHelperTest.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.helpers.tests;
+
+import static com.android.helpers.MetricUtility.constructKey;
+import static org.junit.Assert.fail;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.runner.AndroidJUnit4;
+import com.android.helpers.ShowmapSnapshotHelper;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+
+/**
+ * Android Unit tests for {@link ShowmapSnapshotHelper}.
+ *
+ * To run:
+ * atest CollectorsHelperTest:com.android.helpers.tests.ShowmapSnapshotHelperTest
+ */
+@RunWith(AndroidJUnit4.class)
+public class ShowmapSnapshotHelperTest {
+  private static final String TAG = ShowmapSnapshotHelperTest.class.getSimpleName();
+
+  // Valid output file
+  private static final String VALID_OUTPUT_DIR = "/sdcard/test_results";
+  // Invalid output file (no permissions to write)
+  private static final String INVALID_OUTPUT_DIR = "/data/local/tmp";
+  // Valid metric index string.
+  private static final String METRIC_INDEX_STR = "rss:1,pss:2";
+  // Invalid metric index string. Reverse order.
+  private static final String METRIC_INVALID_INDEX_STR = "1:pss";
+  // Empty metric index string.
+  private static final String METRIC_EMPTY_INDEX_STR = "";
+
+  // Lists of process names
+  private static final String[] EMPTY_PROCESS_LIST = {};
+  private static final String[] ONE_PROCESS_LIST = {"com.android.systemui"};
+  private static final String[] TWO_PROCESS_LIST = {"com.android.systemui", "system_server"};
+  private static final String[] NO_PROCESS_LIST = {null};
+
+  private ShowmapSnapshotHelper mShowmapSnapshotHelper;
+
+  @Before
+  public void setUp() {
+    mShowmapSnapshotHelper = new ShowmapSnapshotHelper();
+  }
+
+  /**
+   * Test start collecting returns false if the helper has not been properly set up.
+   */
+  @Test
+  public void testSetUpNotCalled() {
+    assertFalse(mShowmapSnapshotHelper.startCollecting());
+  }
+
+  /**
+   * Test invalid options for drop cache flag.
+   */
+  @Test
+  public void testInvalidDropCacheOptions() {
+    assertFalse(mShowmapSnapshotHelper.setDropCacheOption(-1));
+    assertFalse(mShowmapSnapshotHelper.setDropCacheOption(0));
+    assertFalse(mShowmapSnapshotHelper.setDropCacheOption(4));
+  }
+
+  /**
+   * Test invalid options for drop cache flag.
+   */
+  @Test
+  public void testValidDropCacheOptions() {
+    assertTrue(mShowmapSnapshotHelper.setDropCacheOption(1));
+    assertTrue(mShowmapSnapshotHelper.setDropCacheOption(2));
+    assertTrue(mShowmapSnapshotHelper.setDropCacheOption(3));
+  }
+
+  /**
+   * Test no metrics are sampled if process name is empty.
+   */
+  @Test
+  public void testEmptyProcessName() {
+    mShowmapSnapshotHelper.setUp(VALID_OUTPUT_DIR, EMPTY_PROCESS_LIST);
+    Map<String, String> metrics = mShowmapSnapshotHelper.getMetrics();
+    assertTrue(metrics.isEmpty());
+  }
+
+  /**
+   * Test sampling on a valid and running process.
+   */
+  @Test
+  public void testValidFile() {
+    mShowmapSnapshotHelper.setUp(VALID_OUTPUT_DIR, ONE_PROCESS_LIST);
+    assertTrue(mShowmapSnapshotHelper.startCollecting());
+  }
+
+  /**
+   * Test sampling on using an invalid output file.
+   */
+  @Test
+  public void testInvalidFile() {
+    mShowmapSnapshotHelper.setUp(INVALID_OUTPUT_DIR, ONE_PROCESS_LIST);
+    assertFalse(mShowmapSnapshotHelper.startCollecting());
+  }
+
+  /**
+   * Test getting metrics from one process.
+   */
+  @Test
+  public void testGetMetrics_OneProcess() {
+    testProcessList(METRIC_INDEX_STR, ONE_PROCESS_LIST);
+  }
+
+  /**
+   * Test getting metrics from multiple processes process.
+   */
+  @Test
+  public void testGetMetrics_MultipleProcesses() {
+    testProcessList(METRIC_INDEX_STR, TWO_PROCESS_LIST);
+  }
+
+  /**
+   * Test all process flag return more than 2 processes metrics atleast.
+   */
+  @Test
+  public void testGetMetrics_AllProcess() {
+    mShowmapSnapshotHelper.setUp(VALID_OUTPUT_DIR, NO_PROCESS_LIST);
+    mShowmapSnapshotHelper.setMetricNameIndex(METRIC_INDEX_STR);
+    mShowmapSnapshotHelper.setAllProcesses();
+    assertTrue(mShowmapSnapshotHelper.startCollecting());
+    Map<String, String> metrics = mShowmapSnapshotHelper.getMetrics();
+    assertTrue(metrics.size() > 2);
+    assertTrue(metrics.containsKey(ShowmapSnapshotHelper.OUTPUT_FILE_PATH_KEY));
+
+  }
+
+  @Test
+  public void testGetMetrics_Invalid_Metric_Pattern() {
+    mShowmapSnapshotHelper.setUp(VALID_OUTPUT_DIR, NO_PROCESS_LIST);
+    try {
+        mShowmapSnapshotHelper.setMetricNameIndex(METRIC_INVALID_INDEX_STR);
+        fail("Should have thrown an exception due to invalid pattern.");
+    } catch (Exception e) {
+        // No-op during the exception
+    }
+
+    mShowmapSnapshotHelper.setAllProcesses();
+    assertTrue(mShowmapSnapshotHelper.startCollecting());
+    Map<String, String> metrics = mShowmapSnapshotHelper.getMetrics();
+    // process count and path to snapshot file in the output by default.
+    assertTrue(metrics.size() == 2);
+  }
+
+  @Test
+  public void testGetMetrics_Empty_Metric_Pattern() {
+    mShowmapSnapshotHelper.setUp(VALID_OUTPUT_DIR, NO_PROCESS_LIST);
+    mShowmapSnapshotHelper.setMetricNameIndex(METRIC_EMPTY_INDEX_STR);
+
+    mShowmapSnapshotHelper.setAllProcesses();
+    assertTrue(mShowmapSnapshotHelper.startCollecting());
+    Map<String, String> metrics = mShowmapSnapshotHelper.getMetrics();
+    // process count and path to snapshot file in the output by default.
+    assertTrue(metrics.size() == 2);
+  }
+
+
+  private void testProcessList(String metricIndexStr, String... processNames) {
+    mShowmapSnapshotHelper.setUp(VALID_OUTPUT_DIR, processNames);
+    mShowmapSnapshotHelper.setMetricNameIndex(metricIndexStr);
+    assertTrue(mShowmapSnapshotHelper.startCollecting());
+    Map<String, String> metrics = mShowmapSnapshotHelper.getMetrics();
+    assertFalse(metrics.isEmpty());
+    for (String processName : processNames) {
+      assertTrue(
+          metrics.containsKey(constructKey(String.format(
+                  ShowmapSnapshotHelper.OUTPUT_METRIC_PATTERN, "rss"), processName)));
+      assertTrue(
+              metrics.containsKey(constructKey(String.format(
+                      ShowmapSnapshotHelper.OUTPUT_METRIC_PATTERN, "pss"), processName)));
+    }
+    assertTrue(metrics.containsKey(ShowmapSnapshotHelper.OUTPUT_FILE_PATH_KEY));
+  }
+}
diff --git a/libraries/collectors-helper/perfetto/src/com/android/helpers/PerfettoHelper.java b/libraries/collectors-helper/perfetto/src/com/android/helpers/PerfettoHelper.java
index abf92e2..c2ddb17 100644
--- a/libraries/collectors-helper/perfetto/src/com/android/helpers/PerfettoHelper.java
+++ b/libraries/collectors-helper/perfetto/src/com/android/helpers/PerfettoHelper.java
@@ -149,7 +149,7 @@
      *
      * @return true if perfetto is stopped successfully.
      */
-    private boolean stopPerfetto() throws IOException {
+    public boolean stopPerfetto() throws IOException {
         String stopOutput = mUIDevice.executeShellCommand(PERFETTO_STOP_CMD);
         Log.i(LOG_TAG, String.format("Perfetto stop command output - %s", stopOutput));
         int waitCount = 0;
diff --git a/libraries/collectors-helper/tests/Android.bp b/libraries/collectors-helper/tests/Android.bp
new file mode 100644
index 0000000..50e844c
--- /dev/null
+++ b/libraries/collectors-helper/tests/Android.bp
@@ -0,0 +1,27 @@
+// Copyright (C) 2020 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.
+
+android_test {
+    name: "CollectorsHelperAospTest",
+    defaults: ["tradefed_errorprone_defaults"],
+
+    static_libs: [
+        "perfetto-helper-test",
+        "jank-helper-test",
+        "memory-helper-test",
+        "system-helper-test",
+    ],
+
+    sdk_version: "current",
+}
diff --git a/libraries/collectors-helper/tests/AndroidManifest.xml b/libraries/collectors-helper/tests/AndroidManifest.xml
new file mode 100644
index 0000000..bfcba7b
--- /dev/null
+++ b/libraries/collectors-helper/tests/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.helpers.tests" >
+    <uses-sdk android:minSdkVersion="24" android:targetSdkVersion="24" />
+    <uses-permission android:name="android.permission.DUMP" />
+    <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+    <uses-permission android:name="android.permission.REAL_GET_TASKS" />
+    <application>
+        <uses-library android:name="android.test.runner"/>
+    </application>
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.helpers.tests"
+        android:label="Collector Helper Tests" />
+</manifest>
diff --git a/libraries/collectors-helper/tests/AndroidTest.xml b/libraries/collectors-helper/tests/AndroidTest.xml
new file mode 100644
index 0000000..3a1e771
--- /dev/null
+++ b/libraries/collectors-helper/tests/AndroidTest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 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.
+-->
+<configuration description="Configuration for AOSP platform collectors helper tests.">
+    <option name="test-suite-tag" value="collectors_helper_aosp" />
+    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="CollectorsHelperAospTest.apk" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.helpers.tests" />
+    </test>
+</configuration>
+
diff --git a/libraries/device-collectors/src/main/java/android/device/collectors/BaseMetricListener.java b/libraries/device-collectors/src/main/java/android/device/collectors/BaseMetricListener.java
index cac6157..eb83858 100644
--- a/libraries/device-collectors/src/main/java/android/device/collectors/BaseMetricListener.java
+++ b/libraries/device-collectors/src/main/java/android/device/collectors/BaseMetricListener.java
@@ -70,6 +70,9 @@
     // Default collect iteration interval.
     private static final int DEFAULT_COLLECT_INTERVAL = 1;
 
+    // Default skip metric until iteration count.
+    private static final int SKIP_UNTIL_DEFAULT_ITERATION = 0;
+
     /** Options keys that the collector can receive. */
     // Filter groups, comma separated list of group name to be included or excluded
     public static final String INCLUDE_FILTER_GROUP_KEY = "include-filter-group";
@@ -79,6 +82,11 @@
     // Collect metric every nth iteration of a test with the same name.
     public static final String COLLECT_ITERATION_INTERVAL = "collect_iteration_interval";
 
+    // Skip metric collection until given n iteration. Uses 1 indexing here.
+    // For example if overall iteration is 10 and skip until iteration is set
+    // to 3. Metric will not be collected for 1st,2nd and 3rd iteration.
+    public static final String SKIP_METRIC_UNTIL_ITERATION = "skip_metric_until_iteration";
+
     private static final String NAMESPACE_SEPARATOR = ":";
 
     private DataRecord mRunData;
@@ -91,6 +99,7 @@
     // Store the method name and invocation count.
     private Map<String, Integer> mTestIdInvocationCount = new HashMap<>();
     private int mCollectIterationInterval = 1;
+    private int mSkipMetricUntilIteration = 0;
 
     public BaseMetricListener() {
         mIncludeFilters = new ArrayList<>();
@@ -137,8 +146,12 @@
 
     @Override
     public final void testStarted(Description description) throws Exception {
+
+        // Update the current invocation before proceeding with metric collection.
+        // mTestIdInvocationCount uses 1 indexing.
         mTestIdInvocationCount.compute(description.toString(),
                 (key, value) -> (value == null) ? 1 : value + 1);
+
         if (shouldRun(description)) {
             try {
                 mTestData = createDataRecord();
@@ -345,6 +358,9 @@
         }
         mCollectIterationInterval = Integer.parseInt(args.getString(
                 COLLECT_ITERATION_INTERVAL, String.valueOf(DEFAULT_COLLECT_INTERVAL)));
+        mSkipMetricUntilIteration = Integer.parseInt(args.getString(
+                SKIP_METRIC_UNTIL_ITERATION, String.valueOf(SKIP_UNTIL_DEFAULT_ITERATION)));
+
         if (mCollectIterationInterval < 1) {
             Log.i(getTag(), "Metric collection iteration interval cannot be less than 1."
                     + "Switching to collect for all the iterations.");
@@ -403,6 +419,7 @@
         if (mLogOnly) {
             return false;
         }
+
         MetricOption annotation = desc.getAnnotation(MetricOption.class);
         List<String> groups = new ArrayList<>();
         if (annotation != null) {
@@ -428,10 +445,21 @@
             return false;
         }
 
+        // Skip metric collection if current iteration is lesser than or equal to
+        // given skip until iteration count.
+        // mTestIdInvocationCount uses 1 indexing.
+        if (mTestIdInvocationCount.containsKey(desc.toString())
+                && mTestIdInvocationCount.get(desc.toString()) <= mSkipMetricUntilIteration) {
+            Log.i(getTag(), String.format("Skipping metric collection. Current iteration is %d."
+                    + "Requested to skip metric until %d",
+                    mTestIdInvocationCount.get(desc.toString()),
+                    mSkipMetricUntilIteration));
+            return false;
+        }
+
         // Check for iteration interval metric collection criteria.
-        if ((mTestIdInvocationCount.containsKey(desc.toString()))
-                && (mTestIdInvocationCount.get(desc.toString())
-                        % mCollectIterationInterval != 0)) {
+        if (mTestIdInvocationCount.containsKey(desc.toString())
+                && (mTestIdInvocationCount.get(desc.toString()) % mCollectIterationInterval != 0)) {
             return false;
         }
         return true;
diff --git a/libraries/device-collectors/src/main/java/android/device/collectors/PerfettoListener.java b/libraries/device-collectors/src/main/java/android/device/collectors/PerfettoListener.java
index 9ba0e8b..6f5ee06 100644
--- a/libraries/device-collectors/src/main/java/android/device/collectors/PerfettoListener.java
+++ b/libraries/device-collectors/src/main/java/android/device/collectors/PerfettoListener.java
@@ -23,6 +23,8 @@
 import android.util.Log;
 import androidx.annotation.VisibleForTesting;
 import com.android.helpers.PerfettoHelper;
+
+import java.io.IOException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.HashMap;
@@ -31,6 +33,7 @@
 import java.util.function.Supplier;
 import org.junit.runner.Description;
 import org.junit.runner.Result;
+import org.junit.runner.notification.Failure;
 
 /**
  * A {@link PerfettoListener} that captures the perfetto trace during each test method
@@ -61,6 +64,8 @@
     // Collect per run if it is set to true otherwise collect per test.
     public static final String COLLECT_PER_RUN = "per_run";
     public static final String PERFETTO_PREFIX = "perfetto_";
+    // Skip failure metrics collection if this flag is set to true.
+    public static final String SKIP_TEST_FAILURE_METRICS = "skip_test_failure_metrics";
 
     private final WakeLockContext mWakeLockContext;
     private final Supplier<WakeLock> mWakelockSupplier;
@@ -79,6 +84,8 @@
     private boolean mPerfettoStartSuccess = false;
     private boolean mIsConfigTextProto = false;
     private boolean mIsCollectPerRun;
+    private boolean mSkipTestFailureMetrics;
+    private boolean mIsTestFailed = false;
 
     private PerfettoHelper mPerfettoHelper = new PerfettoHelper();
 
@@ -150,6 +157,9 @@
         // Defaulted to /sdcard/test_results if test_output_root is not passed.
         mTestOutputRoot = args.getString(TEST_OUTPUT_ROOT, DEFAULT_OUTPUT_ROOT);
 
+        // By default this flag is set to false to collect the metrics on test failure.
+        mSkipTestFailureMetrics = "true".equals(args.getString(SKIP_TEST_FAILURE_METRICS));
+
         if (!mIsCollectPerRun) {
             return;
         }
@@ -170,6 +180,7 @@
 
     @Override
     public void onTestStart(DataRecord testData, Description description) {
+        mIsTestFailed = false;
         if (mIsCollectPerRun) {
             return;
         }
@@ -192,6 +203,11 @@
     }
 
     @Override
+    public void onTestFail(DataRecord testData, Description description, Failure failure) {
+        mIsTestFailed = true;
+    }
+
+    @Override
     public void onTestEnd(DataRecord testData, Description description) {
         if (mIsCollectPerRun) {
             return;
@@ -205,30 +221,42 @@
             return;
         }
 
-        Runnable task =
-                () -> {
-                    Log.i(getTag(), "Stopping perfetto after test ended.");
-                    // Construct test output directory in the below format
-                    // <root_folder>/<test_display_name>/PerfettoListener/<test_display_name>-<count>.pb
-                    Path path =
-                            Paths.get(
-                                    mTestOutputRoot,
-                                    getTestFileName(description),
-                                    this.getClass().getSimpleName(),
-                                    String.format(
-                                            "%s%s-%d.pb",
-                                            PERFETTO_PREFIX,
-                                            getTestFileName(description),
-                                            mTestIdInvocationCount.get(
-                                                    getTestFileName(description))));
-                    stopPerfettoTracing(path, testData);
-                };
-
-        if (mHoldWakelockWhileCollecting) {
-            Log.d(getTag(), "Holding a wakelock at onTestEnd.");
-            mWakeLockContext.run(task);
+        Runnable task = null;
+        if (mSkipTestFailureMetrics && mIsTestFailed) {
+            Log.i(getTag(), "Skipping the metric collection due to test failure.");
+            // Stop the existing perfetto trace collection.
+            try {
+                if (!mPerfettoHelper.stopPerfetto()) {
+                    Log.e(getTag(), "Failed to stop the perfetto process.");
+                }
+            } catch (IOException e) {
+                Log.e(getTag(), "Failed to stop the perfetto.", e);
+            }
         } else {
-            task.run();
+            task =
+                    () -> {
+                        Log.i(getTag(), "Stopping perfetto after test ended.");
+                        // Construct test output directory in the below format
+                        // <root_folder>/<test_name>/PerfettoListener/<test_name>-<count>.pb
+                        Path path =
+                                Paths.get(
+                                        mTestOutputRoot,
+                                        getTestFileName(description),
+                                        this.getClass().getSimpleName(),
+                                        String.format(
+                                                "%s%s-%d.pb",
+                                                PERFETTO_PREFIX,
+                                                getTestFileName(description),
+                                                mTestIdInvocationCount.get(
+                                                        getTestFileName(description))));
+                        stopPerfettoTracing(path, testData);
+                    };
+            if (mHoldWakelockWhileCollecting) {
+                Log.d(getTag(), "Holding a wakelock at onTestEnd.");
+                mWakeLockContext.run(task);
+            } else {
+                task.run();
+            }
         }
     }
 
diff --git a/libraries/device-collectors/src/main/java/android/device/collectors/RssSnapshotListener.java b/libraries/device-collectors/src/main/java/android/device/collectors/RssSnapshotListener.java
deleted file mode 100644
index 976fc3d..0000000
--- a/libraries/device-collectors/src/main/java/android/device/collectors/RssSnapshotListener.java
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * Copyright (C) 2019 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.device.collectors;
-
-import android.device.collectors.annotations.OptionClass;
-import android.os.Bundle;
-import android.util.Log;
-import androidx.annotation.VisibleForTesting;
-import com.android.helpers.RssSnapshotHelper;
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * A {@link RssSnapshotListener} that takes a snapshot of Rss sizes for the list of
- * specified processes.
- *
- * Options:
- * -e process-names [processNames] : a comma-separated list of processes
- * -e drop-cache [pagecache | slab | all] : drop cache flag
- * -e test-output-dir [path] : path to the output directory
- */
-@OptionClass(alias = "rsssnapshot-collector")
-public class RssSnapshotListener extends BaseCollectionListener<String> {
-  private static final String TAG = RssSnapshotListener.class.getSimpleName();
-  private static final String DEFAULT_OUTPUT_DIR = "/sdcard/test_results";
-
-  @VisibleForTesting static final String PROCESS_SEPARATOR = ",";
-  @VisibleForTesting static final String PROCESS_NAMES_KEY = "process-names";
-  @VisibleForTesting static final String DROP_CACHE_KEY = "drop-cache";
-  @VisibleForTesting static final String OUTPUT_DIR_KEY = "test-output-dir";
-
-  private RssSnapshotHelper mRssSnapshotHelper = new RssSnapshotHelper();
-  private final Map<String, Integer> dropCacheValues = new HashMap<String, Integer>() {
-    {
-      put("pagecache", 1);
-      put("slab", 2);
-      put("all", 3);
-    }
-  };
-
-  public RssSnapshotListener() {
-    createHelperInstance(mRssSnapshotHelper);
-  }
-
-  /**
-   * Constructor to simulate receiving the instrumentation arguments. Should not be used except
-   * for testing.
-   */
-  @VisibleForTesting
-  public RssSnapshotListener(Bundle args, RssSnapshotHelper helper) {
-    super(args, helper);
-    mRssSnapshotHelper = helper;
-    createHelperInstance(mRssSnapshotHelper);
-  }
-
-  /**
-   * Adds the options for rss snapshot collector.
-   */
-  @Override
-  public void setupAdditionalArgs() {
-    Bundle args = getArgsBundle();
-    String testOutputDir = args.getString(OUTPUT_DIR_KEY, DEFAULT_OUTPUT_DIR);
-    String procsString = args.getString(PROCESS_NAMES_KEY);
-    if (procsString == null) {
-      Log.e(TAG, "No processes provided to sample");
-      return;
-    }
-    String[] procs = procsString.split(PROCESS_SEPARATOR);
-
-    mRssSnapshotHelper.setUp(testOutputDir, procs);
-
-    String dropCacheValue = args.getString(DROP_CACHE_KEY);
-    if (dropCacheValue != null) {
-      if (dropCacheValues.containsKey(dropCacheValue)) {
-        mRssSnapshotHelper.setDropCacheOption(dropCacheValues.get(dropCacheValue));
-      } else {
-        Log.e(TAG, "Value for \"" + DROP_CACHE_KEY + "\" parameter is invalid");
-        return;
-      }
-    }
-  }
-}
diff --git a/libraries/device-collectors/src/main/java/android/device/collectors/ScheduledRunCollectionListener.java b/libraries/device-collectors/src/main/java/android/device/collectors/ScheduledRunCollectionListener.java
new file mode 100644
index 0000000..ce319c3
--- /dev/null
+++ b/libraries/device-collectors/src/main/java/android/device/collectors/ScheduledRunCollectionListener.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2019 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.device.collectors;
+
+import android.device.collectors.util.SendToInstrumentation;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.util.Log;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.helpers.ICollectorHelper;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import org.junit.runner.Description;
+import org.junit.runner.Result;
+
+/**
+ * Extend this class for a periodic metric collection which relies on ICollectorHelper to collect
+ * metrics and dump the time-series in csv format. In case of system crashes, the time series up to
+ * the point where the crash happened will still be stored.
+ *
+ * In case of running tests with Tradefed file pulller, use the option
+ * {@link file-puller-log-collector:directory-keys} from {{@link FilePullerLogCollector} to
+ * specify the directory path under which the output file should be pulled from (i.e.
+ * <external_storage>/test_results, where <external_storage> is /sdcard for Android phones and
+ * /storage/emulated/10 for Android Auto), instead of using
+ * {@link file-puller-log-collector:pull-pattern-keys}.
+ */
+public class ScheduledRunCollectionListener<T extends Number> extends ScheduledRunMetricListener {
+    private static final String LOG_TAG = ScheduledRunCollectionListener.class.getSimpleName();
+    private static final String TIME_SERIES_PREFIX = "time_series_";
+    @VisibleForTesting public static final String OUTPUT_ROOT = "test_results";
+    @VisibleForTesting public static final String OUTPUT_FILE_PATH = "%s_time_series_path";
+
+    @VisibleForTesting
+    public static final String TIME_SERIES_HEADER =
+            String.format("%-20s,%-100s,%-20s", "time", "metric_key", "value");
+
+    private static final String TIME_SERIES_BODY = "%-20d,%-100s,%-20s";
+    @VisibleForTesting public static final String MEAN_SUFFIX = "-mean";
+    @VisibleForTesting public static final String MAX_SUFFIX = "-max";
+    @VisibleForTesting public static final String MIN_SUFFIX = "-min";
+
+    protected ICollectorHelper<T> mHelper;
+    private TimeSeriesCsvWriter mTimeSeriesCsvWriter;
+    private TimeSeriesStatistics mTimeSeriesStatistics;
+    private long mStartTime;
+
+    public ScheduledRunCollectionListener() {}
+
+    @VisibleForTesting
+    ScheduledRunCollectionListener(Bundle argsBundle, ICollectorHelper helper) {
+        super(argsBundle);
+        mHelper = helper;
+    }
+
+    /**
+     * Write a time-series in csv format to the given destination under external storage as an
+     * unpivoted table like:
+     *
+     * time  ,metric_key ,value
+     * 0     ,metric1    ,5
+     * 0     ,metric2    ,10
+     * 0     ,metric3    ,15
+     * 1000  ,metric1    ,6
+     * 1000  ,metric2    ,11
+     * 1000  ,metric3    ,16
+     */
+    private class TimeSeriesCsvWriter {
+        private File mDestFile;
+        private boolean mIsHeaderWritten = false;
+
+        private TimeSeriesCsvWriter(Path destination) {
+            // Create parent directory if it doesn't exist.
+            File destDir = createAndEmptyDirectory(destination.getParent().toString());
+            mDestFile = new File(destDir, destination.getFileName().toString());
+        }
+
+        private void write(Map<String, T> dataPoint, long timeStamp) {
+            try (BufferedWriter writer = new BufferedWriter(new FileWriter(mDestFile, true))) {
+                if (!mIsHeaderWritten) {
+                    writer.append(TIME_SERIES_HEADER);
+                    writer.append("\n");
+                    mIsHeaderWritten = true;
+                }
+
+                for (String key : dataPoint.keySet()) {
+                    writer.append(
+                            String.format(TIME_SERIES_BODY, timeStamp, key, dataPoint.get(key)));
+                    writer.append("\n");
+                }
+            } catch (IOException e) {
+                Log.e(
+                        LOG_TAG,
+                        String.format("Fail to output time series due to : %s.", e.getMessage()));
+            }
+        }
+    }
+
+    private class TimeSeriesStatistics {
+        Map<String, T> minMap = new HashMap<>();
+        Map<String, T> maxMap = new HashMap<>();
+        Map<String, Double> sumMap = new HashMap<>();
+        Map<String, Long> countMap = new HashMap<>();
+
+        private void update(Map<String, T> dataPoint) {
+            for (String key : dataPoint.keySet()) {
+                T value = dataPoint.get(key);
+                // Add / replace min.
+                minMap.computeIfPresent(key, (k, v) -> compareAsDouble(value, v) == -1 ? value : v);
+                minMap.computeIfAbsent(key, k -> value);
+                // Add / replace max.
+                maxMap.computeIfPresent(key, (k, v) -> compareAsDouble(value, v) == 1 ? value : v);
+                maxMap.computeIfAbsent(key, k -> value);
+                // Add / update sum.
+                sumMap.put(key, value.doubleValue() + sumMap.getOrDefault(key, 0.));
+                // Add / update count.
+                countMap.put(key, 1 + countMap.getOrDefault(key, 0L));
+            }
+        }
+
+        private Map<String, String> getStatistics() {
+            Map<String, String> res = new HashMap<>();
+            for (String key : minMap.keySet()) {
+                res.put(key + MIN_SUFFIX, minMap.get(key).toString());
+            }
+            for (String key : maxMap.keySet()) {
+                res.put(key + MAX_SUFFIX, maxMap.get(key).toString());
+            }
+            for (String key : sumMap.keySet()) {
+                if (countMap.containsKey(key)) {
+                    double mean = sumMap.get(key) / countMap.get(key);
+                    res.put(key + MEAN_SUFFIX, Double.toString(mean));
+                }
+            }
+            return res;
+        }
+
+        /** Compare to Number objects. Return -1 if the n1 < n2; 0 if n1 == n2; 1 if n1 > n2. */
+        private int compareAsDouble(Number n1, Number n2) {
+            Double d1 = Double.valueOf(n1.doubleValue());
+            Double d2 = Double.valueOf(n2.doubleValue());
+            return d1.compareTo(d2);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    void onStart(DataRecord runData, Description description) {
+        setupAdditionalArgs();
+        Path path =
+                Paths.get(
+                        OUTPUT_ROOT,
+                        getClass().getSimpleName(),
+                        String.format(
+                                "%s%s-%d.csv",
+                                TIME_SERIES_PREFIX,
+                                getClass().getSimpleName(),
+                                UUID.randomUUID().hashCode()));
+        mTimeSeriesCsvWriter = new TimeSeriesCsvWriter(path);
+        mTimeSeriesStatistics = new TimeSeriesStatistics();
+        mStartTime = SystemClock.uptimeMillis();
+        mHelper.startCollecting();
+        // Send to stdout the path where the time-series files will be stored.
+        Bundle filePathBundle = new Bundle();
+        filePathBundle.putString(
+                String.format(OUTPUT_FILE_PATH, getClass().getSimpleName()),
+                mTimeSeriesCsvWriter.mDestFile.toString());
+        SendToInstrumentation.sendBundle(getInstrumentation(), filePathBundle);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    void onEnd(DataRecord runData, Result result) {
+        mHelper.stopCollecting();
+        for (Map.Entry<String, String> entry : mTimeSeriesStatistics.getStatistics().entrySet()) {
+            runData.addStringMetric(entry.getKey(), entry.getValue());
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void collect(DataRecord runData, Description description) throws InterruptedException {
+        long timeStamp = SystemClock.uptimeMillis() - mStartTime;
+        Map<String, T> dataPoint = mHelper.getMetrics();
+        mTimeSeriesCsvWriter.write(dataPoint, timeStamp);
+        mTimeSeriesStatistics.update(dataPoint);
+    }
+
+    /**
+     * To add listener specific extra args implement this method in the sub class and add the
+     * listener specific args.
+     */
+    public void setupAdditionalArgs() {
+        // NO-OP by default
+    }
+
+    protected void createHelperInstance(ICollectorHelper helper) {
+        mHelper = helper;
+    }
+}
+
diff --git a/libraries/device-collectors/src/main/java/android/device/collectors/ScreenRecordCollector.java b/libraries/device-collectors/src/main/java/android/device/collectors/ScreenRecordCollector.java
new file mode 100644
index 0000000..69f7085
--- /dev/null
+++ b/libraries/device-collectors/src/main/java/android/device/collectors/ScreenRecordCollector.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2019 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.device.collectors;
+
+import android.device.collectors.annotations.OptionClass;
+import android.os.SystemClock;
+import android.support.test.uiautomator.UiDevice;
+import android.util.Log;
+import androidx.annotation.VisibleForTesting;
+
+import java.io.IOException;
+import java.io.File;
+import java.nio.file.Paths;
+import java.util.Map;
+import java.util.HashMap;
+
+import org.junit.runner.Description;
+
+/**
+ * A {@link BaseMetricListener} that captures video of the screen.
+ *
+ * <p>This class needs external storage permission. See {@link BaseMetricListener} how to grant
+ * external storage permission, especially at install time.
+ */
+@OptionClass(alias = "screen-record-collector")
+public class ScreenRecordCollector extends BaseMetricListener {
+    @VisibleForTesting static final int MAX_RECORDING_PARTS = 5;
+    private static final long VIDEO_TAIL_BUFFER = 2000;
+
+    static final String OUTPUT_DIR = "run_listeners/videos";
+
+    private UiDevice mDevice;
+    private File mDestDir;
+
+    // Tracks multiple parts to a single recording.
+    private int mParts;
+    // Avoid recording after the test is finished.
+    private boolean mContinue;
+
+    // Tracks the test iterations to ensure that each failure gets unique filenames.
+    // Key: test description; value: number of iterations.
+    private Map<String, Integer> mTestIterations = new HashMap<String, Integer>();
+
+    @Override
+    public void onTestRunStart(DataRecord runData, Description description) {
+        mDestDir = createAndEmptyDirectory(OUTPUT_DIR);
+    }
+
+    @Override
+    public void onTestStart(DataRecord testData, Description description) {
+        if (mDestDir == null) {
+            return;
+        }
+
+        // Track the number of iteration for this test.
+        amendIterations(description);
+        // Start the screen recording operation.
+        mParts = 1;
+        mContinue = true;
+        startScreenRecordThread(description);
+    }
+
+    @Override
+    public void onTestEnd(DataRecord testData, Description description) {
+        // Skip if not directory.
+        if (mDestDir == null) {
+            return;
+        }
+
+        // Add some extra time to the video end.
+        SystemClock.sleep(getTailBuffer());
+        // Ctrl + C all screen record processes.
+        mContinue = false;
+        killScreenRecordProcesses();
+
+        // Add the output files to the data record.
+        for (int i = 1; i < mParts; i++) {
+            File output = getOutputFile(description, i);
+            testData.addFileMetric(String.format("%s_%s", getTag(), output.getName()), output);
+        }
+
+        // TODO(b/144869954): Delete when tests pass.
+    }
+
+    /** Updates the number of iterations performed for a given test {@link Description}. */
+    private void amendIterations(Description description) {
+        String testName = description.getDisplayName();
+        mTestIterations.computeIfPresent(testName, (name, iterations) -> iterations + 1);
+        mTestIterations.computeIfAbsent(testName, name -> 1);
+    }
+
+    private File getOutputFile(Description description, int part) {
+        final String baseName =
+                String.format("%s.%s", description.getClassName(), description.getMethodName());
+        // Omit the iteration number for the first iteration.
+        int iteration = mTestIterations.get(description.getDisplayName());
+        final String fileName =
+                String.format(
+                        "%s-video%s.mp4",
+                        iteration == 1
+                                ? baseName
+                                : String.join("-", baseName, String.valueOf(iteration)),
+                        part == 1 ? "" : part);
+        return Paths.get(mDestDir.getAbsolutePath(), fileName).toFile();
+    }
+
+    /** Spawns a thread to start screen recording that will save to the provided {@code path}. */
+    public void startScreenRecordThread(final Description description) {
+        new Thread("test-screenrecord-thread") {
+            @Override
+            public void run() {
+                try {
+                    for (int i = 0; i < MAX_RECORDING_PARTS && mContinue; i++) {
+                        String output = getOutputFile(description, mParts).getAbsolutePath();
+                        Log.d(getTag(), String.format("Recording screen to %s", output));
+                        // Make sure not to block on this background command so the test runs.
+                        getDevice().executeShellCommand(String.format("screenrecord %s", output));
+                        mParts++;
+                    }
+                } catch (IOException e) {
+                    throw new RuntimeException("Caught exception while screen recording.");
+                }
+            }
+        }.start();
+    }
+
+    /** Kills all screen recording processes that are actively running on the device. */
+    public void killScreenRecordProcesses() {
+        try {
+            // Identify the screenrecord PIDs and send SIGINT 2 (Ctrl + C) to each.
+            String[] pids = getDevice().executeShellCommand("pidof screenrecord").split(" ");
+            for (String pid : pids) {
+                // Avoid empty process ids, because of weird splitting behavior.
+                if (pid.isEmpty()) {
+                    continue;
+                }
+
+                getDevice().executeShellCommand(String.format("kill -2 %s", pid));
+                Log.d(getTag(), String.format("Sent SIGINT 2 to screenrecord process (%s)", pid));
+            }
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to kill screen recording process.");
+        }
+    }
+
+    /** Returns a buffer duration for the end of the video. */
+    @VisibleForTesting
+    public long getTailBuffer() {
+        return VIDEO_TAIL_BUFFER;
+    }
+
+    /** Returns the currently active {@link UiDevice}. */
+    public UiDevice getDevice() {
+        if (mDevice == null) {
+            mDevice = UiDevice.getInstance(getInstrumentation());
+        }
+        return mDevice;
+    }
+}
diff --git a/libraries/device-collectors/src/main/java/android/device/collectors/ShowmapSnapshotListener.java b/libraries/device-collectors/src/main/java/android/device/collectors/ShowmapSnapshotListener.java
new file mode 100644
index 0000000..e2fb8ab
--- /dev/null
+++ b/libraries/device-collectors/src/main/java/android/device/collectors/ShowmapSnapshotListener.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2020 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.device.collectors;
+
+import android.device.collectors.annotations.OptionClass;
+import android.os.Bundle;
+import android.util.Log;
+import androidx.annotation.VisibleForTesting;
+import com.android.helpers.ShowmapSnapshotHelper;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A {@link ShowmapSnapshotListener} that takes a snapshot of memory sizes for the list of
+ * specified processes.
+ *
+ * Options:
+ * -e process-names [processNames] : a comma-separated list of processes
+ * -e drop-cache [pagecache | slab | all] : drop cache flag
+ * -e test-output-dir [path] : path to the output directory
+ * -e metric-index [rss:2,pss:3,privatedirty:7] : memory metric name corresponding
+ *  to index in the showmap output.
+ */
+@OptionClass(alias = "showmapsnapshot-collector")
+public class ShowmapSnapshotListener extends BaseCollectionListener<String> {
+  private static final String TAG = ShowmapSnapshotListener.class.getSimpleName();
+  private static final String DEFAULT_OUTPUT_DIR = "/sdcard/test_results";
+
+  @VisibleForTesting static final String PROCESS_SEPARATOR = ",";
+  @VisibleForTesting static final String PROCESS_NAMES_KEY = "process-names";
+  @VisibleForTesting static final String METRIC_NAME_INDEX = "metric-name-index";
+  @VisibleForTesting static final String DROP_CACHE_KEY = "drop-cache";
+  @VisibleForTesting static final String OUTPUT_DIR_KEY = "test-output-dir";
+
+  private ShowmapSnapshotHelper mShowmapSnapshotHelper = new ShowmapSnapshotHelper();
+  private final Map<String, Integer> dropCacheValues = new HashMap<String, Integer>() {
+    {
+      put("pagecache", 1);
+      put("slab", 2);
+      put("all", 3);
+    }
+  };
+
+  // Sample output
+  // -------- -------- -------- -------- -------- -------- -------- -------- -------- ------ --
+  // virtual                     shared   shared  private  private
+  //  size      RSS      PSS    clean    dirty    clean    dirty     swap  swapPSS flags object
+  // ------- -------- -------- -------- -------- -------- -------- -------- -------- ----- ---
+  // 10810272     5400     1585     3800      168      264     1168        0        0      TOTAL
+
+  // Default to collect rss, pss and private dirty.
+  private String mMemoryMetricNameIndex = "rss:1,pss:2,privatedirty:6";
+
+  public ShowmapSnapshotListener() {
+    createHelperInstance(mShowmapSnapshotHelper);
+  }
+
+  /**
+   * Constructor to simulate receiving the instrumentation arguments. Should not be used except
+   * for testing.
+   */
+  @VisibleForTesting
+  public ShowmapSnapshotListener(Bundle args, ShowmapSnapshotHelper helper) {
+    super(args, helper);
+    mShowmapSnapshotHelper = helper;
+    createHelperInstance(mShowmapSnapshotHelper);
+  }
+
+  /**
+   * Adds the options for showmap snapshot collector.
+   */
+  @Override
+  public void setupAdditionalArgs() {
+    Bundle args = getArgsBundle();
+    String testOutputDir = args.getString(OUTPUT_DIR_KEY, DEFAULT_OUTPUT_DIR);
+    // Collect for all processes if process list is empty or null.
+    String procsString = args.getString(PROCESS_NAMES_KEY);
+
+    // Metric name and corresponding index in the output of showmap summary.
+    String metricNameIndexArg = args.getString(METRIC_NAME_INDEX);
+    if (metricNameIndexArg != null && !metricNameIndexArg.isEmpty()) {
+        mMemoryMetricNameIndex = metricNameIndexArg;
+    }
+    mShowmapSnapshotHelper.setMetricNameIndex(mMemoryMetricNameIndex);
+
+    String[] procs = null;
+    if (procsString == null || procsString.isEmpty()) {
+      mShowmapSnapshotHelper.setAllProcesses();
+    } else {
+      procs = procsString.split(PROCESS_SEPARATOR);
+    }
+
+
+    mShowmapSnapshotHelper.setUp(testOutputDir, procs);
+
+    String dropCacheValue = args.getString(DROP_CACHE_KEY);
+    if (dropCacheValue != null) {
+      if (dropCacheValues.containsKey(dropCacheValue)) {
+        mShowmapSnapshotHelper.setDropCacheOption(dropCacheValues.get(dropCacheValue));
+      } else {
+        Log.e(TAG, "Value for \"" + DROP_CACHE_KEY + "\" parameter is invalid");
+        return;
+      }
+    }
+  }
+}
diff --git a/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/README.md b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/README.md
index acd59ed..a6bc18c 100644
--- a/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/README.md
+++ b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/README.md
@@ -9,11 +9,14 @@
 
 ## Checking in a config
 
-To check in a config, follow these steps:
+To check in config(s) for a new set of metrics, follow these steps:
 
-1. Create a directory under this directory for the new config (e.g. `app-start`).
-2. Put the new config in the subdirectory using the directory name + `.pb` extension.
-3. Write a README file explaining what the config does and put it under the new subdirectory.
+1. Create a directory under this directory for the new metrics (e.g. `app-start`).
+2. Put the new config(s) in the subdirectory using the directory name and optionally with additional
+suffixes if there are multiple configs related to the overarching metrics, with `.pb` extension.
+This ensures that each config has a unique name.
+3. Write a README file explaining what the config(s) in the new subdirectory does and put it under
+the new subdirectory.
 
 # (Internal only) Creating a config
 
diff --git a/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/greenday-power/README.md b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/greenday-power/README.md
new file mode 100644
index 0000000..e8985eb
--- /dev/null
+++ b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/greenday-power/README.md
@@ -0,0 +1,3 @@
+# Greenday Power Configs for Run Level and Test Level Metrics
+
+Configs for the following power metrics : CPUClusterTime, CPUTimePerFreq, CPUTimePerThreadFreq, CPUTimePerUidFreq, MobileBytesTransfer, ProcessCPUTime, RemainingBatteryCapacity, SubsystemSleepState and WifiBytesTransfer.
\ No newline at end of file
diff --git a/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/greenday-power/greenday-power-run-level.pb b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/greenday-power/greenday-power-run-level.pb
new file mode 100644
index 0000000..e2bcee6
--- /dev/null
+++ b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/greenday-power/greenday-power-run-level.pb
Binary files differ
diff --git a/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/greenday-power/greenday-power-test-level.pb b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/greenday-power/greenday-power-test-level.pb
new file mode 100644
index 0000000..54078a0
--- /dev/null
+++ b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/greenday-power/greenday-power-test-level.pb
Binary files differ
diff --git a/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/remaining-battery-capacity/README.md b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/remaining-battery-capacity/README.md
new file mode 100644
index 0000000..558107a
--- /dev/null
+++ b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/remaining-battery-capacity/README.md
@@ -0,0 +1,4 @@
+# Remaining Battery Capacity Configs
+
+These configs are used to collect the remaining battery capacity on the device as defined in the
+RemainingBatteryCapacity (Colomb counter) atom.
diff --git a/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/remaining-battery-capacity/remaining-battery-capacity-run-level.pb b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/remaining-battery-capacity/remaining-battery-capacity-run-level.pb
new file mode 100644
index 0000000..ab1f5c2
--- /dev/null
+++ b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/remaining-battery-capacity/remaining-battery-capacity-run-level.pb
Binary files differ
diff --git a/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/remaining-battery-capacity/remaining-battery-capacity-test-level.pb b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/remaining-battery-capacity/remaining-battery-capacity-test-level.pb
new file mode 100644
index 0000000..55a8f3d
--- /dev/null
+++ b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/remaining-battery-capacity/remaining-battery-capacity-test-level.pb
Binary files differ
diff --git a/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/wifi-bytes-transfer/README.md b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/wifi-bytes-transfer/README.md
new file mode 100644
index 0000000..63c917e
--- /dev/null
+++ b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/wifi-bytes-transfer/README.md
@@ -0,0 +1,4 @@
+# WiFi Bytes Transfer Configs
+
+The configs here collects the WiFi bytes transferred on the start and end of tests or test run,
+respectively.
diff --git a/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/wifi-bytes-transfer/wifi-bytes-transfer-run-level.pb b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/wifi-bytes-transfer/wifi-bytes-transfer-run-level.pb
new file mode 100644
index 0000000..ef522ac
--- /dev/null
+++ b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/wifi-bytes-transfer/wifi-bytes-transfer-run-level.pb
Binary files differ
diff --git a/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/wifi-bytes-transfer/wifi-bytes-transfer-test-level.pb b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/wifi-bytes-transfer/wifi-bytes-transfer-test-level.pb
new file mode 100644
index 0000000..f022f32
--- /dev/null
+++ b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/wifi-bytes-transfer/wifi-bytes-transfer-test-level.pb
Binary files differ
diff --git a/libraries/device-collectors/src/main/platform-collectors/src/android/device/collectors/StatsdListener.java b/libraries/device-collectors/src/main/platform-collectors/src/android/device/collectors/StatsdListener.java
index cd05278..97f4719 100644
--- a/libraries/device-collectors/src/main/platform-collectors/src/android/device/collectors/StatsdListener.java
+++ b/libraries/device-collectors/src/main/platform-collectors/src/android/device/collectors/StatsdListener.java
@@ -21,7 +21,9 @@
 import android.content.res.AssetManager;
 import android.os.Bundle;
 import android.os.Environment;
+import android.os.SystemClock;
 import android.util.Log;
+import android.util.StatsLog;
 import androidx.annotation.VisibleForTesting;
 import androidx.test.InstrumentationRegistry;
 
@@ -43,6 +45,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.UUID;
+import java.util.concurrent.TimeUnit;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
@@ -72,6 +75,12 @@
     // Common prefix for the metric file.
     static final String REPORT_FILENAME_PREFIX = "statsd-";
 
+    // Labels used to signify test events to statsd with the AppBreadcrumbReported atom.
+    static final int RUN_EVENT_LABEL = 7;
+    static final int TEST_EVENT_LABEL = 11;
+    // A short delay after pushing the AppBreadcrumbReported event so that metrics can be dumped.
+    static final long METRIC_PULL_DELAY = TimeUnit.SECONDS.toMillis(1);
+
     // Configs used for the test run and each test, respectively.
     private Map<String, StatsdConfig> mRunLevelConfigs = new HashMap<String, StatsdConfig>();
     private Map<String, StatsdConfig> mTestLevelConfigs = new HashMap<String, StatsdConfig>();
@@ -95,6 +104,10 @@
         mTestLevelConfigs.putAll(getConfigsFromOption(OPTION_CONFIGS_TEST_LEVEL));
 
         mRunLevelConfigIds = registerConfigsWithStatsManager(mRunLevelConfigs);
+
+        if (!logStart(RUN_EVENT_LABEL)) {
+            Log.w(LOG_TAG, "Failed to log a test run start event. Metrics might be incomplete.");
+        }
     }
 
     /**
@@ -104,6 +117,11 @@
      */
     @Override
     public void onTestRunEnd(DataRecord runData, Result result) {
+        if (!logStop(RUN_EVENT_LABEL)) {
+            Log.w(LOG_TAG, "Failed to log a test run end event. Metrics might be incomplete.");
+        }
+        SystemClock.sleep(METRIC_PULL_DELAY);
+
         Map<String, File> configReports =
                 pullReportsAndRemoveConfigs(
                         mRunLevelConfigIds, Paths.get(REPORT_PATH_ROOT, REPORT_PATH_RUN_LEVEL), "");
@@ -118,6 +136,10 @@
         mTestIterations.computeIfPresent(description.getDisplayName(), (name, count) -> count + 1);
         mTestIterations.computeIfAbsent(description.getDisplayName(), name -> 1);
         mTestLevelConfigIds = registerConfigsWithStatsManager(mTestLevelConfigs);
+
+        if (!logStart(TEST_EVENT_LABEL)) {
+            Log.w(LOG_TAG, "Failed to log a test start event. Metrics might be incomplete.");
+        }
     }
 
     /**
@@ -127,6 +149,11 @@
      */
     @Override
     public void onTestEnd(DataRecord testData, Description description) {
+        if (!logStop(TEST_EVENT_LABEL)) {
+            Log.w(LOG_TAG, "Failed to log a test end event. Metrics might be incomplete.");
+        }
+        SystemClock.sleep(METRIC_PULL_DELAY);
+
         Map<String, File> configReports =
                 pullReportsAndRemoveConfigs(
                         mTestLevelConfigIds,
@@ -411,4 +438,24 @@
                                 configName ->
                                         parseConfigFromName(manager, optionName, configName)));
     }
+
+    /**
+     * Log a "start" AppBreadcrumbReported event to statsd. Wraps a static method for testing.
+     *
+     * @hide
+     */
+    @VisibleForTesting
+    protected boolean logStart(int label) {
+        return StatsLog.logStart(label);
+    }
+
+    /**
+     * Log a "stop" AppBreadcrumbReported event to statsd. Wraps a static method for testing.
+     *
+     * @hide
+     */
+    @VisibleForTesting
+    protected boolean logStop(int label) {
+        return StatsLog.logStop(label);
+    }
 }
diff --git a/libraries/device-collectors/src/test/java/android/device/collectors/BaseCollectionListenerTest.java b/libraries/device-collectors/src/test/java/android/device/collectors/BaseCollectionListenerTest.java
index 79f16d0..af6d75b 100644
--- a/libraries/device-collectors/src/test/java/android/device/collectors/BaseCollectionListenerTest.java
+++ b/libraries/device-collectors/src/test/java/android/device/collectors/BaseCollectionListenerTest.java
@@ -150,7 +150,7 @@
 
         mListener.onTestRunStart(mListener.createDataRecord(), FAKE_DESCRIPTION);
         verify(helper, times(0)).startCollecting();
-        mListener.onTestStart(mListener.createDataRecord(), FAKE_TEST_DESCRIPTION);
+        mListener.testStarted(FAKE_TEST_DESCRIPTION);
         verify(helper, times(1)).startCollecting();
         Failure failureDesc = new Failure(FAKE_TEST_DESCRIPTION,
                 new Exception());
@@ -171,7 +171,7 @@
 
         mListener.onTestRunStart(mListener.createDataRecord(), FAKE_DESCRIPTION);
         verify(helper, times(0)).startCollecting();
-        mListener.onTestStart(mListener.createDataRecord(), FAKE_TEST_DESCRIPTION);
+        mListener.testStarted(FAKE_TEST_DESCRIPTION);
         verify(helper, times(1)).startCollecting();
         Failure failureDesc = new Failure(FAKE_TEST_DESCRIPTION,
                 new Exception());
@@ -195,7 +195,7 @@
 
         mListener.onTestRunStart(mListener.createDataRecord(), FAKE_DESCRIPTION);
         verify(helper, times(0)).startCollecting();
-        mListener.onTestStart(mListener.createDataRecord(), FAKE_TEST_DESCRIPTION);
+        mListener.testStarted(FAKE_TEST_DESCRIPTION);
         verify(helper, times(1)).startCollecting();
         Failure failureDesc = new Failure(FAKE_TEST_DESCRIPTION,
                 new Exception());
diff --git a/libraries/device-collectors/src/test/java/android/device/collectors/BaseMetricListenerInstrumentedTest.java b/libraries/device-collectors/src/test/java/android/device/collectors/BaseMetricListenerInstrumentedTest.java
index 11cba99..0931070 100644
--- a/libraries/device-collectors/src/test/java/android/device/collectors/BaseMetricListenerInstrumentedTest.java
+++ b/libraries/device-collectors/src/test/java/android/device/collectors/BaseMetricListenerInstrumentedTest.java
@@ -478,6 +478,66 @@
     }
 
     /**
+     * Metric collection does not happen on the skipped iterations.
+     */
+    @MetricOption(group = "testGroup")
+    @Test
+    public void testSameMethodNameWithSkipIterationOption() throws Exception {
+        Bundle args = new Bundle();
+        args.putString(BaseMetricListener.SKIP_METRIC_UNTIL_ITERATION, "2");
+        mListener = createWithArgs(args);
+        mListener.setInstrumentation(mMockInstrumentation);
+
+        // Skip until iteration is set to 2.
+        // Metric will not be collected for 1st and 2nd iterations.
+        Description runDescription = Description.createSuiteDescription("run");
+        mListener.testRunStarted(runDescription);
+        Description testDescription = Description.createTestDescription("class", "method");
+        mListener.testStarted(testDescription);
+        mListener.testFinished(testDescription);
+        mListener.testStarted(testDescription);
+        mListener.testFinished(testDescription);
+        mListener.testStarted(testDescription);
+        mListener.testFinished(testDescription);
+        mListener.testStarted(testDescription);
+        mListener.testFinished(testDescription);
+        mListener.testStarted(testDescription);
+        mListener.testFinished(testDescription);
+        mListener.testRunFinished(new Result());
+        // AJUR runner is then gonna call instrumentationRunFinished
+        Bundle resultBundle = new Bundle();
+        mListener.instrumentationRunFinished(System.out, resultBundle, new Result());
+
+        // Check instrumentation status inprogress called only 3 times.
+        ArgumentCaptor<Bundle> capture = ArgumentCaptor.forClass(Bundle.class);
+        Mockito.verify(mMockInstrumentation, Mockito.times(3))
+                .sendStatus(Mockito.eq(
+                        SendToInstrumentation.INST_STATUS_IN_PROGRESS), capture.capture());
+
+        List<Bundle> capturedBundle = capture.getAllValues();
+        assertEquals(3, capturedBundle.size());
+        Bundle check = capturedBundle.get(0);
+        assertEquals(TEST_START_VALUE + "method", check.getString(TEST_START_KEY));
+        assertEquals(TEST_END_VALUE + "method", check.getString(TEST_END_KEY));
+        assertEquals(2, check.size());
+
+        Bundle check2 = capturedBundle.get(1);
+        assertEquals(TEST_START_VALUE + "method", check2.getString(TEST_START_KEY));
+        assertEquals(TEST_END_VALUE + "method", check2.getString(TEST_END_KEY));
+        assertEquals(2, check2.size());
+
+        Bundle check3 = capturedBundle.get(2);
+        assertEquals(TEST_START_VALUE + "method", check3.getString(TEST_START_KEY));
+        assertEquals(TEST_END_VALUE + "method", check3.getString(TEST_END_KEY));
+        assertEquals(2, check2.size());
+
+        // Check that final bundle contains run results
+        assertEquals(RUN_START_VALUE, resultBundle.getString(RUN_START_KEY));
+        assertEquals(RUN_END_VALUE, resultBundle.getString(RUN_END_KEY));
+        assertEquals(2, resultBundle.size());
+    }
+
+    /**
      * Metric collection happens on all the iteration if the interval is
      * invalid (i.e less than 1).
      */
diff --git a/libraries/device-collectors/src/test/java/android/device/collectors/PerfettoListenerTest.java b/libraries/device-collectors/src/test/java/android/device/collectors/PerfettoListenerTest.java
index 20ae4d6..859bf01 100644
--- a/libraries/device-collectors/src/test/java/android/device/collectors/PerfettoListenerTest.java
+++ b/libraries/device-collectors/src/test/java/android/device/collectors/PerfettoListenerTest.java
@@ -40,12 +40,14 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.Description;
+import org.junit.runner.notification.Failure;
 import org.junit.runner.Result;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 import org.mockito.Spy;
 
+
 /**
  * Android Unit tests for {@link PerfettoListener}.
  *
@@ -58,6 +60,9 @@
     // A {@code Description} to pass when faking a test run start call.
     private static final Description FAKE_DESCRIPTION = Description.createSuiteDescription("run");
 
+    private static final Description FAKE_TEST_DESCRIPTION = Description
+            .createTestDescription("class", "method");
+
     private Description mRunDesc;
     private Description mTest1Desc;
     private Description mTest2Desc;
@@ -128,6 +133,62 @@
     }
 
     /*
+     * Verify stop collecting called exactly once when the test failed and the
+     * skip test failure mmetrics is enabled.
+     */
+    @Test
+    public void testPerfettoPerTestFailureFlowDefault() throws Exception {
+        Bundle b = new Bundle();
+        b.putString(PerfettoListener.SKIP_TEST_FAILURE_METRICS, "false");
+        mListener = initListener(b);
+
+        doReturn(true).when(mPerfettoHelper).startCollecting(anyString(), anyBoolean());
+        doReturn(true).when(mPerfettoHelper).stopCollecting(anyLong(), anyString());
+        // Test run start behavior
+        mListener.testRunStarted(mRunDesc);
+
+        // Test test start behavior
+        mListener.testStarted(mTest1Desc);
+        verify(mPerfettoHelper, times(1)).startCollecting(anyString(), anyBoolean());
+
+        // Test fail behaviour
+        Failure failureDesc = new Failure(FAKE_TEST_DESCRIPTION,
+                new Exception());
+        mListener.onTestFail(mDataRecord, mTest1Desc, failureDesc);
+        mListener.onTestEnd(mDataRecord, mTest1Desc);
+        verify(mPerfettoHelper, times(1)).stopCollecting(anyLong(), anyString());
+
+    }
+
+    /*
+     * Verify stop perfetto called exactly once when the test failed and the
+     * skip test failure metrics is enabled.
+     */
+    @Test
+    public void testPerfettoPerTestFailureFlowWithSkipMmetrics() throws Exception {
+        Bundle b = new Bundle();
+        b.putString(PerfettoListener.SKIP_TEST_FAILURE_METRICS, "true");
+        mListener = initListener(b);
+
+        doReturn(true).when(mPerfettoHelper).startCollecting(anyString(), anyBoolean());
+        doReturn(true).when(mPerfettoHelper).stopPerfetto();
+        // Test run start behavior
+        mListener.testRunStarted(mRunDesc);
+
+        // Test test start behavior
+        mListener.testStarted(mTest1Desc);
+        verify(mPerfettoHelper, times(1)).startCollecting(anyString(), anyBoolean());
+
+        // Test fail behaviour
+        Failure failureDesc = new Failure(FAKE_TEST_DESCRIPTION,
+                new Exception());
+        mListener.onTestFail(mDataRecord, mTest1Desc, failureDesc);
+        mListener.onTestEnd(mDataRecord, mTest1Desc);
+        verify(mPerfettoHelper, times(1)).stopPerfetto();
+
+    }
+
+    /*
      * Verify perfetto start and stop collection methods called exactly once for test run.
      * and not during each test method.
      */
diff --git a/libraries/device-collectors/src/test/java/android/device/collectors/ScheduledRunCollectionListenerTest.java b/libraries/device-collectors/src/test/java/android/device/collectors/ScheduledRunCollectionListenerTest.java
new file mode 100644
index 0000000..f573dc2
--- /dev/null
+++ b/libraries/device-collectors/src/test/java/android/device/collectors/ScheduledRunCollectionListenerTest.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2019 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.device.collectors;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.Instrumentation;
+import android.device.collectors.util.SendToInstrumentation;
+import android.os.Bundle;
+import android.os.Environment;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.helpers.ICollectorHelper;
+
+import java.io.File;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.Description;
+import org.junit.runner.Result;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Android Unit tests for {@link ScheduledRunCollectionListener}. */
+@RunWith(AndroidJUnit4.class)
+public class ScheduledRunCollectionListenerTest {
+
+    private static final String TEST_METRIC_KEY = "test_metric_key";
+    private static final Integer[] TEST_METRIC_VALUES = {0, 1, 2, 3, 4};
+    private static final long TEST_INTERVAL = 100L;
+    private static final long TEST_DURATION = 450L;
+    // Collects at 0ms, 100ms, 200ms, and so on.
+    private static final long NUMBER_OF_COLLECTIONS = TEST_DURATION / TEST_INTERVAL + 1;
+    private static final String DATA_REGEX =
+            "(?<timestamp>[0-9]+)\\s+," + TEST_METRIC_KEY + "\\s+,(?<value>[0-9])\\s+";
+
+    @Mock private ICollectorHelper mHelper;
+
+    @Mock private Instrumentation mInstrumentation;
+
+    private ScheduledRunCollectionListener mListener;
+
+    private ScheduledRunCollectionListener initListener() {
+        Bundle b = new Bundle();
+        b.putString(ScheduledRunCollectionListener.INTERVAL_ARG_KEY, Long.toString(TEST_INTERVAL));
+        doReturn(true).when(mHelper).startCollecting();
+        Map<String, Integer> first = new HashMap<>();
+        first.put(TEST_METRIC_KEY, TEST_METRIC_VALUES[0]);
+        Map<String, Integer>[] rest =
+                Arrays.stream(TEST_METRIC_VALUES)
+                        .skip(1)
+                        .map(
+                                testMetricValue -> {
+                                    Map<String, Integer> m = new HashMap<>();
+                                    m.put(TEST_METRIC_KEY, testMetricValue);
+                                    return m;
+                                })
+                        .toArray(Map[]::new);
+        // <code>thenReturn</code> call signature requires thenReturn(T value, T... values).
+        when(mHelper.getMetrics()).thenReturn(first, rest);
+        doReturn(true).when(mHelper).stopCollecting();
+        ScheduledRunCollectionListener listener =
+                new ScheduledRunCollectionListener<Integer>(b, mHelper);
+        // Mock getUiAutomation method for the purpose of enabling createAndEmptyDirectory method
+        // from BaseMetricListener.
+        doReturn(InstrumentationRegistry.getInstrumentation().getUiAutomation())
+                .when(mInstrumentation)
+                .getUiAutomation();
+        listener.setInstrumentation(mInstrumentation);
+        return listener;
+    }
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mListener = initListener();
+    }
+
+    @After
+    public void tearDown() {
+        // Remove files/directories that have been created.
+        Path outputFilePath =
+                Paths.get(
+                        Environment.getExternalStorageDirectory().toString(),
+                        ScheduledRunCollectionListener.OUTPUT_ROOT,
+                        ScheduledRunCollectionListener.class.getSimpleName());
+        mListener.executeCommandBlocking("rm -rf " + outputFilePath.toString());
+    }
+
+    @Test
+    public void testCompleteRun() throws Exception {
+        testRun(true);
+    }
+
+    @Test
+    public void testIncompleteRun() throws Exception {
+        testRun(false);
+    }
+
+    @Test
+    public void testInstrumentationResult() throws Exception {
+        Description runDescription = Description.createSuiteDescription("run");
+        mListener.testRunStarted(runDescription);
+
+        Thread.sleep(TEST_DURATION);
+        mListener.testRunFinished(new Result());
+        // AJUR runner is then gonna call instrumentationRunFinished.
+        Bundle result = new Bundle();
+        mListener.instrumentationRunFinished(System.out, result, new Result());
+        int expectedMin = Arrays.stream(TEST_METRIC_VALUES).min(Integer::compare).get();
+        assertEquals(
+                expectedMin,
+                Integer.parseInt(
+                        result.getString(
+                                TEST_METRIC_KEY + ScheduledRunCollectionListener.MIN_SUFFIX)));
+        int expectedMax = Arrays.stream(TEST_METRIC_VALUES).max(Integer::compare).get();
+        assertEquals(
+                expectedMax,
+                Integer.parseInt(
+                        result.getString(
+                                TEST_METRIC_KEY + ScheduledRunCollectionListener.MAX_SUFFIX)));
+        double expectedMean =
+                Arrays.stream(TEST_METRIC_VALUES).mapToDouble(i -> i.doubleValue()).sum()
+                        / NUMBER_OF_COLLECTIONS;
+        assertEquals(
+                expectedMean,
+                Double.parseDouble(
+                        result.getString(
+                                TEST_METRIC_KEY + ScheduledRunCollectionListener.MEAN_SUFFIX)),
+                0.1);
+    }
+
+    private void testRun(boolean isComplete) throws Exception {
+        Description runDescription = Description.createSuiteDescription("run");
+        mListener.testRunStarted(runDescription);
+
+        Thread.sleep(TEST_DURATION);
+        // If incomplete run happens, for example, when a system crash happens half way through the
+        // run, <code>testRunFinished</code> method will be skipped, but the output file should
+        // still be present, and the time-series up to the point when the crash happens should still
+        // be recorded.
+        if (isComplete) {
+            mListener.testRunFinished(new Result());
+        }
+
+        ArgumentCaptor<Bundle> bundle = ArgumentCaptor.forClass(Bundle.class);
+
+        // Verify that the path of the time-series output file has been sent to instrumentation.
+        verify(mInstrumentation, atLeast(1))
+                .sendStatus(eq(SendToInstrumentation.INST_STATUS_IN_PROGRESS), bundle.capture());
+        Bundle pathBundle = bundle.getAllValues().get(0);
+        String pathKey =
+                String.format(
+                        ScheduledRunCollectionListener.OUTPUT_FILE_PATH,
+                        ScheduledRunCollectionListener.class.getSimpleName());
+        String path = pathBundle.getString(pathKey);
+        assertNotNull(path);
+
+        // Check the output file exists.
+        File outputFile = new File(path);
+        assertTrue(outputFile.exists());
+
+        // Check that output file contains some of the periodic run results, sample results are
+        // like:
+        //
+        // time  ,metric_key       ,value
+        // 2     ,test_metric_key  ,0
+        // 102   ,test_metric_key  ,0
+        // 203   ,test_metric_key  ,0
+        // ...
+        List<String> lines = Files.readAllLines(outputFile.toPath(), Charset.defaultCharset());
+        assertEquals(NUMBER_OF_COLLECTIONS, lines.size() - 1);
+        assertEquals(lines.get(0), ScheduledRunCollectionListener.TIME_SERIES_HEADER);
+        for (int i = 1; i != lines.size(); ++i) {
+            Pattern p = Pattern.compile(DATA_REGEX);
+            Matcher m = p.matcher(lines.get(i));
+            assertTrue(m.matches());
+            long timestamp = Long.parseLong(m.group("timestamp"));
+            long delta = TEST_INTERVAL / 2;
+            assertEquals((i - 1) * TEST_INTERVAL, timestamp, delta);
+            Integer value = Integer.valueOf(m.group("value"));
+            assertEquals(TEST_METRIC_VALUES[i - 1], value);
+        }
+
+        // For incomplete run, invoke testRunFinished in the end to prevent resource leak.
+        if (!isComplete) {
+            mListener.testRunFinished(new Result());
+        }
+    }
+}
diff --git a/libraries/device-collectors/src/test/java/android/device/collectors/ScreenRecordCollectorTest.java b/libraries/device-collectors/src/test/java/android/device/collectors/ScreenRecordCollectorTest.java
new file mode 100644
index 0000000..8f45a74
--- /dev/null
+++ b/libraries/device-collectors/src/test/java/android/device/collectors/ScreenRecordCollectorTest.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2019 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.device.collectors;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.AdditionalMatchers.not;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.endsWith;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.matches;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.app.Instrumentation;
+import android.device.collectors.util.SendToInstrumentation;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.test.uiautomator.UiDevice;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.Description;
+import org.junit.runner.notification.Failure;
+import org.junit.runner.Result;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Android Unit tests for {@link ScreenRecordCollector}.
+ *
+ * <p>To run: atest CollectorDeviceLibTest:android.device.collectors.ScreenRecordCollectorTest
+ */
+@RunWith(AndroidJUnit4.class)
+public class ScreenRecordCollectorTest {
+
+    private static final int NUM_TEST_CASE = 10;
+
+    private File mLogDir;
+    private Description mRunDesc;
+    private Description mTestDesc;
+    private ScreenRecordCollector mListener;
+
+    @Mock private Instrumentation mInstrumentation;
+
+    @Mock private UiDevice mDevice;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mLogDir = new File("tmp/");
+        mRunDesc = Description.createSuiteDescription("run");
+        mTestDesc = Description.createTestDescription("run", "test");
+    }
+
+    @After
+    public void tearDown() {
+        if (mLogDir != null) {
+            mLogDir.delete();
+        }
+    }
+
+    private ScreenRecordCollector initListener() throws IOException {
+        ScreenRecordCollector listener = spy(new ScreenRecordCollector());
+        listener.setInstrumentation(mInstrumentation);
+        doReturn(mLogDir).when(listener).createAndEmptyDirectory(anyString());
+        doReturn(0L).when(listener).getTailBuffer();
+        doReturn(mDevice).when(listener).getDevice();
+        doReturn("1234").when(mDevice).executeShellCommand(eq("pidof screenrecord"));
+        doReturn("").when(mDevice).executeShellCommand(not(eq("pidof screenrecord")));
+        return listener;
+    }
+
+    /**
+     * Test that screen recording is properly started and ended for each test over the course of a
+     * test run.
+     */
+    @Test
+    public void testScreenRecord() throws Exception {
+        mListener = initListener();
+
+        // Verify output directories are created on test run start.
+        mListener.testRunStarted(mRunDesc);
+        verify(mListener).createAndEmptyDirectory(ScreenRecordCollector.OUTPUT_DIR);
+
+        // Walk through a number of test cases to simulate behavior.
+        for (int i = 1; i <= NUM_TEST_CASE; i++) {
+            // Verify a thread is started when the test starts.
+            mListener.testStarted(mTestDesc);
+            verify(mListener, times(i)).startScreenRecordThread(any());
+            // Delay verification by 100 ms to ensure the thread was started.
+            SystemClock.sleep(100);
+            // Expect all recordings to be finished because of mocked commands.
+            verify(mDevice, times(i)).executeShellCommand(matches("screenrecord .*video.mp4"));
+            for (int r = 2; r < ScreenRecordCollector.MAX_RECORDING_PARTS; r++) {
+                verify(mDevice, times(i))
+                        .executeShellCommand(
+                                matches(String.format("screenrecord .*video%d.mp4", r)));
+            }
+
+            // Alternate between pass and fail for variety.
+            if (i % 2 == 0) {
+                mListener.testFailure(new Failure(mTestDesc, new RuntimeException("I failed")));
+            }
+
+            // Verify all processes are killed when the test ends.
+            mListener.testFinished(mTestDesc);
+            verify(mListener, times(i)).killScreenRecordProcesses();
+            verify(mDevice, times(i)).executeShellCommand(eq("pidof screenrecord"));
+            verify(mDevice, times(i)).executeShellCommand(matches("kill -2 1234"));
+        }
+
+        // Verify files are reported
+        mListener.testRunFinished(new Result());
+
+        Bundle resultBundle = new Bundle();
+        mListener.instrumentationRunFinished(System.out, resultBundle, new Result());
+
+        ArgumentCaptor<Bundle> capture = ArgumentCaptor.forClass(Bundle.class);
+        Mockito.verify(mInstrumentation, times(NUM_TEST_CASE))
+                .sendStatus(
+                        Mockito.eq(SendToInstrumentation.INST_STATUS_IN_PROGRESS),
+                        capture.capture());
+        List<Bundle> capturedBundle = capture.getAllValues();
+        assertEquals(NUM_TEST_CASE, capturedBundle.size());
+
+        int videoCount = 0;
+        for (Bundle bundle : capturedBundle) {
+            for (String key : bundle.keySet()) {
+                if (key.contains("mp4")) videoCount++;
+            }
+        }
+        assertEquals(NUM_TEST_CASE * ScreenRecordCollector.MAX_RECORDING_PARTS, videoCount);
+    }
+
+    /** Test that screen recording is properly done for multiple tests and labels iterations. */
+    @Test
+    public void testScreenRecord_multipleTests() throws Exception {
+        mListener = initListener();
+
+        // Run through a sequence of `NUM_TEST_CASE` failing tests.
+        mListener.testRunStarted(mRunDesc);
+
+        // Walk through a number of test cases to simulate behavior.
+        for (int i = 1; i <= NUM_TEST_CASE; i++) {
+            mListener.testStarted(mTestDesc);
+            SystemClock.sleep(100);
+            mListener.testFinished(mTestDesc);
+        }
+        mListener.testRunFinished(new Result());
+
+        // Verify that videos are saved with iterations.
+        InOrder videoVerifier = inOrder(mDevice);
+        // The first video should not have an iteration number.
+        videoVerifier
+                .verify(mDevice, times(ScreenRecordCollector.MAX_RECORDING_PARTS))
+                .executeShellCommand(matches("^.*[^1]-video.*.mp4$"));
+        // The subsequent videos should have an iteration number.
+        for (int i = 1; i < NUM_TEST_CASE; i++) {
+            videoVerifier
+                    .verify(mDevice)
+                    .executeShellCommand(endsWith(String.format("%d-video.mp4", i + 1)));
+            // Verify the iteration-specific and part-specific interactions too.
+            for (int p = 2; p <= ScreenRecordCollector.MAX_RECORDING_PARTS; p++) {
+                videoVerifier
+                        .verify(mDevice)
+                        .executeShellCommand(endsWith(String.format("%d-video%d.mp4", i + 1, p)));
+            }
+        }
+    }
+}
diff --git a/libraries/device-collectors/src/test/java/android/device/collectors/RssSnapshotListenerTest.java b/libraries/device-collectors/src/test/java/android/device/collectors/ShowmapSnapshotListenerTest.java
similarity index 61%
rename from libraries/device-collectors/src/test/java/android/device/collectors/RssSnapshotListenerTest.java
rename to libraries/device-collectors/src/test/java/android/device/collectors/ShowmapSnapshotListenerTest.java
index 883a058..ec12f3b 100644
--- a/libraries/device-collectors/src/test/java/android/device/collectors/RssSnapshotListenerTest.java
+++ b/libraries/device-collectors/src/test/java/android/device/collectors/ShowmapSnapshotListenerTest.java
@@ -16,16 +16,17 @@
 
 package android.device.collectors;
 
-import static android.device.collectors.RssSnapshotListener.DROP_CACHE_KEY;
-import static android.device.collectors.RssSnapshotListener.OUTPUT_DIR_KEY;
-import static android.device.collectors.RssSnapshotListener.PROCESS_NAMES_KEY;
-import static android.device.collectors.RssSnapshotListener.PROCESS_SEPARATOR;
+import static android.device.collectors.ShowmapSnapshotListener.DROP_CACHE_KEY;
+import static android.device.collectors.ShowmapSnapshotListener.METRIC_NAME_INDEX;
+import static android.device.collectors.ShowmapSnapshotListener.OUTPUT_DIR_KEY;
+import static android.device.collectors.ShowmapSnapshotListener.PROCESS_NAMES_KEY;
+import static android.device.collectors.ShowmapSnapshotListener.PROCESS_SEPARATOR;
 import static org.mockito.Mockito.verify;
 
 import android.app.Instrumentation;
 import android.os.Bundle;
 import androidx.test.runner.AndroidJUnit4;
-import com.android.helpers.RssSnapshotHelper;
+import com.android.helpers.ShowmapSnapshotHelper;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.Description;
@@ -34,20 +35,21 @@
 import org.mockito.MockitoAnnotations;
 
 /**
- * Android Unit tests for {@link RssSnapshotListener}.
+ * Android Unit tests for {@link ShowmapSnapshotListener}.
  *
  * To run:
- * atest CollectorDeviceLibTest:android.device.collectors.RssSnapshotListenerTest
+ * atest CollectorDeviceLibTest:android.device.collectors.ShowmapSnapshotListenerTest
  */
 @RunWith(AndroidJUnit4.class)
-public class RssSnapshotListenerTest {
+public class ShowmapSnapshotListenerTest {
   @Mock private Instrumentation mInstrumentation;
-  @Mock private RssSnapshotHelper mRssSnapshotHelper;
+  @Mock private ShowmapSnapshotHelper mShowmapSnapshotHelper;
 
-  private RssSnapshotListener mListener;
+  private ShowmapSnapshotListener mListener;
   private Description mRunDesc;
 
   private static final String VALID_OUTPUT_DIR = "/data/local/tmp";
+  private static final String SAMPLE_METRIC_INDEX = "rss:1,pss:2";
 
   @Before
   public void setUp() {
@@ -55,8 +57,8 @@
     mRunDesc = Description.createSuiteDescription("run");
   }
 
-  private RssSnapshotListener initListener(Bundle b) {
-    RssSnapshotListener listener = new RssSnapshotListener(b, mRssSnapshotHelper);
+  private ShowmapSnapshotListener initListener(Bundle b) {
+    ShowmapSnapshotListener listener = new ShowmapSnapshotListener(b, mShowmapSnapshotHelper);
     listener.setInstrumentation(mInstrumentation);
     return listener;
   }
@@ -71,21 +73,23 @@
 
     mListener.testRunStarted(mRunDesc);
 
-    verify(mRssSnapshotHelper).setUp(VALID_OUTPUT_DIR, "process1", "process2");
+    verify(mShowmapSnapshotHelper).setUp(VALID_OUTPUT_DIR, "process1", "process2");
   }
 
   @Test
   public void testAdditionalOptions() throws Exception {
     Bundle b = new Bundle();
     b.putString(PROCESS_NAMES_KEY, "process1");
+    b.putString(METRIC_NAME_INDEX, "rss:1,pss:2");
     b.putString(OUTPUT_DIR_KEY, VALID_OUTPUT_DIR);
     b.putString(DROP_CACHE_KEY, "all");
     mListener = initListener(b);
 
     mListener.testRunStarted(mRunDesc);
 
-    verify(mRssSnapshotHelper).setUp(VALID_OUTPUT_DIR, "process1");
+    verify(mShowmapSnapshotHelper).setUp(VALID_OUTPUT_DIR, "process1");
+    verify(mShowmapSnapshotHelper).setMetricNameIndex(SAMPLE_METRIC_INDEX);
     // DROP_CACHE_KEY values: "pagecache" = 1, "slab" = 2, "all" = 3
-    verify(mRssSnapshotHelper).setDropCacheOption(3);
+    verify(mShowmapSnapshotHelper).setDropCacheOption(3);
   }
 }
diff --git a/libraries/device-collectors/src/test/platform/android/device/collectors/StatsdListenerTest.java b/libraries/device-collectors/src/test/platform/android/device/collectors/StatsdListenerTest.java
index 731f3de..db8606d 100644
--- a/libraries/device-collectors/src/test/platform/android/device/collectors/StatsdListenerTest.java
+++ b/libraries/device-collectors/src/test/platform/android/device/collectors/StatsdListenerTest.java
@@ -17,6 +17,7 @@
 
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.anyLong;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
@@ -96,6 +97,9 @@
         // Stub calls to permission APIs.
         doNothing().when(mListener).adoptShellPermissionIdentity();
         doNothing().when(mListener).dropShellPermissionIdentity();
+        // Stub calls to StatsLog APIs.
+        doReturn(true).when(mListener).logStart(anyInt());
+        doReturn(true).when(mListener).logStop(anyInt());
         // Stub file I/O.
         doAnswer(invocation -> invocation.getArgument(0)).when(mListener).writeToFile(any(), any());
         // Stub randome UUID generation.
@@ -116,8 +120,10 @@
         mListener.onTestRunStart(runData, description);
         verify(mListener, times(1)).addStatsConfig(eq(CONFIG_ID_1), eq(CONFIG_1.toByteArray()));
         verify(mListener, times(1)).addStatsConfig(eq(CONFIG_ID_2), eq(CONFIG_2.toByteArray()));
+        verify(mListener, times(1)).logStart(eq(StatsdListener.RUN_EVENT_LABEL));
 
         mListener.onTestRunEnd(runData, new Result());
+        verify(mListener, times(1)).logStop(eq(StatsdListener.RUN_EVENT_LABEL));
         verify(mListener, times(1)).getStatsReports(eq(CONFIG_ID_1));
         verify(mListener, times(1)).getStatsReports(eq(CONFIG_ID_2));
         verify(mListener, times(1)).removeStatsConfig(eq(CONFIG_ID_1));
@@ -201,8 +207,10 @@
         mListener.onTestStart(testData, description);
         verify(mListener, times(1)).addStatsConfig(eq(CONFIG_ID_1), eq(CONFIG_1.toByteArray()));
         verify(mListener, times(1)).addStatsConfig(eq(CONFIG_ID_2), eq(CONFIG_2.toByteArray()));
+        verify(mListener, times(1)).logStart(eq(StatsdListener.TEST_EVENT_LABEL));
 
         mListener.onTestEnd(testData, description);
+        verify(mListener, times(1)).logStop(eq(StatsdListener.TEST_EVENT_LABEL));
         verify(mListener, times(1)).getStatsReports(eq(CONFIG_ID_1));
         verify(mListener, times(1)).getStatsReports(eq(CONFIG_ID_2));
         verify(mListener, times(1)).removeStatsConfig(eq(CONFIG_ID_1));
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/Assertions.java b/libraries/flicker/src/com/android/server/wm/flicker/Assertions.java
index ff2de41..0723221 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/Assertions.java
+++ b/libraries/flicker/src/com/android/server/wm/flicker/Assertions.java
@@ -16,8 +16,12 @@
 
 package com.android.server.wm.flicker;
 
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Function;
+import java.util.stream.Collectors;
 
 /**
  * Collection of functional interfaces and classes representing assertions and their associated
@@ -50,14 +54,72 @@
      * Utility class to store assertions with an identifier to help generate more useful debug data
      * when dealing with multiple assertions.
      */
-    public static class NamedAssertion<T> {
-        public final TraceAssertion<T> assertion;
-        public final String name;
+    public static class NamedAssertion<T> implements TraceAssertion<T> {
+        private final TraceAssertion<T> assertion;
+        private final String name;
 
         public NamedAssertion(TraceAssertion<T> assertion, String name) {
             this.assertion = assertion;
             this.name = name;
         }
+
+        public String getName() {
+            return this.name;
+        }
+
+        @Override
+        public Result apply(T t) {
+            return this.assertion.apply(t);
+        }
+
+        @Override
+        public String toString() {
+            return "Assertion(" + this.name + ")";
+        }
+    }
+
+    public static class CompoundAssertion<T> extends NamedAssertion<T> {
+        private final List<NamedAssertion<T>> assertions = new ArrayList<>();
+
+        public CompoundAssertion(TraceAssertion<T> assertion, String name) {
+            super(assertion, name);
+
+            add(assertion, name);
+        }
+
+        public void add(TraceAssertion<T> assertion, String name) {
+            this.assertions.add(new NamedAssertion<>(assertion, name));
+        }
+
+        @Override
+        public String getName() {
+            return this.assertions.stream().map(p -> p.name).collect(Collectors.joining(" and "));
+        }
+
+        @Override
+        public Result apply(T t) {
+            List<Result> assertionResults =
+                    this.assertions.stream().map(p -> p.apply(t)).collect(Collectors.toList());
+
+            boolean passed = assertionResults.stream().allMatch(Result::passed);
+            String reason =
+                    assertionResults
+                            .stream()
+                            .filter(p -> !p.passed())
+                            .map(p -> p.reason)
+                            .collect(Collectors.joining(" and "));
+
+            Optional<Long> timestamp = assertionResults.stream().map(p -> p.timestamp).findFirst();
+
+            return timestamp
+                    .map(p -> new Result(passed, p, this.getName(), reason))
+                    .orElseGet(() -> new Result(passed, reason));
+        }
+
+        @Override
+        public String toString() {
+            return "CompoundAssertion(" + this.getName() + ")";
+        }
     }
 
     /** Contains the result of an assertion including the reason for failed assertions. */
@@ -114,9 +176,13 @@
         private String prettyTimestamp(long timestamp_ns) {
             StringBuilder prettyTimestamp = new StringBuilder();
             TimeUnit[] timeUnits = {
-                TimeUnit.HOURS, TimeUnit.MINUTES, TimeUnit.SECONDS, TimeUnit.MILLISECONDS
+                TimeUnit.DAYS,
+                TimeUnit.HOURS,
+                TimeUnit.MINUTES,
+                TimeUnit.SECONDS,
+                TimeUnit.MILLISECONDS
             };
-            String[] unitSuffixes = {"h", "m", "s", "ms"};
+            String[] unitSuffixes = {"d", "h", "m", "s", "ms"};
 
             for (int i = 0; i < timeUnits.length; i++) {
                 long convertedTime = timeUnits[i].convert(timestamp_ns, TimeUnit.NANOSECONDS);
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/AssertionsChecker.java b/libraries/flicker/src/com/android/server/wm/flicker/AssertionsChecker.java
index c30e012..68cc812 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/AssertionsChecker.java
+++ b/libraries/flicker/src/com/android/server/wm/flicker/AssertionsChecker.java
@@ -17,8 +17,8 @@
 package com.android.server.wm.flicker;
 
 import com.android.server.wm.flicker.Assertions.NamedAssertion;
+import com.android.server.wm.flicker.Assertions.CompoundAssertion;
 import com.android.server.wm.flicker.Assertions.Result;
-import com.android.server.wm.flicker.Assertions.TraceAssertion;
 
 import java.util.ArrayList;
 import java.util.LinkedList;
@@ -35,11 +35,28 @@
     private boolean mFilterEntriesByRange = false;
     private long mFilterStartTime = 0;
     private long mFilterEndTime = 0;
+    private boolean mSkipUntilFirstAssertion = false;
     private AssertionOption mOption = AssertionOption.NONE;
-    private List<NamedAssertion<T>> mAssertions = new LinkedList<>();
+    private List<CompoundAssertion<T>> mAssertions = new LinkedList<>();
 
     public void add(Assertions.TraceAssertion<T> assertion, String name) {
-        mAssertions.add(new NamedAssertion<>(assertion, name));
+        mAssertions.add(new CompoundAssertion<>(assertion, name));
+    }
+
+    public void append(Assertions.TraceAssertion<T> assertion, String name) {
+        CompoundAssertion<T> lastAssertion = mAssertions.get(mAssertions.size() - 1);
+        lastAssertion.add(assertion, name);
+    }
+
+    /**
+     * Ignores the first entries in the trace, until the first assertion passes. If it reaches the
+     * end of the trace without passing any assertion, return a failure with the name/reason from
+     * the first assertion
+     *
+     * @return
+     */
+    public void skipUntilFirstAssertion() {
+        mSkipUntilFirstAssertion = true;
     }
 
     public void filterByRange(long startTime, long endTime) {
@@ -76,7 +93,6 @@
      */
     public List<Result> test(List<T> entries) {
         List<T> filteredEntries;
-        List<Result> failures;
 
         if (mFilterEntriesByRange) {
             filteredEntries =
@@ -106,8 +122,12 @@
      * added. Each assertion must be true for at least a single trace entry.
      *
      * <p>This can be used to check for asserting a change in property over a trace. Such as
-     * visibility for a window changes from true to false or top-most window changes from A to Bb
-     * and back to A again.
+     * visibility for a window changes from true to false or top-most window changes from A to B and
+     * back to A again.
+     *
+     * <p>It is also possible to ignore failures on initial elements, until the first assertion
+     * passes, this allows the trace to be recorded for longer periods, and the checks to happen
+     * only after some time.
      */
     private List<Result> assertChanges(List<T> entries) {
         List<Result> failures = new ArrayList<>();
@@ -120,14 +140,21 @@
         }
 
         while (assertionIndex < mAssertions.size() && entryIndex < entries.size()) {
-            TraceAssertion<T> currentAssertion = mAssertions.get(assertionIndex).assertion;
+            NamedAssertion<T> currentAssertion = mAssertions.get(assertionIndex);
             Result result = currentAssertion.apply(entries.get(entryIndex));
+            boolean ignoreFailure = mSkipUntilFirstAssertion && lastPassedAssertionIndex == -1;
+
             if (result.passed()) {
                 lastPassedAssertionIndex = assertionIndex;
                 entryIndex++;
                 continue;
             }
 
+            if (ignoreFailure) {
+                entryIndex++;
+                continue;
+            }
+
             if (lastPassedAssertionIndex != assertionIndex) {
                 failures.add(result);
                 break;
@@ -140,25 +167,29 @@
             }
         }
 
+        if (lastPassedAssertionIndex == -1 && mAssertions.size() > 0 && failures.isEmpty()) {
+            failures.add(new Result(false /* success */, mAssertions.get(0).getName()));
+        }
+
         if (failures.isEmpty()) {
             if (assertionIndex != mAssertions.size() - 1) {
                 String reason =
                         "\nAssertion "
-                                + mAssertions.get(assertionIndex).name
+                                + mAssertions.get(assertionIndex).getName()
                                 + " never became false";
                 reason +=
                         "\nPassed assertions: "
                                 + mAssertions
                                         .stream()
                                         .limit(assertionIndex)
-                                        .map(assertion -> assertion.name)
+                                        .map(NamedAssertion::getName)
                                         .collect(Collectors.joining(","));
                 reason +=
                         "\nUntested assertions: "
                                 + mAssertions
                                         .stream()
                                         .skip(assertionIndex + 1)
-                                        .map(assertion -> assertion.name)
+                                        .map(NamedAssertion::getName)
                                         .collect(Collectors.joining(","));
 
                 Result result =
@@ -176,7 +207,7 @@
     private List<Result> assertEntry(T entry) {
         List<Result> failures = new ArrayList<>();
         for (NamedAssertion<T> assertion : mAssertions) {
-            Result result = assertion.assertion.apply(entry);
+            Result result = assertion.apply(entry);
             if (result.failed()) {
                 failures.add(result);
             }
@@ -187,9 +218,7 @@
     private List<Result> assertAll(List<T> entries) {
         return mAssertions
                 .stream()
-                .flatMap(
-                        assertion ->
-                                entries.stream().map(assertion.assertion).filter(Result::failed))
+                .flatMap(assertion -> entries.stream().map(assertion).filter(Result::failed))
                 .collect(Collectors.toList());
     }
 
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/LayersTrace.java b/libraries/flicker/src/com/android/server/wm/flicker/LayersTrace.java
index 74faca7..f3eee72 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/LayersTrace.java
+++ b/libraries/flicker/src/com/android/server/wm/flicker/LayersTrace.java
@@ -44,10 +44,12 @@
 public class LayersTrace {
     private final List<Entry> mEntries;
     @Nullable private final Path mSource;
+    @Nullable private final String mSourceChecksum;
 
-    private LayersTrace(List<Entry> entries, Path source) {
+    private LayersTrace(List<Entry> entries, Path source, String sourceChecksum) {
         this.mEntries = entries;
         this.mSource = source;
+        this.mSourceChecksum = sourceChecksum;
     }
 
     /**
@@ -58,8 +60,21 @@
      * @param source Path to source of data for additional debug information
      * @param orphanLayerCallback a callback to handle any unexpected orphan layers
      */
-    public static LayersTrace parseFrom(byte[] data, Path source,
-            Consumer<Layer> orphanLayerCallback) {
+    public static LayersTrace parseFrom(
+            byte[] data, Path source, Consumer<Layer> orphanLayerCallback) {
+        return parseFrom(data, source, null /* sourceChecksum */, orphanLayerCallback);
+    }
+
+    /**
+     * Parses {@code LayersTraceFileProto} from {@code data} and uses the proto to generates a list
+     * of trace entries, storing the flattened layers into its hierarchical structure.
+     *
+     * @param data binary proto data
+     * @param source Path to source of data for additional debug information
+     * @param orphanLayerCallback a callback to handle any unexpected orphan layers
+     */
+    public static LayersTrace parseFrom(
+            byte[] data, Path source, String sourceChecksum, Consumer<Layer> orphanLayerCallback) {
         List<Entry> entries = new ArrayList<>();
         LayersTraceFileProto fileProto;
         try {
@@ -74,7 +89,18 @@
                             orphanLayerCallback);
             entries.add(entry);
         }
-        return new LayersTrace(entries, source);
+        return new LayersTrace(entries, source, sourceChecksum);
+    }
+
+    /**
+     * Parses {@code LayersTraceFileProto} from {@code data} and uses the proto to generates a list
+     * of trace entries, storing the flattened layers into its hierarchical structure.
+     *
+     * @param data binary proto data
+     * @param source Path to source of data for additional debug information
+     */
+    public static LayersTrace parseFrom(byte[] data, Path source, String sourceChecksum) {
+        return parseFrom(data, source, sourceChecksum, null /* orphanLayerCallback */);
     }
 
     /**
@@ -85,7 +111,7 @@
      * @param source Path to source of data for additional debug information
      */
     public static LayersTrace parseFrom(byte[] data, Path source) {
-        return parseFrom(data, source, null /* orphanLayerCallback */);
+        return parseFrom(data, source, null /* sourceChecksum */, null /* orphanLayerCallback */);
     }
 
     /**
@@ -95,7 +121,7 @@
      * @param data binary proto data
      */
     public static LayersTrace parseFrom(byte[] data) {
-        return parseFrom(data, null);
+        return parseFrom(data, null /* source */);
     }
 
     public List<Entry> getEntries() {
@@ -115,6 +141,10 @@
         return Optional.ofNullable(mSource);
     }
 
+    public String getSourceChecksum() {
+        return mSourceChecksum;
+    }
+
     /** Represents a single Layer trace entry. */
     public static class Entry implements ITraceEntry {
         private long mTimestamp;
@@ -126,6 +156,33 @@
             this.mRootLayers = rootLayers;
         }
 
+        /**
+         * Determines the id of the root element.
+         *
+         * <p>On some files, such as the ones used in the FlickerLib testdata, the root nodes are
+         * those that have parent=0, on newer traces, the root nodes are those that have parent=-1
+         *
+         * <p>This function keeps compatibility with both new and older traces by searching for a
+         * known root layer (Display Root) and considering its parent Id as overall root.
+         */
+        private static Layer getRootLayer(SparseArray<Layer> layerMap) {
+            Layer knownRoot = null;
+            int numKeys = layerMap.size();
+            for (int i = 0; i < numKeys; ++i) {
+                Layer currentLayer = layerMap.valueAt(i);
+                if (currentLayer.isRootLayer()) {
+                    knownRoot = currentLayer;
+                    break;
+                }
+            }
+
+            if (knownRoot == null) {
+                throw new IllegalStateException("Display root layer not found.");
+            }
+
+            return layerMap.get(knownRoot.getParentId());
+        }
+
         /** Constructs the layer hierarchy from a flattened list of layers. */
         public static Entry fromFlattenedLayers(long timestamp, LayerProto[] protos,
                 Consumer<Layer> orphanLayerCallback) {
@@ -156,13 +213,14 @@
                 newLayer.addParent(layerMap.get(parentId));
             }
 
-            // Remove root node (id = 0)
-            orphans.remove(layerMap.get(-1));
+            // Remove root node
+            Layer rootLayer = getRootLayer(layerMap);
+            orphans.remove(rootLayer);
             // Fail if we find orphan layers.
             orphans.forEach(
                     orphan -> {
                         if (orphanLayerCallback != null) {
-                            // Workaround for b/141326137, ignore the existance of an orphan layer
+                            // Workaround for b/141326137, ignore the existence of an orphan layer
                             orphanLayerCallback.accept(orphan);
                             return;
                         }
@@ -171,7 +229,7 @@
                                         .stream()
                                         .map(node -> Integer.toString(node.getId()))
                                         .collect(Collectors.joining(", "));
-                        int orphanId = orphan.mChildren.get(0).mProto.parent;
+                        int orphanId = orphan.mChildren.get(0).getParentId();
                         throw new RuntimeException(
                                 "Failed to parse layers trace. Found orphan layers with parent "
                                         + "layer id:"
@@ -180,7 +238,7 @@
                                         + childNodes);
                     });
 
-            return new Entry(timestamp, layerMap.get(-1).mChildren);
+            return new Entry(timestamp, rootLayer.mChildren);
         }
 
         /** Extracts {@link Rect} from {@link RectProto}. */
@@ -282,6 +340,22 @@
             return new Result(false /* success */, this.mTimestamp, assertionName, reason);
         }
 
+        /** Checks if a layer with name {@code layerName} exists in the hierarchy. */
+        public Result exists(String layerName) {
+            String assertionName = "exists";
+            String reason = "Could not find " + layerName;
+            for (Layer layer : asFlattenedLayers()) {
+                if (layer.mProto.name.contains(layerName)) {
+                    return new Result(
+                            true /* success */,
+                            this.mTimestamp,
+                            assertionName,
+                            layer.mProto.name + " exists");
+                }
+            }
+            return new Result(false /* success */, this.mTimestamp, assertionName, reason);
+        }
+
         /** Checks if a layer with name {@code layerName} is visible. */
         public Result isVisible(String layerName) {
             String assertionName = "isVisible";
@@ -363,6 +437,18 @@
             return mProto.id;
         }
 
+        public int getParentId() {
+            return mProto.parent;
+        }
+
+        public String getName() {
+            if (mProto != null) {
+                return mProto.name;
+            }
+
+            return "";
+        }
+
         public boolean isActiveBufferEmpty() {
             return this.mProto.activeBuffer == null
                     || this.mProto.activeBuffer.height == 0
@@ -394,7 +480,7 @@
         }
 
         public boolean isRootLayer() {
-            return mParent == null || mParent.mProto == null;
+            return mParent != null && mParent.mProto == null;
         }
 
         public boolean isInvisible() {
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/LayersTraceSubject.java b/libraries/flicker/src/com/android/server/wm/flicker/LayersTraceSubject.java
index 70bcdcd..21a3f2b 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/LayersTraceSubject.java
+++ b/libraries/flicker/src/com/android/server/wm/flicker/LayersTraceSubject.java
@@ -45,6 +45,15 @@
             LayersTraceSubject::new;
 
     private AssertionsChecker<Entry> mChecker = new AssertionsChecker<>();
+    private boolean mNewAssertion = true;
+
+    private void addAssertion(Assertions.TraceAssertion<Entry> assertion, String name) {
+        if (mNewAssertion) {
+            mChecker.add(assertion, name);
+        } else {
+            mChecker.append(assertion, name);
+        }
+    }
 
     private LayersTraceSubject(FailureMetadata fm, @Nullable LayersTrace subject) {
         super(fm, subject);
@@ -70,8 +79,12 @@
     // User-defined entry point
     public static LayersTraceSubject assertThat(@Nullable TransitionResult result,
             Consumer<LayersTrace.Layer> orphanLayerCallback) {
-        LayersTrace entries = LayersTrace.parseFrom(result.getLayersTrace(),
-                result.getLayersTracePath(), orphanLayerCallback);
+        LayersTrace entries =
+                LayersTrace.parseFrom(
+                        result.getLayersTrace(),
+                        result.getLayersTracePath(),
+                        result.getLayersTraceChecksum(),
+                        orphanLayerCallback);
         return assertWithMessage(result.toString()).about(FACTORY).that(entries);
     }
 
@@ -90,10 +103,29 @@
     }
 
     public LayersTraceSubject then() {
+        mNewAssertion = true;
         mChecker.checkChangingAssertions();
         return this;
     }
 
+    public LayersTraceSubject and() {
+        mNewAssertion = false;
+        mChecker.checkChangingAssertions();
+        return this;
+    }
+
+    /**
+     * Ignores the first entries in the trace, until the first assertion passes. If it reaches the
+     * end of the trace without passing any assertion, return a failure with the name/reason from
+     * the first assertion
+     *
+     * @return
+     */
+    public LayersTraceSubject skipUntilFirstAssertion() {
+        mChecker.skipUntilFirstAssertion();
+        return this;
+    }
+
     public void inTheBeginning() {
         if (actual().getEntries().isEmpty()) {
             fail("No entries found.");
@@ -120,6 +152,8 @@
                 tracePath =
                         "\nLayers Trace can be found in: "
                                 + actual().getSource().get().toAbsolutePath()
+                                + "\nChecksum: "
+                                + actual().getSourceChecksum()
                                 + "\n";
             }
             fail(tracePath + failureLogs);
@@ -127,24 +161,40 @@
     }
 
     public LayersTraceSubject coversRegion(Rect rect) {
-        mChecker.add(entry -> entry.coversRegion(rect), "coversRegion(" + rect + ")");
+        addAssertion(entry -> entry.coversRegion(rect), "coversRegion(" + rect + ")");
         return this;
     }
 
     public LayersTraceSubject hasVisibleRegion(String layerName, Rect size) {
-        mChecker.add(
+        addAssertion(
                 entry -> entry.hasVisibleRegion(layerName, size),
                 "hasVisibleRegion(" + layerName + size + ")");
         return this;
     }
 
-    public LayersTraceSubject showsLayer(String layerName) {
-        mChecker.add(entry -> entry.isVisible(layerName), "showsLayer(" + layerName + ")");
+    public LayersTraceSubject hasNotLayer(String layerName) {
+        addAssertion(entry -> entry.exists(layerName).negate(), "hasNotLayer(" + layerName + ")");
         return this;
     }
 
-    public LayersTraceSubject hidesLayer(String layerName) {
-        mChecker.add(entry -> entry.isVisible(layerName).negate(), "hidesLayer(" + layerName + ")");
+    public LayersTraceSubject hasLayer(String layerName) {
+        addAssertion(entry -> entry.exists(layerName), "hasLayer(" + layerName + ")");
         return this;
     }
+
+    public LayersTraceSubject showsLayer(String layerName) {
+        addAssertion(entry -> entry.isVisible(layerName), "showsLayer(" + layerName + ")");
+        return this;
+    }
+
+    public LayersTraceSubject replaceVisibleLayer(
+            String previousLayerName, String currentLayerName) {
+        return hidesLayer(previousLayerName).and().showsLayer(currentLayerName);
+    }
+
+    public LayersTraceSubject hidesLayer(String layerName) {
+        addAssertion(entry -> entry.isVisible(layerName).negate(), "hidesLayer(" + layerName + ")");
+        return this;
+    }
+
 }
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/TransitionRunner.java b/libraries/flicker/src/com/android/server/wm/flicker/TransitionRunner.java
index 007196b..0b74a6c 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/TransitionRunner.java
+++ b/libraries/flicker/src/com/android/server/wm/flicker/TransitionRunner.java
@@ -16,7 +16,7 @@
 
 package com.android.server.wm.flicker;
 
-import static com.android.server.wm.flicker.monitor.ITransitionMonitor.OUTPUT_DIR;
+import static com.android.server.wm.flicker.monitor.TransitionMonitor.OUTPUT_DIR;
 
 import android.util.Log;
 
@@ -25,15 +25,13 @@
 import androidx.annotation.VisibleForTesting;
 import androidx.test.InstrumentationRegistry;
 
-import com.android.server.wm.flicker.monitor.ITransitionMonitor;
+import com.android.server.wm.flicker.monitor.TransitionMonitor;
 import com.android.server.wm.flicker.monitor.LayersTraceMonitor;
 import com.android.server.wm.flicker.monitor.ScreenRecorder;
 import com.android.server.wm.flicker.monitor.WindowAnimationFrameStatsMonitor;
 import com.android.server.wm.flicker.monitor.WindowManagerTraceMonitor;
 
-import com.google.common.io.Files;
-
-import java.io.IOException;
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
@@ -101,8 +99,8 @@
     private final LayersTraceMonitor mLayersTraceMonitor;
     private final WindowAnimationFrameStatsMonitor mFrameStatsMonitor;
 
-    private final List<ITransitionMonitor> mAllRunsMonitors;
-    private final List<ITransitionMonitor> mPerRunMonitors;
+    private final List<TransitionMonitor> mAllRunsMonitors;
+    private final List<TransitionMonitor> mPerRunMonitors;
     private final List<Runnable> mBeforeAlls;
     private final List<Runnable> mBefores;
     private final List<Runnable> mTransitions;
@@ -148,13 +146,13 @@
      */
     public TransitionRunner run() {
         mResults = new ArrayList<>();
-        mAllRunsMonitors.forEach(ITransitionMonitor::start);
+        mAllRunsMonitors.forEach(TransitionMonitor::start);
         mBeforeAlls.forEach(Runnable::run);
         for (int iteration = 0; iteration < mIterations; iteration++) {
             mBefores.forEach(Runnable::run);
-            mPerRunMonitors.forEach(ITransitionMonitor::start);
+            mPerRunMonitors.forEach(TransitionMonitor::start);
             mTransitions.forEach(Runnable::run);
-            mPerRunMonitors.forEach(ITransitionMonitor::stop);
+            mPerRunMonitors.forEach(TransitionMonitor::stop);
             mAfters.forEach(Runnable::run);
             if (runJankFree() && mFrameStatsMonitor.jankyFramesDetected()) {
                 String msg =
@@ -211,19 +209,31 @@
      */
     private TransitionResult saveResult(int iteration) {
         Path windowTrace = null;
+        String windowTraceChecksum = "";
         Path layerTrace = null;
+        String layerTraceChecksum = "";
         Path screenCaptureVideo = null;
+        String screenCaptureVideoChecksum = "";
 
         if (mPerRunMonitors.contains(mWmTraceMonitor)) {
             windowTrace = mWmTraceMonitor.save(mTestTag, iteration);
+            windowTraceChecksum = mWmTraceMonitor.getChecksum();
         }
         if (mPerRunMonitors.contains(mLayersTraceMonitor)) {
             layerTrace = mLayersTraceMonitor.save(mTestTag, iteration);
+            layerTraceChecksum = mLayersTraceMonitor.getChecksum();
         }
         if (mPerRunMonitors.contains(mScreenRecorder)) {
             screenCaptureVideo = mScreenRecorder.save(mTestTag, iteration);
+            screenCaptureVideoChecksum = mScreenRecorder.getChecksum();
         }
-        return new TransitionResult(layerTrace, windowTrace, screenCaptureVideo);
+        return new TransitionResult(
+                layerTrace,
+                layerTraceChecksum,
+                windowTrace,
+                windowTraceChecksum,
+                screenCaptureVideo,
+                screenCaptureVideoChecksum);
     }
 
     private boolean runJankFree() {
@@ -237,18 +247,27 @@
     /** Stores paths to all test artifacts. */
     @VisibleForTesting
     public static class TransitionResult {
-        @Nullable public final Path layersTrace;
-        @Nullable public final Path windowManagerTrace;
-        @Nullable public final Path screenCaptureVideo;
+        @Nullable private final Path layersTrace;
+        private final String layersTraceChecksum;
+        @Nullable private final Path windowManagerTrace;
+        private final String windowManagerTraceChecksum;
+        @Nullable private final Path screenCaptureVideo;
+        private final String screenCaptureVideoChecksum;
         private boolean flaggedForSaving = true;
 
         public TransitionResult(
                 @Nullable Path layersTrace,
+                String layersTraceChecksum,
                 @Nullable Path windowManagerTrace,
-                @Nullable Path screenCaptureVideo) {
+                String windowManagerTraceChecksum,
+                @Nullable Path screenCaptureVideo,
+                String screenCaptureVideoChecksum) {
             this.layersTrace = layersTrace;
+            this.layersTraceChecksum = layersTraceChecksum;
             this.windowManagerTrace = windowManagerTrace;
+            this.windowManagerTraceChecksum = windowManagerTraceChecksum;
             this.screenCaptureVideo = screenCaptureVideo;
+            this.screenCaptureVideoChecksum = screenCaptureVideoChecksum;
         }
 
         public void flagForSaving() {
@@ -265,8 +284,8 @@
 
         public byte[] getLayersTrace() {
             try {
-                return Files.toByteArray(this.layersTrace.toFile());
-            } catch (IOException e) {
+                return Files.readAllBytes(this.layersTrace);
+            } catch (Exception e) {
                 throw new RuntimeException(e);
             }
         }
@@ -275,14 +294,18 @@
             return layersTrace;
         }
 
+        public String getLayersTraceChecksum() {
+            return layersTraceChecksum;
+        }
+
         public boolean windowManagerTraceExists() {
             return windowManagerTrace != null && windowManagerTrace.toFile().exists();
         }
 
         public byte[] getWindowManagerTrace() {
             try {
-                return Files.toByteArray(this.windowManagerTrace.toFile());
-            } catch (IOException e) {
+                return Files.readAllBytes(this.windowManagerTrace);
+            } catch (Exception e) {
                 throw new RuntimeException(e);
             }
         }
@@ -291,6 +314,10 @@
             return windowManagerTrace;
         }
 
+        public String getWindowManagerTraceChecksum() {
+            return windowManagerTraceChecksum;
+        }
+
         public boolean screenCaptureVideoExists() {
             return screenCaptureVideo != null && screenCaptureVideo.toFile().exists();
         }
@@ -299,6 +326,10 @@
             return screenCaptureVideo;
         }
 
+        public String getScreenCaptureVideoChecksum() {
+            return screenCaptureVideoChecksum;
+        }
+
         public void delete() {
             if (layersTraceExists()) layersTrace.toFile().delete();
             if (windowManagerTraceExists()) windowManagerTrace.toFile().delete();
@@ -313,8 +344,8 @@
         private LayersTraceMonitor mLayersTraceMonitor;
         private WindowAnimationFrameStatsMonitor mFrameStatsMonitor;
 
-        private List<ITransitionMonitor> mAllRunsMonitors = new LinkedList<>();
-        private List<ITransitionMonitor> mPerRunMonitors = new LinkedList<>();
+        private List<TransitionMonitor> mAllRunsMonitors = new LinkedList<>();
+        private List<TransitionMonitor> mPerRunMonitors = new LinkedList<>();
         private List<Runnable> mBeforeAlls = new LinkedList<>();
         private List<Runnable> mBefores = new LinkedList<>();
         private List<Runnable> mTransitions = new LinkedList<>();
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/WindowManagerTrace.java b/libraries/flicker/src/com/android/server/wm/flicker/WindowManagerTrace.java
index 3afecc9..69b5fef 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/WindowManagerTrace.java
+++ b/libraries/flicker/src/com/android/server/wm/flicker/WindowManagerTrace.java
@@ -44,10 +44,12 @@
     private static final int DEFAULT_DISPLAY = 0;
     private final List<Entry> mEntries;
     @Nullable private final Path mSource;
+    @Nullable private final String mSourceChecksum;
 
-    private WindowManagerTrace(List<Entry> entries, Path source) {
+    private WindowManagerTrace(List<Entry> entries, Path source, String sourceChecksum) {
         this.mEntries = entries;
         this.mSource = source;
+        this.mSourceChecksum = sourceChecksum;
     }
 
     /**
@@ -57,7 +59,7 @@
      * @param data binary proto data
      * @param source Path to source of data for additional debug information
      */
-    public static WindowManagerTrace parseFrom(byte[] data, Path source) {
+    public static WindowManagerTrace parseFrom(byte[] data, Path source, String checksum) {
         List<Entry> entries = new ArrayList<>();
 
         WindowManagerTraceFileProto fileProto;
@@ -69,11 +71,11 @@
         for (WindowManagerTraceProto entryProto : fileProto.entry) {
             entries.add(new Entry(entryProto));
         }
-        return new WindowManagerTrace(entries, source);
+        return new WindowManagerTrace(entries, source, checksum);
     }
 
     public static WindowManagerTrace parseFrom(byte[] data) {
-        return parseFrom(data, null);
+        return parseFrom(data, null /* source */, null /* checksum */);
     }
 
     public List<Entry> getEntries() {
@@ -93,6 +95,10 @@
         return Optional.ofNullable(mSource);
     }
 
+    public String getSourceChecksum() {
+        return mSourceChecksum;
+    }
+
     /** Represents a single WindowManager trace entry. */
     public static class Entry implements ITraceEntry {
         private final WindowManagerTraceProto mProto;
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/WmTraceSubject.java b/libraries/flicker/src/com/android/server/wm/flicker/WmTraceSubject.java
index 21a2606..c44e0b2 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/WmTraceSubject.java
+++ b/libraries/flicker/src/com/android/server/wm/flicker/WmTraceSubject.java
@@ -39,6 +39,16 @@
             WmTraceSubject::new;
 
     private AssertionsChecker<WindowManagerTrace.Entry> mChecker = new AssertionsChecker<>();
+    private boolean mNewAssertion = true;
+
+    private void addAssertion(
+            Assertions.TraceAssertion<WindowManagerTrace.Entry> assertion, String name) {
+        if (mNewAssertion) {
+            mChecker.add(assertion, name);
+        } else {
+            mChecker.append(assertion, name);
+        }
+    }
 
     private WmTraceSubject(FailureMetadata fm, @Nullable WindowManagerTrace subject) {
         super(fm, subject);
@@ -53,7 +63,9 @@
     public static WmTraceSubject assertThat(@Nullable TransitionResult result) {
         WindowManagerTrace entries =
                 WindowManagerTrace.parseFrom(
-                        result.getWindowManagerTrace(), result.getWindowManagerTracePath());
+                        result.getWindowManagerTrace(),
+                        result.getWindowManagerTracePath(),
+                        result.getWindowManagerTraceChecksum());
         return assertWithMessage(result.toString()).about(FACTORY).that(entries);
     }
 
@@ -72,12 +84,31 @@
     }
 
     public WmTraceSubject then() {
+        mNewAssertion = true;
         mChecker.checkChangingAssertions();
         return this;
     }
 
+    public WmTraceSubject and() {
+        mNewAssertion = false;
+        mChecker.checkChangingAssertions();
+        return this;
+    }
+
+    /**
+     * Ignores the first entries in the trace, until the first assertion passes. If it reaches the
+     * end of the trace without passing any assertion, return a failure with the name/reason from
+     * the first assertion
+     *
+     * @return
+     */
+    public WmTraceSubject skipUntilFirstAssertion() {
+        mChecker.skipUntilFirstAssertion();
+        return this;
+    }
+
     public void inTheBeginning() {
-        if (getSubject().getEntries().isEmpty()) {
+        if (actual().getEntries().isEmpty()) {
             fail("No entries found.");
         }
         mChecker.checkFirstEntry();
@@ -85,7 +116,7 @@
     }
 
     public void atTheEnd() {
-        if (getSubject().getEntries().isEmpty()) {
+        if (actual().getEntries().isEmpty()) {
             fail("No entries found.");
         }
         mChecker.checkLastEntry();
@@ -93,9 +124,9 @@
     }
 
     private void test() {
-        List<Result> failures = mChecker.test(getSubject().getEntries());
+        List<Result> failures = mChecker.test(actual().getEntries());
         if (!failures.isEmpty()) {
-            Optional<Path> failureTracePath = getSubject().getSource();
+            Optional<Path> failureTracePath = actual().getSource();
             String failureLogs =
                     failures.stream().map(Result::toString).collect(Collectors.joining("\n"));
             String tracePath = "";
@@ -103,6 +134,8 @@
                 tracePath =
                         "\nWindowManager Trace can be found in: "
                                 + failureTracePath.get().toAbsolutePath()
+                                + "\nChecksum: "
+                                + actual().getSourceChecksum()
                                 + "\n";
             }
             fail(tracePath + failureLogs);
@@ -110,49 +143,49 @@
     }
 
     public WmTraceSubject showsAboveAppWindow(String partialWindowTitle) {
-        mChecker.add(
+        addAssertion(
                 entry -> entry.isAboveAppWindowVisible(partialWindowTitle),
                 "showsAboveAppWindow(" + partialWindowTitle + ")");
         return this;
     }
 
     public WmTraceSubject hidesAboveAppWindow(String partialWindowTitle) {
-        mChecker.add(
+        addAssertion(
                 entry -> entry.isAboveAppWindowVisible(partialWindowTitle).negate(),
                 "hidesAboveAppWindow" + "(" + partialWindowTitle + ")");
         return this;
     }
 
     public WmTraceSubject showsBelowAppWindow(String partialWindowTitle) {
-        mChecker.add(
+        addAssertion(
                 entry -> entry.isBelowAppWindowVisible(partialWindowTitle),
                 "showsBelowAppWindow(" + partialWindowTitle + ")");
         return this;
     }
 
     public WmTraceSubject hidesBelowAppWindow(String partialWindowTitle) {
-        mChecker.add(
+        addAssertion(
                 entry -> entry.isBelowAppWindowVisible(partialWindowTitle).negate(),
                 "hidesBelowAppWindow" + "(" + partialWindowTitle + ")");
         return this;
     }
 
     public WmTraceSubject showsImeWindow(String partialWindowTitle) {
-        mChecker.add(
+        addAssertion(
                 entry -> entry.isImeWindowVisible(partialWindowTitle),
                 "showsBelowAppWindow(" + partialWindowTitle + ")");
         return this;
     }
 
     public WmTraceSubject hidesImeWindow(String partialWindowTitle) {
-        mChecker.add(
+        addAssertion(
                 entry -> entry.isImeWindowVisible(partialWindowTitle).negate(),
                 "hidesImeWindow" + "(" + partialWindowTitle + ")");
         return this;
     }
 
     public WmTraceSubject showsAppWindowOnTop(String partialWindowTitle) {
-        mChecker.add(
+        addAssertion(
                 entry -> {
                     Result result = entry.isAppWindowVisible(partialWindowTitle);
                     if (result.passed()) {
@@ -165,7 +198,7 @@
     }
 
     public WmTraceSubject hidesAppWindowOnTop(String partialWindowTitle) {
-        mChecker.add(
+        addAssertion(
                 entry -> {
                     Result result = entry.isAppWindowVisible(partialWindowTitle).negate();
                     if (result.failed()) {
@@ -178,14 +211,14 @@
     }
 
     public WmTraceSubject showsAppWindow(String partialWindowTitle) {
-        mChecker.add(
+        addAssertion(
                 entry -> entry.isAppWindowVisible(partialWindowTitle),
                 "showsAppWindow(" + partialWindowTitle + ")");
         return this;
     }
 
     public WmTraceSubject hidesAppWindow(String partialWindowTitle) {
-        mChecker.add(
+        addAssertion(
                 entry -> entry.isAppWindowVisible(partialWindowTitle).negate(),
                 "hidesAppWindow(" + partialWindowTitle + ")");
         return this;
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/monitor/ITransitionMonitor.java b/libraries/flicker/src/com/android/server/wm/flicker/monitor/ITransitionMonitor.java
deleted file mode 100644
index e092d0b..0000000
--- a/libraries/flicker/src/com/android/server/wm/flicker/monitor/ITransitionMonitor.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.wm.flicker.monitor;
-
-import android.os.Environment;
-
-import java.nio.file.Path;
-import java.nio.file.Paths;
-
-/** Collects test artifacts during a UI transition. */
-public interface ITransitionMonitor {
-    Path OUTPUT_DIR = Paths.get(Environment.getExternalStorageDirectory().toString(), "flicker");
-
-    /** Starts monitor. */
-    void start();
-
-    /** Stops monitor. */
-    void stop();
-
-    /**
-     * Saves any monitor artifacts to file adding {@code testTag} and {@code iteration} to the file
-     * name.
-     *
-     * @param testTag suffix added to artifact name
-     * @param iteration suffix added to artifact name
-     * @return Path to saved artifact
-     */
-    default Path save(String testTag, int iteration) {
-        return save(testTag + "_" + iteration);
-    }
-
-    /**
-     * Saves any monitor artifacts to file adding {@code testTag} to the file name.
-     *
-     * @param testTag suffix added to artifact name
-     * @return Path to saved artifact
-     */
-    default Path save(String testTag) {
-        throw new UnsupportedOperationException("Save not implemented for this monitor");
-    }
-}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/monitor/LayersTraceMonitor.java b/libraries/flicker/src/com/android/server/wm/flicker/monitor/LayersTraceMonitor.java
index 03cf2d4..0130931 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/monitor/LayersTraceMonitor.java
+++ b/libraries/flicker/src/com/android/server/wm/flicker/monitor/LayersTraceMonitor.java
@@ -24,6 +24,7 @@
 
 /** Captures Layers trace from SurfaceFlinger. */
 public class LayersTraceMonitor extends TraceMonitor {
+    private static final String TRACE_FILE = "layers_trace.pb";
     private IWindowManager mWm = WindowManagerGlobal.getWindowManagerService();
 
     public LayersTraceMonitor() {
@@ -31,7 +32,7 @@
     }
 
     public LayersTraceMonitor(Path outputDir) {
-        super(outputDir, "layers_trace.pb");
+        super(outputDir, TRACE_FILE);
     }
 
     @Override
@@ -45,7 +46,7 @@
     }
 
     @Override
-    public boolean isEnabled() throws RemoteException {
+    public boolean isEnabled() {
         try {
             return mWm.isLayerTracing();
         } catch (RemoteException e) {
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/monitor/ScreenRecorder.java b/libraries/flicker/src/com/android/server/wm/flicker/monitor/ScreenRecorder.java
index 8b138aa..3a7c012 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/monitor/ScreenRecorder.java
+++ b/libraries/flicker/src/com/android/server/wm/flicker/monitor/ScreenRecorder.java
@@ -18,51 +18,46 @@
 
 import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
 
-import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
-
 import android.util.Log;
 
 import androidx.annotation.VisibleForTesting;
 
 import java.io.IOException;
-import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.Locale;
 
 /** Captures screen contents and saves it as a mp4 video file. */
-public class ScreenRecorder implements ITransitionMonitor {
-    @VisibleForTesting
-    public static final Path DEFAULT_OUTPUT_PATH = OUTPUT_DIR.resolve("transition.mp4");
-
-    private static final String TAG = "FLICKER";
+public class ScreenRecorder extends TraceMonitor {
+    private static final String TRACE_FILE = "transition.mp4";
     private int mWidth;
     private int mHeight;
     private Thread mRecorderThread;
 
     public ScreenRecorder() {
-        this(720, 1280);
+        this(720, 1280, OUTPUT_DIR, TRACE_FILE);
     }
 
-    public ScreenRecorder(int width, int height) {
+    public ScreenRecorder(int width, int height, Path outputPath, String traceFile) {
+        super(outputPath, outputPath.resolve(traceFile));
         mWidth = width;
         mHeight = height;
     }
 
     @VisibleForTesting
-    public static Path getPath(String testTag) {
-        return OUTPUT_DIR.resolve(testTag + ".mp4");
+    public Path getPath() {
+        return mOutputPath;
     }
 
     @Override
     public void start() {
-        OUTPUT_DIR.toFile().mkdirs();
+        mOutputPath.toFile().mkdirs();
         String command =
                 String.format(
                         Locale.getDefault(),
                         "screenrecord --size %dx%d %s",
                         mWidth,
                         mHeight,
-                        DEFAULT_OUTPUT_PATH);
+                        mTraceFile);
         mRecorderThread =
                 new Thread(
                         () -> {
@@ -86,18 +81,7 @@
     }
 
     @Override
-    public Path save(String testTag) {
-        if (!Files.exists(DEFAULT_OUTPUT_PATH)) {
-            Log.w(TAG, "No video file found on " + DEFAULT_OUTPUT_PATH);
-            return null;
-        }
-
-        try {
-            Path targetPath = Files.move(DEFAULT_OUTPUT_PATH, getPath(testTag), REPLACE_EXISTING);
-            Log.i(TAG, "Video saved to " + targetPath.toString());
-            return targetPath;
-        } catch (IOException e) {
-            throw new RuntimeException(e);
-        }
+    public boolean isEnabled() {
+        return mRecorderThread != null && mRecorderThread.isAlive();
     }
 }
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/monitor/TraceMonitor.java b/libraries/flicker/src/com/android/server/wm/flicker/monitor/TraceMonitor.java
index 333d723..778db73 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/monitor/TraceMonitor.java
+++ b/libraries/flicker/src/com/android/server/wm/flicker/monitor/TraceMonitor.java
@@ -23,65 +23,50 @@
 import androidx.annotation.VisibleForTesting;
 
 import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.Locale;
 
 /**
  * Base class for monitors containing common logic to read the trace as a byte array and save the
  * trace to another location.
  */
-public abstract class TraceMonitor implements ITransitionMonitor {
-    public static final String TAG = "FLICKER";
-    private static final String TRACE_DIR = "/data/misc/wmtrace/";
+public abstract class TraceMonitor extends TransitionMonitor {
+    private static final Path TRACE_DIR = Paths.get("/data/misc/wmtrace/");
 
-    private Path mOutputDir;
-    public String mTraceFileName;
+    protected Path mTraceFile;
 
     public abstract boolean isEnabled() throws RemoteException;
 
-    public TraceMonitor(Path outputDir, String traceFileName) {
-        mOutputDir = outputDir;
-        mTraceFileName = traceFileName;
+    TraceMonitor(Path outputDir, Path traceFile) {
+        mTraceFile = traceFile;
+        mOutputPath = outputDir;
     }
 
-    /**
-     * Saves trace file to the external storage directory suffixing the name with the testtag and
-     * iteration.
-     *
-     * <p>Moves the trace file from the default location via a shell command since the test app does
-     * not have security privileges to access /data/misc/wmtrace.
-     *
-     * @param testTag suffix added to trace name used to identify trace
-     * @return Path to saved trace file
-     */
-    @Override
-    public Path save(String testTag) {
-        mOutputDir.toFile().mkdirs();
-        Path traceFileCopy = getOutputTraceFilePath(testTag);
+    TraceMonitor(Path outputDir, String traceFileName) {
+        this(outputDir, TRACE_DIR.resolve(traceFileName));
+    }
 
-        // Move the trace file to the output directory
+    protected Path saveTrace(String testTag) {
+        Path traceFileCopy = getOutputTraceFilePath(testTag);
+        moveFile(mTraceFile, traceFileCopy);
+
+        return traceFileCopy;
+    }
+
+    protected void moveFile(Path src, Path dst) {
+        // Move the  file to the output directory
         // Note: Due to b/141386109, certain devices do not allow moving the files between
         //       directories with different encryption policies, so manually copy and then
         //       remove the original file
         String copyCommand =
-                String.format(
-                        Locale.getDefault(),
-                        "cp %s%s %s",
-                        TRACE_DIR,
-                        mTraceFileName,
-                        traceFileCopy.toString());
+                String.format(Locale.getDefault(), "cp %s %s", src.toString(), dst.toString());
         runShellCommand(copyCommand);
-        String removeCommand =
-                String.format(
-                        Locale.getDefault(),
-                        "rm %s%s",
-                        TRACE_DIR,
-                        mTraceFileName);
+        String removeCommand = String.format(Locale.getDefault(), "rm %s", src.toString());
         runShellCommand(removeCommand);
-        return traceFileCopy;
     }
 
     @VisibleForTesting
     public Path getOutputTraceFilePath(String testTag) {
-        return mOutputDir.resolve(mTraceFileName + "_" + testTag);
+        return mOutputPath.resolve(testTag + "_" + mTraceFile.getFileName());
     }
 }
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/monitor/TransitionMonitor.java b/libraries/flicker/src/com/android/server/wm/flicker/monitor/TransitionMonitor.java
new file mode 100644
index 0000000..ea64d33
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/monitor/TransitionMonitor.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm.flicker.monitor;
+
+import android.os.Environment;
+
+import com.google.common.io.BaseEncoding;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/** Collects test artifacts during a UI transition. */
+public abstract class TransitionMonitor {
+    static final String TAG = "FLICKER";
+    public static final Path OUTPUT_DIR =
+            Paths.get(Environment.getExternalStorageDirectory().toString(), "flicker");
+    protected String mChecksum;
+    protected Path mOutputPath;
+
+    /** Starts monitor. */
+    public abstract void start();
+
+    /** Stops monitor. */
+    public abstract void stop();
+
+    /**
+     * Saves any monitor artifacts to file adding {@code testTag} and {@code iteration} to the file
+     * name.
+     *
+     * @param testTag suffix added to artifact name
+     * @param iteration suffix added to artifact name
+     * @return Path to saved artifact
+     */
+    public Path save(String testTag, int iteration) {
+        return save(testTag + "_" + iteration);
+    }
+
+    /**
+     * Saves any monitor artifacts to file adding {@code testTag} to the file name.
+     *
+     * @param testTag suffix added to artifact name
+     * @return Path to saved artifact
+     */
+    /**
+     * Saves trace file to the external storage directory suffixing the name with the testtag and
+     * iteration.
+     *
+     * <p>Moves the trace file from the default location via a shell command since the test app does
+     * not have security privileges to access /data/misc/wmtrace.
+     *
+     * @param testTag suffix added to trace name used to identify trace
+     * @return Path to saved trace file and file checksum (SHA-256)
+     */
+    public Path save(String testTag) {
+        mOutputPath.toFile().mkdirs();
+        Path savedTrace = saveTrace(testTag);
+        mChecksum = calculateChecksum(savedTrace);
+        return savedTrace;
+    }
+
+    protected Path saveTrace(String testTag) {
+        throw new UnsupportedOperationException("Save not implemented for this monitor");
+    }
+
+    public String getChecksum() {
+        return mChecksum;
+    }
+
+    static String calculateChecksum(Path traceFile) {
+        try {
+            MessageDigest digest = MessageDigest.getInstance("SHA-256");
+            byte[] fileData = Files.readAllBytes(traceFile);
+            byte[] hash = digest.digest(fileData);
+            return BaseEncoding.base16().encode(hash).toLowerCase();
+        } catch (NoSuchAlgorithmException e) {
+            throw new IllegalArgumentException("Checksum algorithm SHA-256 not found", e);
+        } catch (IOException e) {
+            throw new IllegalArgumentException("File not found", e);
+        }
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/monitor/WindowAnimationFrameStatsMonitor.java b/libraries/flicker/src/com/android/server/wm/flicker/monitor/WindowAnimationFrameStatsMonitor.java
index 4c950a4..c1fab77 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/monitor/WindowAnimationFrameStatsMonitor.java
+++ b/libraries/flicker/src/com/android/server/wm/flicker/monitor/WindowAnimationFrameStatsMonitor.java
@@ -28,7 +28,7 @@
  * <p>Adapted from {@link androidx.test.jank.internal.WindowAnimationFrameStatsMonitorImpl} using
  * the same threshold to determine jank.
  */
-public class WindowAnimationFrameStatsMonitor implements ITransitionMonitor {
+public class WindowAnimationFrameStatsMonitor extends TransitionMonitor {
 
     private static final String TAG = "FLICKER";
     // Maximum normalized error in frame duration before the frame is considered janky
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/monitor/WindowManagerTraceMonitor.java b/libraries/flicker/src/com/android/server/wm/flicker/monitor/WindowManagerTraceMonitor.java
index f2be413..c216170 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/monitor/WindowManagerTraceMonitor.java
+++ b/libraries/flicker/src/com/android/server/wm/flicker/monitor/WindowManagerTraceMonitor.java
@@ -24,6 +24,7 @@
 
 /** Captures WindowManager trace from WindowManager. */
 public class WindowManagerTraceMonitor extends TraceMonitor {
+    private static final String TRACE_FILE = "wm_trace.pb";
     private IWindowManager mWm = WindowManagerGlobal.getWindowManagerService();
 
     public WindowManagerTraceMonitor() {
@@ -31,7 +32,7 @@
     }
 
     public WindowManagerTraceMonitor(Path outputDir) {
-        super(outputDir, "wm_trace.pb");
+        super(outputDir, TRACE_FILE);
     }
 
     @Override
diff --git a/libraries/flicker/test/assets/testdata/layers_trace_root.pb b/libraries/flicker/test/assets/testdata/layers_trace_root.pb
new file mode 100644
index 0000000..d961714
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/layers_trace_root.pb
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/layers_trace_root_aosp.pb b/libraries/flicker/test/assets/testdata/layers_trace_root_aosp.pb
new file mode 100644
index 0000000..666b328
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/layers_trace_root_aosp.pb
Binary files differ
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/AssertionsCheckerTest.java b/libraries/flicker/test/src/com/android/server/wm/flicker/AssertionsCheckerTest.java
index 4c6b229..6974d60 100644
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/AssertionsCheckerTest.java
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/AssertionsCheckerTest.java
@@ -163,6 +163,22 @@
         assertThat(failures).hasSize(1);
     }
 
+    @Test
+    public void canFailCheckChangingAssertions_ifUsingCompoundAssertion() {
+        AssertionsChecker<SimpleEntry> checker = new AssertionsChecker<>();
+        checker.add(SimpleEntry::isData42, "isData42");
+        checker.append(SimpleEntry::isData0, "isData0");
+        checker.checkChangingAssertions();
+
+        List<Result> failures = checker.test(getTestEntries(0, 0, 0, 0, 0));
+
+        assertThat(failures).hasSize(1);
+        assertThat(failures.get(0).assertionName).contains("isData42");
+        assertThat(failures.get(0).assertionName).contains("isData0");
+        assertThat(failures.get(0).reason).contains("!is42");
+        assertThat(failures.get(0).reason).doesNotContain("!is0");
+    }
+
     static class SimpleEntry implements ITraceEntry {
         long mTimestamp;
         int mData;
@@ -178,11 +194,11 @@
         }
 
         Result isData42() {
-            return new Result(this.mData == 42, this.mTimestamp, "is42", "");
+            return new Result(this.mData == 42, this.mTimestamp, "is42", "!is42");
         }
 
         Result isData0() {
-            return new Result(this.mData == 0, this.mTimestamp, "is42", "");
+            return new Result(this.mData == 0, this.mTimestamp, "is42", "!is0");
         }
     }
 }
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/LayersTraceSubjectTest.java b/libraries/flicker/test/src/com/android/server/wm/flicker/LayersTraceSubjectTest.java
index 0415f81..2ccae42 100644
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/LayersTraceSubjectTest.java
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/LayersTraceSubjectTest.java
@@ -33,6 +33,8 @@
 import org.junit.runners.MethodSorters;
 
 import java.nio.file.Paths;
+import java.util.List;
+import java.util.stream.Collectors;
 
 /**
  * Contains {@link LayersTraceSubject} tests. To run this test: {@code atest
@@ -89,7 +91,9 @@
             assertWithMessage("Contains path to trace")
                     .that(e.getMessage())
                     .contains("layers_trace_invalid_layer_visibility.pb");
-            assertWithMessage("Contains timestamp").that(e.getMessage()).contains("70h13m14s303ms");
+            assertWithMessage("Contains timestamp")
+                    .that(e.getMessage())
+                    .contains("2d22h13m14s303ms");
             assertWithMessage("Contains assertion function")
                     .that(e.getMessage())
                     .contains("!isVisible");
@@ -100,4 +104,37 @@
                                     + ".SimpleActivity#0 is visible");
         }
     }
+
+    private void detectRootLayer(String fileName) {
+        LayersTrace layersTrace = readLayerTraceFromFile(fileName);
+
+        for (LayersTrace.Entry entry : layersTrace.getEntries()) {
+            List<LayersTrace.Layer> flattened = entry.asFlattenedLayers();
+            List<LayersTrace.Layer> rootLayers =
+                    flattened
+                            .stream()
+                            .filter(LayersTrace.Layer::isRootLayer)
+                            .collect(Collectors.toList());
+
+            assertWithMessage("Does not have any root layer")
+                    .that(rootLayers.size())
+                    .isGreaterThan(0);
+
+            int firstParentId = rootLayers.get(0).getParentId();
+
+            assertWithMessage("Has multiple root layers")
+                    .that(rootLayers.stream().allMatch(p -> p.getParentId() == firstParentId))
+                    .isTrue();
+        }
+    }
+
+    @Test
+    public void testCanDetectRootLayer() {
+        detectRootLayer("layers_trace_root.pb");
+    }
+
+    @Test
+    public void testCanDetectRootLayerAOSP() {
+        detectRootLayer("layers_trace_root_aosp.pb");
+    }
 }
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/TransitionRunnerTest.java b/libraries/flicker/test/src/com/android/server/wm/flicker/TransitionRunnerTest.java
index d75c58a..10562d9 100644
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/TransitionRunnerTest.java
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/TransitionRunnerTest.java
@@ -45,7 +45,6 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-import java.io.IOException;
 import java.nio.file.Paths;
 import java.util.List;
 
@@ -58,7 +57,7 @@
     @Mock private WindowManagerTraceMonitor mWindowManagerTraceMonitorMock;
     @Mock private LayersTraceMonitor mLayersTraceMonitorMock;
     @Mock private WindowAnimationFrameStatsMonitor mWindowAnimationFrameStatsMonitor;
-    @InjectMocks private TransitionBuilder mTransitionBuilder;
+    @InjectMocks private TransitionBuilder mTransitionBuilder = TransitionRunner.newBuilder();
 
     @Before
     public void init() {
@@ -165,6 +164,7 @@
         orderVerifier
                 .verify(mWindowManagerTraceMonitorMock)
                 .save("mCaptureWmTraceTransitionRunner", 0);
+        orderVerifier.verify(mWindowManagerTraceMonitorMock).getChecksum();
         verifyNoMoreInteractions(mWindowManagerTraceMonitorMock);
     }
 
@@ -183,6 +183,7 @@
         orderVerifier
                 .verify(mLayersTraceMonitorMock)
                 .save("mCaptureLayersTraceTransitionRunner", 0);
+        orderVerifier.verify(mLayersTraceMonitorMock).getChecksum();
         verifyNoMoreInteractions(mLayersTraceMonitorMock);
     }
 
@@ -202,14 +203,16 @@
         orderVerifier.verify(mScreenRecorderMock).start();
         orderVerifier.verify(mScreenRecorderMock).stop();
         orderVerifier.verify(mScreenRecorderMock).save("mRecordEachRun", 0);
+        orderVerifier.verify(mScreenRecorderMock).getChecksum();
         orderVerifier.verify(mScreenRecorderMock).start();
         orderVerifier.verify(mScreenRecorderMock).stop();
         orderVerifier.verify(mScreenRecorderMock).save("mRecordEachRun", 1);
+        orderVerifier.verify(mScreenRecorderMock).getChecksum();
         verifyNoMoreInteractions(mScreenRecorderMock);
     }
 
     @Test
-    public void canRecordAllRuns() throws IOException {
+    public void canRecordAllRuns() {
         doReturn(
                         Paths.get(
                                 Environment.getExternalStorageDirectory().getAbsolutePath(),
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/LayersTraceMonitorTest.java b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/LayersTraceMonitorTest.java
index f349217..849e593 100644
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/LayersTraceMonitorTest.java
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/LayersTraceMonitorTest.java
@@ -35,6 +35,7 @@
 import org.junit.runners.MethodSorters;
 
 import java.io.File;
+import java.nio.file.Path;
 
 /**
  * Contains {@link LayersTraceMonitor} tests. To run this test: {@code atest
@@ -74,8 +75,11 @@
     public void captureLayersTrace() throws Exception {
         mLayersTraceMonitor.start();
         mLayersTraceMonitor.stop();
-        File testFile = mLayersTraceMonitor.save("captureLayersTrace").toFile();
+        Path testFilePath = mLayersTraceMonitor.save("captureWindowTrace");
+        File testFile = testFilePath.toFile();
         assertThat(testFile.exists()).isTrue();
+        String calculatedChecksum = TransitionMonitor.calculateChecksum(testFilePath);
+        assertThat(calculatedChecksum).isEqualTo(mLayersTraceMonitor.getChecksum());
         byte[] trace = Files.toByteArray(testFile);
         assertThat(trace.length).isGreaterThan(0);
         LayersTraceFileProto mLayerTraceFileProto = LayersTraceFileProto.parseFrom(trace);
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/ScreenRecorderTest.java b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/ScreenRecorderTest.java
index 967f565..603590e 100644
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/ScreenRecorderTest.java
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/ScreenRecorderTest.java
@@ -18,9 +18,6 @@
 
 import static android.os.SystemClock.sleep;
 
-import static com.android.server.wm.flicker.monitor.ScreenRecorder.DEFAULT_OUTPUT_PATH;
-import static com.android.server.wm.flicker.monitor.ScreenRecorder.getPath;
-
 import static com.google.common.truth.Truth.assertThat;
 
 import androidx.test.runner.AndroidJUnit4;
@@ -33,6 +30,8 @@
 import org.junit.runners.MethodSorters;
 
 import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
 
 /**
  * Contains {@link ScreenRecorder} tests. To run this test: {@code atest
@@ -43,6 +42,7 @@
 public class ScreenRecorderTest {
     private static final String TEST_VIDEO_FILENAME = "test.mp4";
     private ScreenRecorder mScreenRecorder;
+    private Path mSavedVideoPath = null;
 
     @Before
     public void setup() {
@@ -51,8 +51,10 @@
 
     @After
     public void teardown() {
-        DEFAULT_OUTPUT_PATH.toFile().delete();
-        getPath(TEST_VIDEO_FILENAME).toFile().delete();
+        mScreenRecorder.getPath().toFile().delete();
+        if (mSavedVideoPath != null) {
+            mSavedVideoPath.toFile().delete();
+        }
     }
 
     @Test
@@ -60,7 +62,7 @@
         mScreenRecorder.start();
         sleep(100);
         mScreenRecorder.stop();
-        File file = DEFAULT_OUTPUT_PATH.toFile();
+        File file = mScreenRecorder.getPath().toFile();
         assertThat(file.exists()).isTrue();
     }
 
@@ -69,8 +71,7 @@
         mScreenRecorder.start();
         sleep(100);
         mScreenRecorder.stop();
-        mScreenRecorder.save(TEST_VIDEO_FILENAME);
-        File file = getPath(TEST_VIDEO_FILENAME).toFile();
-        assertThat(file.exists()).isTrue();
+        Path file = mScreenRecorder.save(TEST_VIDEO_FILENAME);
+        assertThat(Files.exists(file)).isTrue();
     }
 }
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/WindowManagerTraceMonitorTest.java b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/WindowManagerTraceMonitorTest.java
index 034de4c..b234703 100644
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/WindowManagerTraceMonitorTest.java
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/WindowManagerTraceMonitorTest.java
@@ -35,6 +35,7 @@
 import org.junit.runners.MethodSorters;
 
 import java.io.File;
+import java.nio.file.Path;
 
 /**
  * Contains {@link WindowManagerTraceMonitor} tests. To run this test: {@code atest
@@ -74,8 +75,11 @@
     public void captureWindowTrace() throws Exception {
         mWindowManagerTraceMonitor.start();
         mWindowManagerTraceMonitor.stop();
-        File testFile = mWindowManagerTraceMonitor.save("captureWindowTrace").toFile();
+        Path testFilePath = mWindowManagerTraceMonitor.save("captureWindowTrace");
+        File testFile = testFilePath.toFile();
         assertThat(testFile.exists()).isTrue();
+        String calculatedChecksum = TransitionMonitor.calculateChecksum(testFilePath);
+        assertThat(calculatedChecksum).isEqualTo(mWindowManagerTraceMonitor.getChecksum());
         byte[] trace = Files.toByteArray(testFile);
         assertThat(trace.length).isGreaterThan(0);
         WindowManagerTraceFileProto mWindowTraceFileProto =
diff --git a/libraries/health/rules/src/android/platform/test/rule/DropCachesRule.java b/libraries/health/rules/src/android/platform/test/rule/DropCachesRule.java
index 44f0e1c..b5a6bf7 100644
--- a/libraries/health/rules/src/android/platform/test/rule/DropCachesRule.java
+++ b/libraries/health/rules/src/android/platform/test/rule/DropCachesRule.java
@@ -15,9 +15,16 @@
  */
 package android.platform.test.rule;
 
+import android.support.test.uiautomator.UiDevice;
 import androidx.annotation.VisibleForTesting;
+import androidx.test.platform.app.InstrumentationRegistry;
 import org.junit.runner.Description;
 
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+
 import android.os.SystemClock;
 import android.util.Log;
 
@@ -30,6 +37,39 @@
     @VisibleForTesting static final String KEY_DROP_CACHE = "drop-cache";
     private static boolean mDropCache = true;
 
+    /**
+     * Shell equivalent of $(echo 3 > /proc/sys/vm/drop_caches)
+     *
+     * Clears out the system pagecache for files and inodes metadata.
+     */
+    public static void executeDropCaches() {
+        // Create a temporary file which contains the dropCaches command.
+        // Do this because we cannot write to /proc/sys/vm/drop_caches directly,
+        // as executeShellCommand parses the '>' character as a literal.
+        try {
+            File outputDir =
+                    InstrumentationRegistry.getInstrumentation().getContext().getCacheDir();
+            File outputFile = File.createTempFile("drop_cache_script", "sh", outputDir);
+            outputFile.setWritable(true);
+            outputFile.setExecutable(true, /*ownersOnly*/false);
+
+            String outputFilePath = outputFile.toString();
+
+            // If this works correctly, the next log-line will print 'Success'.
+            String str = "echo 3 > /proc/sys/vm/drop_caches && echo Success || echo Failure";
+            BufferedWriter writer = new BufferedWriter(new FileWriter(outputFilePath));
+            writer.write(str);
+            writer.close();
+
+            UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+            String result = device.executeShellCommand(outputFilePath);
+            Log.v(LOG_TAG, "dropCaches output was: " + result);
+            outputFile.delete();
+        } catch (IOException e) {
+            throw new AssertionError (e);
+        }
+    }
+
     @Override
     protected void starting(Description description) {
         // Identify the filter option to use.
@@ -38,7 +78,7 @@
             return;
         }
 
-        executeShellCommand("echo 3 > /proc/sys/vm/drop_caches");
+        executeDropCaches();
         // TODO: b/117868612 to identify the root cause for additional wait.
         SystemClock.sleep(3000);
     }
diff --git a/libraries/health/rules/src/android/platform/test/rule/IorapCompilationRule.java b/libraries/health/rules/src/android/platform/test/rule/IorapCompilationRule.java
new file mode 100644
index 0000000..87a7289
--- /dev/null
+++ b/libraries/health/rules/src/android/platform/test/rule/IorapCompilationRule.java
@@ -0,0 +1,289 @@
+/*
+ * Copyright (C) 2020 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.platform.test.rule;
+
+import android.os.SystemClock;
+import android.platform.test.rule.DropCachesRule;
+import android.util.Log;
+import androidx.annotation.VisibleForTesting;
+
+import org.junit.runner.Description;
+import org.junit.runners.model.InitializationError;
+
+/** This rule toggles iorap compilation for an app, or skips if unspecified. */
+public class IorapCompilationRule extends TestWatcher {
+    //
+    private static final String TAG = IorapCompilationRule.class.getSimpleName();
+    // constants
+    @VisibleForTesting static final String ARGUMENT_IORAPD_ENABLED = "iorapd-enabled";
+
+    @VisibleForTesting
+    static final String IORAP_COMPILE_CMD = "dumpsys iorapd --compile-package %s";
+    @VisibleForTesting
+    static final String IORAP_MAINTENANCE_CMD = "dumpsys iorapd --purge-package %s";
+    @VisibleForTesting
+    static final String IORAP_DUMPSYS_CMD = "dumpsys iorapd";
+
+    private static final int IORAP_COMPILE_CMD_TIMEOUT_SEC = 60;  // in seconds: 1 minutes
+    private static final int IORAP_COMPILE_MIN_TRACES = 1;  // configure iorapd to need 1 trace.
+    private static final int IORAP_COMPILE_RETRIES = 3;  // retry compiler 3 times if it fails.
+    private static final int IORAP_TRACE_DURATION_TIMEOUT = 7000; // Allow 7s for trace to complete.
+    private static final int IORAP_TRIAL_LAUNCH_ITERATIONS = 3;  // min 3 launches to merge traces.
+
+    // Global static counter. Each junit instrument command must launch 1 package with
+    // 1 compiler filter.
+    private static int sIterationCounter = 0;
+    private String mApplication;
+
+    private enum IorapStatus {
+        UNDEFINED,
+        ENABLED,
+        DISABLED
+    }
+    private static IorapStatus sIorapStatus = IorapStatus.UNDEFINED;
+
+    private enum IorapCompilationStatus {
+        INCOMPLETE,
+        COMPLETE,
+        INSUFFICIENT_TRACES,
+    }
+
+    @VisibleForTesting
+    protected static void resetState() {
+        sIorapStatus = IorapStatus.UNDEFINED;
+        sIterationCounter = 0;
+        Log.v(TAG, "resetState");
+    }
+
+    public IorapCompilationRule() throws InitializationError {
+        throw new InitializationError("Must supply an application to enable iorapd for.");
+    }
+
+    public IorapCompilationRule(String application) {
+        mApplication = application;
+    }
+
+    protected void sleep(int ms) {
+        SystemClock.sleep(ms);
+    }
+
+    // [[ $(adb shell whoami) == "root" ]]
+    protected boolean checkIfRoot() {
+        String result = executeShellCommand("whoami");
+        return result.contains("root");
+    }
+
+    // Delete all db rows and files associated with a package in iorapd.
+    // Effectively deletes any raw or compiled trace files, unoptimizing the package in iorap.
+    private void purgeIorapPackage(String packageName) {
+        if (!checkIfRoot()) {
+            throw new IllegalStateException("Must be root to toggle iorapd; try adb root?");
+        }
+
+        Log.v(TAG, "Purge iorap package: " + packageName);
+        executeShellCommand(String.format(IORAP_MAINTENANCE_CMD, packageName));
+        Log.v(TAG, "Executed: " + String.format(IORAP_MAINTENANCE_CMD, packageName));
+    }
+
+    /**
+     * Toggle iorapd-based readahead and trace-collection.
+     * If iorapd is already enabled and enable is true, does nothing.
+     * If iorapd is already disabled and enable is false, does nothing.
+     */
+    private void toggleIorapStatus(boolean enable) {
+        boolean currentlyEnabled = false;
+        Log.v(TAG, "toggleIorapStatus " + Boolean.toString(enable));
+
+        // Do nothing if we are already enabled or disabled.
+        if (sIorapStatus == IorapStatus.ENABLED && enable) {
+            return;
+        } else if (sIorapStatus == IorapStatus.DISABLED && !enable) {
+            return;
+        }
+
+        if (!checkIfRoot()) {
+            throw new IllegalStateException("Must be root to toggle iorapd; try adb root?");
+        }
+
+        executeShellCommand(String.format("setprop iorapd.perfetto.enable %b", enable));
+        executeShellCommand(String.format("setprop iorapd.readahead.enable %b", enable));
+        executeShellCommand(String.format(
+                "setprop iorapd.maintenance.min_traces %d", IORAP_COMPILE_MIN_TRACES));
+        executeShellCommand(String.format("dumpsys iorapd --refresh-properties"));
+
+        if (enable) {
+            sIorapStatus = IorapStatus.ENABLED;
+        } else {
+            sIorapStatus = IorapStatus.DISABLED;
+        }
+    }
+
+    /**
+     * Compile the app package using compilerFilter,
+     * retrying if the compilation command fails in between.
+     */
+    private void compileAppForIorapWithRetries(String appPkgName, int retries) {
+        for (int i = 0; i < retries; ++i) {
+            if (compileAppForIorap(appPkgName)) {
+                return;
+            }
+            sleep(1000);
+        }
+
+        throw new IllegalStateException("compileAppForIorapWithRetries: timed out after "
+                + retries + " retries");
+    }
+
+    /**
+     * Compile the app package using iorap.cmd.maintenance and return false
+     * if the compilation failed for some reason.
+     */
+    private boolean compileAppForIorap(String appPkgName) {
+        executeShellCommand(String.format(IORAP_COMPILE_CMD, appPkgName));
+
+        int i = 0;
+        for (i = 0; i < IORAP_COMPILE_CMD_TIMEOUT_SEC; ++i) {
+            IorapCompilationStatus status = waitForIorapCompiled(appPkgName);
+            if (status == IorapCompilationStatus.COMPLETE) {
+                Log.v(TAG, "compileAppForIorap: success");
+                break;
+            } else if (status == IorapCompilationStatus.INSUFFICIENT_TRACES) {
+                Log.e(TAG, "compileAppForIorap: failed due to insufficient traces");
+                throw new IllegalStateException(
+                        "compileAppForIorap: failed due to insufficient traces");
+            } // else INCOMPLETE. keep asking iorapd if it's done yet.
+            sleep(1000);
+        }
+
+        if (i == IORAP_COMPILE_CMD_TIMEOUT_SEC) {
+            Log.e(TAG, "compileAppForIorap: failed due to timeout");
+            return false;
+        }
+
+        return true;
+    }
+
+    private IorapCompilationStatus waitForIorapCompiled(String appPkgName) {
+        String output = executeShellCommand(IORAP_DUMPSYS_CMD);
+
+        String prevLine = "";
+        for (String line : output.split("\n")) {
+            // Match the indented VersionedComponentName string.
+            // "  com.google.android.deskclock/com.android.deskclock.DeskClock@62000712"
+            // Note: spaces are meaningful here.
+            if (prevLine.contains("  " + appPkgName) && prevLine.contains("@")) {
+                // pre-requisite:
+                // Compiled Status: Raw traces pending compilation (3)
+                if (line.contains("Compiled Status: Usable compiled trace")) {
+                    return IorapCompilationStatus.COMPLETE;
+                } else if (line.contains("Compiled Status: ") &&
+                        line.contains("more traces for compilation")) {
+                    //      Compiled Status: Need 1 more traces for compilation
+                    // No amount of waiting will help here because there were
+                    // insufficient traces made.
+                    return IorapCompilationStatus.INSUFFICIENT_TRACES;
+                }
+            }
+            prevLine = line;
+        }
+        return IorapCompilationStatus.INCOMPLETE;
+    }
+
+    /**
+     * The first {@code IORAP_TRIAL_LAUNCH_ITERATIONS} are used for collecting an iorap trace file.
+     */
+    private boolean isIorapTraceBeingCollected() {
+        return sIterationCounter < IORAP_TRIAL_LAUNCH_ITERATIONS;
+    }
+
+    private boolean isLastIorapTraceCollection() {
+        return sIterationCounter == IORAP_TRIAL_LAUNCH_ITERATIONS - 1;
+    }
+
+    private boolean isFirstIorapTraceCollection() {
+        return sIterationCounter == 0;
+    }
+
+    /**
+     * Returns null if iorapd-enabled is unset, otherwise returns
+     * the true/false value of iorapd-enabled.
+     */
+    private Boolean isIorapdEnabled() {
+        String value = getArguments().getString(ARGUMENT_IORAPD_ENABLED);
+        if (value == null) {
+            return null;
+        }
+        return Boolean.parseBoolean(value);
+    }
+
+    @Override
+    protected void starting(Description description) {
+        // Don't do anything if iorapd-enabled was not set.
+        Boolean enabled = isIorapdEnabled();
+        if (enabled == null) {
+            Log.d(TAG, "Skipping iorapd toggling because 'iorapd-enabled' option is unset.");
+            return;
+        }
+        logStatus("starting");
+        // Compile each application in sequence.
+        String app = mApplication;
+
+        toggleIorapStatus(enabled);
+
+        if (!enabled) {
+            return;
+        }
+
+        if (isIorapTraceBeingCollected()) {
+            // Purge all iorap traces prior to first run of an application.
+            if (isFirstIorapTraceCollection()) {
+                purgeIorapPackage(mApplication);
+            }
+
+            // We must always drop caches to simulate a cold start if the app
+            // launch is going to be used for an iorap-trace collection.
+            DropCachesRule.executeDropCaches();
+        }
+    }
+
+    @Override
+    protected void finished(Description description) {
+        logStatus("finishing");
+
+        Boolean enabled = isIorapdEnabled();
+
+        if (Boolean.TRUE.equals(enabled) && isIorapTraceBeingCollected()) {
+            // wait for slightly more than 5s (iorapd.perfetto.trace_duration_ms) for the
+            // trace buffers to complete.
+            sleep(IORAP_TRACE_DURATION_TIMEOUT);
+
+            if (isLastIorapTraceCollection()) {
+                // run the iorap compiler and wait for iorap to compile fully.
+                // this throws an exception if it fails.
+                compileAppForIorapWithRetries(mApplication, IORAP_COMPILE_RETRIES);
+            }
+        }
+
+        logStatus("finished");
+        sIterationCounter++;
+    }
+
+    private void logStatus(String status) {
+        Log.v(TAG, String.format("%s iteration %s for app %s", status, sIterationCounter, mApplication));
+    }
+}
+
diff --git a/libraries/health/rules/tests/src/android/platform/test/rule/IorapCompilationRuleTest.java b/libraries/health/rules/tests/src/android/platform/test/rule/IorapCompilationRuleTest.java
new file mode 100644
index 0000000..bffd026
--- /dev/null
+++ b/libraries/health/rules/tests/src/android/platform/test/rule/IorapCompilationRuleTest.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2020 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.platform.test.rule;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import android.os.Bundle;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.Description;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.junit.runners.model.InitializationError;
+import org.junit.runners.model.Statement;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Unit test the logic for {@link IorapCompilationRule}
+ */
+@RunWith(JUnit4.class)
+public class IorapCompilationRuleTest {
+    /**
+     * Tests that this rule will fail to register if no apps are supplied.
+     */
+    @Test
+    public void testNoAppToKillFails() {
+        try {
+            IorapCompilationRule rule = new IorapCompilationRule();
+            fail("An initialization error should have been thrown, but wasn't.");
+        } catch (InitializationError e) {
+            return;
+        }
+    }
+
+    /**
+     * Tests that this rule does nothing when 'iorapd-enabled' is unset.
+     */
+    @Test
+    public void testDoingNothingWhenParamsUnset() throws Throwable {
+        TestableIorapCompilationRule rule =
+                new TestableIorapCompilationRule(new Bundle(), "example.package.name");
+        rule.apply(rule.getTestStatement(), Description.createTestDescription("clzz", "mthd"))
+                .evaluate();
+        assertThat(rule.getOperations()).containsExactly(
+                "test")
+                .inOrder();
+    }
+
+    /**
+     * Tests that this rule will disable iorapd when 'iorapd-enabled' is false.
+     */
+    @Test
+    public void testDisablingIorapdWhenParamsAreSet() throws Throwable {
+        Bundle bundle = new Bundle();
+        bundle.putString(IorapCompilationRule.ARGUMENT_IORAPD_ENABLED, "false");
+        TestableIorapCompilationRule rule =
+                new TestableIorapCompilationRule(bundle, "example.package.name");
+        rule.apply(rule.getTestStatement(), Description.createTestDescription("clzz", "mthd"))
+                .evaluate();
+        assertThat(rule.getOperations()).containsExactly(
+                "setprop iorapd.perfetto.enable false",
+                "setprop iorapd.readahead.enable false",
+                "setprop iorapd.maintenance.min_traces 1",
+                "dumpsys iorapd --refresh-properties",
+                "test")
+                .inOrder();
+    }
+
+    /**
+     * Tests that this rule will enable iorapd when 'iorapd-enabled' is true.
+     */
+    @Test
+    public void testEnablingIorapdWhenParamsAreSet() throws Throwable {
+        Bundle bundle = new Bundle();
+        bundle.putString(IorapCompilationRule.ARGUMENT_IORAPD_ENABLED, "true");
+        TestableIorapCompilationRule rule =
+                new TestableIorapCompilationRule(bundle, "example.package.name");
+        rule.apply(rule.getTestStatement(), Description.createTestDescription("clzz", "mthd"))
+                .evaluate();
+        assertThat(rule.getOperations()).containsExactly(
+                "setprop iorapd.perfetto.enable true",
+                "setprop iorapd.readahead.enable true",
+                "setprop iorapd.maintenance.min_traces 1",
+                "dumpsys iorapd --refresh-properties",
+                String.format(IorapCompilationRule.IORAP_MAINTENANCE_CMD, "example.package.name"),
+                "test")
+                .inOrder();
+    }
+
+    /**
+     * Tests that this rule will enable iorapd when 'iorapd-enabled' is true.
+     */
+    @Test
+    public void testCompilingIorapdWhenParamsAreSet() throws Throwable {
+        Bundle bundle = new Bundle();
+        bundle.putString(IorapCompilationRule.ARGUMENT_IORAPD_ENABLED, "true");
+        TestableIorapCompilationRule rule =
+                new TestableIorapCompilationRule(bundle, "example.package.name");
+        rule.apply(rule.getTestStatement(), Description.createTestDescription("clzz", "mthd"))
+                .evaluate();
+        // The first iteration turns on iorapd and will trace the app.
+        assertThat(rule.getOperations()).containsExactly(
+                "setprop iorapd.perfetto.enable true",
+                "setprop iorapd.readahead.enable true",
+                "setprop iorapd.maintenance.min_traces 1",
+                "dumpsys iorapd --refresh-properties",
+                String.format(IorapCompilationRule.IORAP_MAINTENANCE_CMD, "example.package.name"),
+                "test")
+                .inOrder();
+
+        // We do nothing special for the second iteration, iorapd will be tracing.
+        TestableIorapCompilationRule rule2 =
+                new TestableIorapCompilationRule(bundle, "example.package.name");
+        rule2.apply(rule2.getTestStatement(), Description.createTestDescription("clzz", "mthd2"))
+                .evaluate();
+        assertThat(rule2.getOperations()).containsExactly(
+                "test")
+                .inOrder();
+
+        // On the 3rd iteration, we iorap compile the package after the test method finishes.
+        TestableIorapCompilationRule rule3 =
+                new TestableIorapCompilationRule(bundle, "example.package.name");
+        rule3.apply(rule3.getTestStatement(), Description.createTestDescription("clzz", "mthd3"))
+                .evaluate();
+        assertThat(rule3.getOperations()).containsExactly(
+                "test",
+                String.format(IorapCompilationRule.IORAP_COMPILE_CMD, "example.package.name"),
+                IorapCompilationRule.IORAP_DUMPSYS_CMD)
+                .inOrder();
+
+        // On the 4th and later iteration, we do nothing.
+        TestableIorapCompilationRule rule4 =
+                new TestableIorapCompilationRule(bundle, "example.package.name");
+        rule4.apply(rule4.getTestStatement(), Description.createTestDescription("clzz", "mthd4"))
+                .evaluate();
+        assertThat(rule4.getOperations()).containsExactly(
+                "test")
+                .inOrder();
+    }
+
+    @Before
+    public void resetRuleState() {
+        TestableIorapCompilationRule.resetState();
+    }
+
+    private static class TestableIorapCompilationRule extends IorapCompilationRule {
+        private List<String> mOperations = new ArrayList<>();
+        private Bundle mBundle;
+
+        public TestableIorapCompilationRule(Bundle bundle, String app) {
+            super(app);
+            mBundle = bundle;
+        }
+
+        @Override
+        protected String executeShellCommand(String cmd) {
+            mOperations.add(cmd);
+
+            if (cmd.equals(IorapCompilationRule.IORAP_DUMPSYS_CMD)) {
+                return "  example.package.name/com.android.example.Activity@62000712" +
+                    "\n    Compiled Status: Usable compiled trace";
+            }
+
+            return "";
+        }
+
+        @Override
+        protected Bundle getArguments() {
+            return mBundle;
+        }
+
+        public List<String> getOperations() {
+            return mOperations;
+        }
+
+        public Statement getTestStatement() {
+            return new Statement() {
+                @Override
+                public void evaluate() throws Throwable {
+                    mOperations.add("test");
+                }
+            };
+        }
+
+        public static void resetState() {
+            IorapCompilationRule.resetState();
+        }
+
+        @Override
+        protected boolean checkIfRoot() {
+            return true;
+        }
+
+        @Override
+        protected void sleep(int ms) {
+            // Intentionally left empty. The tests don't need to sleep.
+        }
+    }
+}
+
diff --git a/libraries/health/runners/longevity/platform/samples/assets/sample_indexed_profile.textpb b/libraries/health/runners/longevity/platform/samples/assets/sample_indexed_profile.textpb
new file mode 100644
index 0000000..4a4ade4
--- /dev/null
+++ b/libraries/health/runners/longevity/platform/samples/assets/sample_indexed_profile.textpb
@@ -0,0 +1,17 @@
+schedule: INDEXED
+scenarios [{
+    index: 1
+    journey: "android.platform.test.longevity.samples.SimpleSuite$PassingTest"
+}, {
+    index: 2
+    journey: "android.platform.test.longevity.samples.SimpleSuite$FailingTest"
+}, {
+    index: 3
+    journey: "android.platform.test.longevity.samples.SimpleSuite$PassingTest"
+}, {
+    index: 4
+    journey: "android.platform.test.longevity.samples.SimpleSuite$PassingTest"
+}, {
+    index: 5
+    journey: "android.platform.test.longevity.samples.SimpleSuite$FailingTest"
+}]
diff --git a/libraries/health/runners/longevity/platform/samples/assets/sample_profile.textpb b/libraries/health/runners/longevity/platform/samples/assets/sample_scheduled_profile.textpb
similarity index 100%
rename from libraries/health/runners/longevity/platform/samples/assets/sample_profile.textpb
rename to libraries/health/runners/longevity/platform/samples/assets/sample_scheduled_profile.textpb
diff --git a/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/Profile.java b/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/Profile.java
index 4cb3933..b981276 100644
--- a/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/Profile.java
+++ b/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/Profile.java
@@ -79,6 +79,17 @@
         }
     }
 
+    // Comparator for sorting indexed CUJs.
+    private static class ScenarioIndexedComparator implements Comparator<Scenario> {
+        public int compare(Scenario s1, Scenario s2) {
+            if (!(s1.hasIndex() && s2.hasIndex())) {
+                throw new IllegalArgumentException(
+                        "Scenarios in indexed profiles must have indexes.");
+            }
+            return Integer.compare(s1.getIndex(), s2.getIndex());
+        }
+    }
+
     public Profile(Bundle args) {
         super();
         // Set the timestamp parser to UTC to get test timstamps as "time elapsed since zero".
@@ -105,6 +116,8 @@
                 throw new IllegalArgumentException(
                         "Cannot parse the timestamp of the first scenario.", e);
             }
+        } else if (mConfiguration.getSchedule().equals(Schedule.INDEXED)) {
+            Collections.sort(mOrderedScenariosList, new ScenarioIndexedComparator());
         } else {
             throw new UnsupportedOperationException(
                     "Only scheduled profiles are currently supported.");
diff --git a/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/ProfileSuite.java b/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/ProfileSuite.java
index 2baa274..d557780 100644
--- a/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/ProfileSuite.java
+++ b/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/ProfileSuite.java
@@ -147,6 +147,11 @@
                         mProfile.getCurrentScenario(),
                         timeout,
                         mProfile.hasNextScheduledScenario());
+
+            case INDEXED:
+                return getIndexedRunner(
+                        (BlockJUnit4ClassRunner) runner, mProfile.getCurrentScenario());
+
             default:
                 throw new RuntimeException(
                         String.format(
@@ -173,4 +178,19 @@
                     e);
         }
     }
+
+    /** Replace a runner with {@link ScenarioRunner} for features specific to indexed profiles. */
+    @VisibleForTesting
+    protected ScenarioRunner getIndexedRunner(BlockJUnit4ClassRunner runner, Scenario scenario) {
+        Class<?> testClass = runner.getTestClass().getJavaClass();
+        try {
+            return new ScenarioRunner(testClass, scenario);
+        } catch (InitializationError e) {
+            throw new RuntimeException(
+                    String.format(
+                            "Unable to run scenario %s with an indexed runner.",
+                            runner.getDescription().getDisplayName()),
+                    e);
+        }
+    }
 }
diff --git a/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/ScenarioRunner.java b/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/ScenarioRunner.java
new file mode 100644
index 0000000..33e4056
--- /dev/null
+++ b/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/ScenarioRunner.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2019 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.platform.test.longevity;
+
+import android.os.Bundle;
+import android.platform.test.longevity.proto.Configuration.Scenario;
+import android.platform.test.longevity.proto.Configuration.Scenario.ExtraArg;
+import androidx.annotation.VisibleForTesting;
+import androidx.test.InstrumentationRegistry;
+
+import org.junit.runner.notification.RunNotifier;
+import org.junit.runners.BlockJUnit4ClassRunner;
+import org.junit.runners.model.FrameworkMethod;
+import org.junit.runners.model.InitializationError;
+
+/** A {@link BlockJUnit4ClassRunner} that runs a test class with profile-specified options. */
+public class ScenarioRunner extends LongevityClassRunner {
+    private final Scenario mScenario;
+    private final Bundle mArguments;
+
+    public ScenarioRunner(Class<?> klass, Scenario scenario) throws InitializationError {
+        this(klass, scenario, InstrumentationRegistry.getArguments());
+    }
+
+    @VisibleForTesting
+    ScenarioRunner(Class<?> klass, Scenario scenario, Bundle arguments) throws InitializationError {
+        super(klass, arguments);
+        mScenario = scenario;
+        mArguments = arguments;
+    }
+
+    @Override
+    protected void runChild(final FrameworkMethod method, RunNotifier notifier) {
+        // Keep a copy of the bundle arguments for restoring later.
+        Bundle modifiedArguments = mArguments.deepCopy();
+        for (ExtraArg argPair : mScenario.getExtrasList()) {
+            if (argPair.getKey() == null || argPair.getValue() == null) {
+                throw new IllegalArgumentException(
+                        String.format(
+                                "Each extra arg entry in scenario must have both a key and a value,"
+                                        + " but scenario is %s.",
+                                mScenario.toString()));
+            }
+            modifiedArguments.putString(argPair.getKey(), argPair.getValue());
+        }
+        // Swap the arguments, run the scenario, and then restore arguments.
+        InstrumentationRegistry.registerInstance(
+                InstrumentationRegistry.getInstrumentation(), modifiedArguments);
+        super.runChild(method, notifier);
+        InstrumentationRegistry.registerInstance(
+                InstrumentationRegistry.getInstrumentation(), mArguments);
+    }
+}
diff --git a/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/ScheduledScenarioRunner.java b/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/ScheduledScenarioRunner.java
index 794b576..ffd54fe 100644
--- a/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/ScheduledScenarioRunner.java
+++ b/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/ScheduledScenarioRunner.java
@@ -47,6 +47,8 @@
 /**
  * A {@link BlockJUnit4ClassRunner} that runs a test class with a specified timeout and optionally
  * performs an idle before teardown (staying inside the app for Android CUJs).
+ *
+ * <p>TODO(b/146215435): Refactor to extends the index-based {@link ScenarioRunner}.
  */
 public class ScheduledScenarioRunner extends LongevityClassRunner {
     // A leeway to ensure that the teardown steps in @After and @AfterClass has time to finish.
@@ -55,7 +57,10 @@
     // Please note that in most cases (when the CUJ does not time out) the actual cushion for
     // teardown is double the value below, as a cushion needs to be created inside the timeout
     // rule and also outside of it.
-    @VisibleForTesting static final long TEARDOWN_LEEWAY_MS = 3000;
+    // This parameter is configurable via the command line as the teardown time varies across CUJs.
+    @VisibleForTesting static final String TEARDOWN_LEEWAY_OPTION = "teardown-window_ms";
+    @VisibleForTesting static final long TEARDOWN_LEEWAY_DEFAULT = 3000L;
+    private long mTeardownLeewayMs = TEARDOWN_LEEWAY_DEFAULT;
 
     private static final String LOG_TAG = ScheduledScenarioRunner.class.getSimpleName();
 
@@ -84,9 +89,13 @@
         mTotalTimeoutMs = max(timeout, 0);
         // Ensure that the enforced timeout is non-negative. This cushion is built in so that the
         // CUJ still has time for teardown steps when the test portion times out.
-        mEnforcedTimeoutMs = max(mTotalTimeoutMs - TEARDOWN_LEEWAY_MS, 0);
+        mEnforcedTimeoutMs = max(mTotalTimeoutMs - mTeardownLeewayMs, 0);
         mShouldIdle = shouldIdle;
         mArguments = arguments;
+        mTeardownLeewayMs =
+                Long.parseLong(
+                        arguments.getString(
+                                TEARDOWN_LEEWAY_OPTION, String.valueOf(mTeardownLeewayMs)));
     }
 
     @Override
@@ -107,9 +116,9 @@
                             // Run the underlying test and report exceptions.
                             statement.evaluate();
                         } finally {
-                            // If there is time left for idling (i.e. more than TEARDOWN_LEEWAY_MS),
+                            // If there is time left for idling (i.e. more than mTeardownLeewayMs),
                             // and the scenario is set to stay in app, idle for the remainder of
-                            // its timeout window until TEARDOWN_LEEWAY_MS before the start time of
+                            // its timeout window until mTeardownLeewayMs before the start time of
                             // the next scenario, before executing the scenario's @After methods.
                             // The above does not apply if current scenario is the last one, in
                             // which case the idle is never performed regardless of its after_test
@@ -123,7 +132,7 @@
                                 performIdleBeforeTeardown(
                                         max(
                                                 getTimeRemainingForTimeoutRule()
-                                                        - TEARDOWN_LEEWAY_MS,
+                                                        - mTeardownLeewayMs,
                                                 0));
                             }
                         }
@@ -252,4 +261,10 @@
             context.unregisterReceiver(receiver);
         }
     }
+
+    /** Expose the teardown leeway since tests rely on it for verifying timing. */
+    @VisibleForTesting
+    long getTeardownLeeway() {
+        return mTeardownLeewayMs;
+    }
 }
diff --git a/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/profile.proto b/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/profile.proto
index 37bed59..d05fd5a 100644
--- a/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/profile.proto
+++ b/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/profile.proto
@@ -21,17 +21,17 @@
 
 message Configuration {
     // Schedule used to run the profile.
-    // TODO(b/122323704): Implement ordered profile.
     enum Schedule {
         TIMESTAMPED = 1;
+        INDEXED = 2;
     }
     optional Schedule schedule = 1 [default = TIMESTAMPED];
 
     // Information for each scenario.
     message Scenario {
         oneof schedule {
-            // Timestamp to run the scenario in HH:MM:SS.
-            string at = 1;
+            string at = 1; // A timestamp (HH:MM:SS) for when to run the scenario.
+            int32 index = 2; // An index for the relative order of the scenario.
         }
         // Reference to the CUJ (<package>.<class>).
         optional string journey = 3;
diff --git a/libraries/health/runners/longevity/platform/tests/assets/testIndexedScheduling_respectsSchedule.textpb b/libraries/health/runners/longevity/platform/tests/assets/testIndexedScheduling_respectsSchedule.textpb
new file mode 100644
index 0000000..6f67f1e
--- /dev/null
+++ b/libraries/health/runners/longevity/platform/tests/assets/testIndexedScheduling_respectsSchedule.textpb
@@ -0,0 +1,11 @@
+schedule: INDEXED
+scenarios [{
+    index: 1
+    journey: "android.platform.test.longevity.samples.testing.SampleBasicProfileSuite$PassingTest1"
+}, {
+    index: 2
+    journey: "android.platform.test.longevity.samples.testing.SampleBasicProfileSuite$PassingTest2"
+}, {
+    index: 3
+    journey: "android.platform.test.longevity.samples.testing.SampleBasicProfileSuite$PassingTest1"
+}]
diff --git a/libraries/health/runners/longevity/platform/tests/assets/testScheduling_respectsSchedule.textpb b/libraries/health/runners/longevity/platform/tests/assets/testTimestampScheduling_respectsSchedule.textpb
similarity index 78%
rename from libraries/health/runners/longevity/platform/tests/assets/testScheduling_respectsSchedule.textpb
rename to libraries/health/runners/longevity/platform/tests/assets/testTimestampScheduling_respectsSchedule.textpb
index 4c02f42..f68cc2f 100644
--- a/libraries/health/runners/longevity/platform/tests/assets/testScheduling_respectsSchedule.textpb
+++ b/libraries/health/runners/longevity/platform/tests/assets/testTimestampScheduling_respectsSchedule.textpb
@@ -1,10 +1,10 @@
 schedule: TIMESTAMPED
 scenarios [{
     at: "00:00:01"
-    journey: "android.platform.test.longevity.samples.testing.SampleProfileSuite$LongIdleTest"
+    journey: "android.platform.test.longevity.samples.testing.SampleTimedProfileSuite$LongIdleTest"
     after_test: STAY_IN_APP
 }, {
     at: "00:00:10"
-    journey: "android.platform.test.longevity.samples.testing.SampleProfileSuite$PassingTest"
+    journey: "android.platform.test.longevity.samples.testing.SampleTimedProfileSuite$PassingTest"
     after_test: STAY_IN_APP
 }]
diff --git a/libraries/health/runners/longevity/platform/tests/assets/testScheduling_respectsSuiteTimeout.textpb b/libraries/health/runners/longevity/platform/tests/assets/testTimestampScheduling_respectsSuiteTimeout.textpb
similarity index 77%
rename from libraries/health/runners/longevity/platform/tests/assets/testScheduling_respectsSuiteTimeout.textpb
rename to libraries/health/runners/longevity/platform/tests/assets/testTimestampScheduling_respectsSuiteTimeout.textpb
index 7c32fa7..06af14f 100644
--- a/libraries/health/runners/longevity/platform/tests/assets/testScheduling_respectsSuiteTimeout.textpb
+++ b/libraries/health/runners/longevity/platform/tests/assets/testTimestampScheduling_respectsSuiteTimeout.textpb
@@ -1,9 +1,9 @@
 schedule: TIMESTAMPED
 scenarios [{
     at: "00:00:01"
-    journey: "android.platform.test.longevity.samples.testing.SampleProfileSuite$PassingTest"
+    journey: "android.platform.test.longevity.samples.testing.SampleTimedProfileSuite$PassingTest"
 }, {
     at: "00:00:05"
-    journey: "android.platform.test.longevity.samples.testing.SampleProfileSuite$LongIdleTest"
+    journey: "android.platform.test.longevity.samples.testing.SampleTimedProfileSuite$LongIdleTest"
     after_test: STAY_IN_APP
 }]
diff --git a/libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/ProfileSuiteTest.java b/libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/ProfileSuiteTest.java
index d42ccd7..8d7237d 100644
--- a/libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/ProfileSuiteTest.java
+++ b/libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/ProfileSuiteTest.java
@@ -30,7 +30,8 @@
 import android.host.test.longevity.listener.TimeoutTerminator;
 import android.os.Bundle;
 import android.os.SystemClock;
-import android.platform.test.longevity.samples.testing.SampleProfileSuite;
+import android.platform.test.longevity.samples.testing.SampleBasicProfileSuite;
+import android.platform.test.longevity.samples.testing.SampleTimedProfileSuite;
 import android.platform.test.scenario.annotation.Scenario;
 
 import org.junit.Assert;
@@ -143,19 +144,21 @@
     @RunWith(Parameterized.class)
     public static class NotSupportedRunner extends BasicScenario {}
 
-    /** Test that a profile's scheduling is followed. */
+    /** Test that a timestamped profile's scheduling is followed. */
     @Test
-    public void testScheduling_respectsSchedule() throws InitializationError {
+    public void testTimestampScheduling_respectsSchedule() throws InitializationError {
         // TODO(harrytczhang@): Find a way to run this without relying on actual idles.
 
         // Arguments with the profile under test.
         Bundle args = new Bundle();
-        args.putString(Profile.PROFILE_OPTION_NAME, "testScheduling_respectsSchedule");
+        args.putString(Profile.PROFILE_OPTION_NAME, "testTimestampScheduling_respectsSchedule");
         // Scenario names from the profile.
         final String firstScenarioName =
-                "android.platform.test.longevity.samples.testing.SampleProfileSuite$LongIdleTest";
+                "android.platform.test.longevity.samples.testing."
+                        + "SampleTimedProfileSuite$LongIdleTest";
         final String secondScenarioName =
-                "android.platform.test.longevity.samples.testing.SampleProfileSuite$PassingTest";
+                "android.platform.test.longevity.samples.testing."
+                        + "SampleTimedProfileSuite$PassingTest";
         // Stores the start time of the test run for the suite. Using AtomicLong here as the time
         // should be initialized when run() is called on the suite, but Java does not want
         // assignment to local varaible in lambda expressions. AtomicLong allows for using the
@@ -164,7 +167,7 @@
         ProfileSuite suite =
                 spy(
                         new ProfileSuite(
-                                SampleProfileSuite.class,
+                                SampleTimedProfileSuite.class,
                                 new AllDefaultPossibilitiesBuilder(true),
                                 mInstrumentation,
                                 mContext,
@@ -242,22 +245,22 @@
                         argThat(notifier -> notifier.equals(mRunNotifier)));
     }
 
-    /** Test that a profile's last scenario is bounded by the suite timeout. */
+    /** Test that a timestamp profile's last scenario is bounded by the suite timeout. */
     @Test
-    public void testScheduling_respectsSuiteTimeout() throws InitializationError {
+    public void testTimestampScheduling_respectsSuiteTimeout() throws InitializationError {
         long suiteTimeoutMsecs = TimeUnit.SECONDS.toMillis(10);
         ArgumentCaptor<Failure> failureCaptor = ArgumentCaptor.forClass(Failure.class);
 
         // Arguments with the profile under test and suite timeout.
         Bundle args = new Bundle();
-        args.putString(Profile.PROFILE_OPTION_NAME, "testScheduling_respectsSuiteTimeout");
+        args.putString(Profile.PROFILE_OPTION_NAME, "testTimestampScheduling_respectsSuiteTimeout");
         args.putString(TimeoutTerminator.OPTION, String.valueOf(suiteTimeoutMsecs));
 
         // Construct and run the profile suite.
         ProfileSuite suite =
                 spy(
                         new ProfileSuite(
-                                SampleProfileSuite.class,
+                                SampleTimedProfileSuite.class,
                                 new AllDefaultPossibilitiesBuilder(true),
                                 mInstrumentation,
                                 mContext,
@@ -288,10 +291,71 @@
                                     long expectedTimeout =
                                             suiteTimeoutMsecs
                                                     - TimeUnit.SECONDS.toMillis(4)
-                                                    - ScheduledScenarioRunner.TEARDOWN_LEEWAY_MS;
+                                                    - ScheduledScenarioRunner
+                                                            .TEARDOWN_LEEWAY_DEFAULT;
                                     return abs(exceptionTimeout - expectedTimeout)
                                             <= SCHEDULE_LEEWAY_MS;
                                 });
         Assert.assertTrue(correctTestTimedOutExceptionFired);
     }
+
+    /** Test that an indexed profile's scheduling is followed. */
+    @Test
+    public void testIndexedScheduling_respectsSchedule() throws InitializationError {
+        // Arguments with the profile under test.
+        Bundle args = new Bundle();
+        args.putString(Profile.PROFILE_OPTION_NAME, "testIndexedScheduling_respectsSchedule");
+        // Scenario names from the profile.
+        final String firstScenarioName =
+                "android.platform.test.longevity.samples.testing."
+                        + "SampleBasicProfileSuite$PassingTest1";
+        final String secondScenarioName =
+                "android.platform.test.longevity.samples.testing."
+                        + "SampleBasicProfileSuite$PassingTest2";
+        final String thirdScenarioName =
+                "android.platform.test.longevity.samples.testing."
+                        + "SampleBasicProfileSuite$PassingTest1";
+        ProfileSuite suite =
+                spy(
+                        new ProfileSuite(
+                                SampleBasicProfileSuite.class,
+                                new AllDefaultPossibilitiesBuilder(true),
+                                mInstrumentation,
+                                mContext,
+                                args));
+
+        InOrder inOrderVerifier = inOrder(suite);
+
+        suite.run(mRunNotifier);
+        // Verify that the first scenario is started.
+        inOrderVerifier
+                .verify(suite)
+                .runChild(
+                        argThat(
+                                runner ->
+                                        runner.getDescription()
+                                                .getDisplayName()
+                                                .equals(firstScenarioName)),
+                        argThat(notifier -> notifier.equals(mRunNotifier)));
+        // Verify that the second scenario is started.
+        inOrderVerifier
+                .verify(suite)
+                .runChild(
+                        argThat(
+                                runner ->
+                                        runner.getDescription()
+                                                .getDisplayName()
+                                                .equals(secondScenarioName)),
+                        argThat(notifier -> notifier.equals(mRunNotifier)));
+        // Verify that the third scenario is started.
+        inOrderVerifier
+                .verify(suite)
+                .runChild(
+                        argThat(
+                                runner ->
+                                        runner.getDescription()
+                                                .getDisplayName()
+                                                .equals(thirdScenarioName)),
+                        argThat(notifier -> notifier.equals(mRunNotifier)));
+    }
 }
diff --git a/libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/ScenarioRunnerTest.java b/libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/ScenarioRunnerTest.java
new file mode 100644
index 0000000..a6f3c50
--- /dev/null
+++ b/libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/ScenarioRunnerTest.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2019 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.platform.test.longevity;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.os.Bundle;
+import android.platform.test.longevity.proto.Configuration.Scenario;
+import android.platform.test.longevity.proto.Configuration.Scenario.ExtraArg;
+import androidx.test.InstrumentationRegistry;
+
+import org.junit.Assert;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runner.notification.Failure;
+import org.junit.runner.notification.RunNotifier;
+import org.junit.runners.JUnit4;
+import org.junit.runners.model.InitializationError;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.exceptions.base.MockitoAssertionError;
+
+import java.util.HashSet;
+import java.util.List;
+
+/** Unit tests for the {@link ScenarioRunner} runner. */
+@RunWith(JUnit4.class)
+public class ScenarioRunnerTest {
+
+    @Mock private RunNotifier mRunNotifier;
+
+    private static final String ASSERTION_FAILURE_MESSAGE = "Test assertion failed";
+
+    public static class ArgumentTest {
+        public static final String TEST_ARG = "test-arg-test-only";
+        public static final String TEST_ARG_DEFAULT = "default";
+        public static final String TEST_ARG_OVERRIDE = "not default";
+
+        @Before
+        public void setUp() {
+            // The actual argument testing happens here as this is where instrumentation args are
+            // parsed in the CUJs.
+            String argValue =
+                    InstrumentationRegistry.getArguments().getString(TEST_ARG, TEST_ARG_DEFAULT);
+            Assert.assertEquals(ASSERTION_FAILURE_MESSAGE, argValue, TEST_ARG_OVERRIDE);
+        }
+
+        @Test
+        public void dummyTest() {
+            // Does nothing; always passes.
+        }
+    }
+
+    // Holds the state of the instrumentation args before each test for restoring after, as one test
+    // might affect the state of another otherwise.
+    // TODO(b/124239142): Avoid manipulating the instrumentation args here.
+    private Bundle mArgumentsBeforeTest;
+
+    @Before
+    public void setUpSuite() throws InitializationError {
+        initMocks(this);
+        mArgumentsBeforeTest = InstrumentationRegistry.getArguments();
+    }
+
+    @After
+    public void restoreSuite() {
+        InstrumentationRegistry.registerInstance(
+                InstrumentationRegistry.getInstrumentation(), mArgumentsBeforeTest);
+    }
+
+    /** Test that the "extras" in a scenario is properly registered before the test. */
+    @Test
+    public void testExtraArgs_registeredBeforeTest() throws Throwable {
+        Scenario testScenario =
+                Scenario.newBuilder()
+                        .setIndex(1)
+                        .setJourney(ArgumentTest.class.getName())
+                        .addExtras(
+                                ExtraArg.newBuilder()
+                                        .setKey(ArgumentTest.TEST_ARG)
+                                        .setValue(ArgumentTest.TEST_ARG_OVERRIDE))
+                        .build();
+        ScenarioRunner runner = spy(new ScenarioRunner(ArgumentTest.class, testScenario));
+        runner.run(mRunNotifier);
+        verifyForAssertionFailures(mRunNotifier);
+    }
+
+    /** Test that the "extras" in a scenario is properly un-registered after the test. */
+    @Test
+    public void testExtraArgs_unregisteredAfterTest() throws Throwable {
+        Bundle argsBeforeTest = InstrumentationRegistry.getArguments();
+        Scenario testScenario =
+                Scenario.newBuilder()
+                        .setIndex(1)
+                        .setJourney(ArgumentTest.class.getName())
+                        .addExtras(
+                                ExtraArg.newBuilder()
+                                        .setKey(ArgumentTest.TEST_ARG)
+                                        .setValue(ArgumentTest.TEST_ARG_OVERRIDE))
+                        .build();
+        ScenarioRunner runner = spy(new ScenarioRunner(ArgumentTest.class, testScenario));
+        runner.run(mRunNotifier);
+        Bundle argsAfterTest = InstrumentationRegistry.getArguments();
+        Assert.assertTrue(bundlesContainSameStringKeyValuePairs(argsBeforeTest, argsAfterTest));
+    }
+
+    /**
+     * Verify that no test failure is fired because of an assertion failure in the stubbed methods.
+     * If the verification fails, check whether it's due the injected assertions failing. If yes,
+     * throw that exception out; otherwise, throw the first exception.
+     */
+    private void verifyForAssertionFailures(final RunNotifier notifier) throws Throwable {
+        try {
+            verify(notifier, never()).fireTestFailure(any());
+        } catch (MockitoAssertionError e) {
+            ArgumentCaptor<Failure> failureCaptor = ArgumentCaptor.forClass(Failure.class);
+            verify(notifier, atLeastOnce()).fireTestFailure(failureCaptor.capture());
+            List<Failure> failures = failureCaptor.getAllValues();
+            // Go through the failures, look for an known failure case from the above exceptions
+            // and throw the exception in the first one out if any.
+            for (Failure failure : failures) {
+                if (failure.getException().getMessage().contains(ASSERTION_FAILURE_MESSAGE)) {
+                    throw failure.getException();
+                }
+            }
+            // Otherwise, throw the exception from the first failure reported.
+            throw failures.get(0).getException();
+        }
+    }
+
+    /**
+     * Helper method to check whether two {@link Bundle}s are equal since the built-in {@code
+     * equals} is not properly overridden.
+     */
+    private boolean bundlesContainSameStringKeyValuePairs(Bundle b1, Bundle b2) {
+        if (b1.size() != b2.size()) {
+            return false;
+        }
+        HashSet<String> allKeys = new HashSet<String>(b1.keySet());
+        allKeys.addAll(b2.keySet());
+        for (String key : allKeys) {
+            if (b1.getString(key) != null) {
+                // If key is in b1 and corresponds to a string, check whether this key corresponds
+                // to the same value in b2.
+                if (!b1.getString(key).equals(b2.getString(key))) {
+                    return false;
+                }
+            } else if (b2.getString(key) != null) {
+                // Otherwise if b2 has a string at this key, return false since we know that b1 does
+                // not have a string at this key.
+                return false;
+            }
+        }
+        return true;
+    }
+}
diff --git a/libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/ScheduledScenarioRunnerTest.java b/libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/ScheduledScenarioRunnerTest.java
index a65fce0..78bc467 100644
--- a/libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/ScheduledScenarioRunnerTest.java
+++ b/libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/ScheduledScenarioRunnerTest.java
@@ -31,7 +31,7 @@
 import android.platform.test.longevity.proto.Configuration.Scenario;
 import android.platform.test.longevity.proto.Configuration.Scenario.AfterTest;
 import android.platform.test.longevity.proto.Configuration.Scenario.ExtraArg;
-import android.platform.test.longevity.samples.testing.SampleProfileSuite;
+import android.platform.test.longevity.samples.testing.SampleTimedProfileSuite;
 import androidx.test.InstrumentationRegistry;
 
 import org.junit.Assert;
@@ -114,13 +114,13 @@
         Scenario testScenario =
                 Scenario.newBuilder()
                         .setAt("00:00:00")
-                        .setJourney(SampleProfileSuite.LongIdleTest.class.getName())
+                        .setJourney(SampleTimedProfileSuite.LongIdleTest.class.getName())
                         .setAfterTest(AfterTest.STAY_IN_APP)
                         .build();
         ScheduledScenarioRunner runner =
                 spy(
                         new ScheduledScenarioRunner(
-                                SampleProfileSuite.LongIdleTest.class,
+                                SampleTimedProfileSuite.LongIdleTest.class,
                                 testScenario,
                                 timeoutMs,
                                 true));
@@ -141,8 +141,7 @@
                                             exception
                                                     .getTimeUnit()
                                                     .toMillis(exception.getTimeout());
-                                    long expectedTimeout =
-                                            timeoutMs - ScheduledScenarioRunner.TEARDOWN_LEEWAY_MS;
+                                    long expectedTimeout = timeoutMs - runner.getTeardownLeeway();
                                     return abs(exceptionTimeout - expectedTimeout)
                                             <= TIMING_LEEWAY_MS;
                                 });
@@ -157,13 +156,13 @@
         Scenario testScenario =
                 Scenario.newBuilder()
                         .setAt("00:00:00")
-                        .setJourney(SampleProfileSuite.LongIdleTest.class.getName())
+                        .setJourney(SampleTimedProfileSuite.LongIdleTest.class.getName())
                         .setAfterTest(AfterTest.STAY_IN_APP)
                         .build();
         ScheduledScenarioRunner runner =
                 spy(
                         new ScheduledScenarioRunner(
-                                SampleProfileSuite.LongIdleTest.class,
+                                SampleTimedProfileSuite.LongIdleTest.class,
                                 testScenario,
                                 TimeUnit.SECONDS.toMillis(6),
                                 true));
@@ -181,13 +180,13 @@
         Scenario testScenario =
                 Scenario.newBuilder()
                         .setAt("00:00:00")
-                        .setJourney(SampleProfileSuite.LongIdleTest.class.getName())
+                        .setJourney(SampleTimedProfileSuite.LongIdleTest.class.getName())
                         .setAfterTest(AfterTest.STAY_IN_APP)
                         .build();
         ScheduledScenarioRunner runner =
                 spy(
                         new ScheduledScenarioRunner(
-                                SampleProfileSuite.LongIdleTest.class,
+                                SampleTimedProfileSuite.LongIdleTest.class,
                                 testScenario,
                                 TimeUnit.SECONDS.toMillis(6),
                                 true));
@@ -196,8 +195,7 @@
         // the leeway set in @{link ScheduledScenarioRunner}.
         verify(runner, times(1))
                 .performIdleBeforeNextScenario(
-                        getWithinMarginMatcher(
-                                ScheduledScenarioRunner.TEARDOWN_LEEWAY_MS, TIMING_LEEWAY_MS));
+                        getWithinMarginMatcher(runner.getTeardownLeeway(), TIMING_LEEWAY_MS));
     }
 
     /** Test that a test set to stay in the app after the test idles after its @Test method. */
@@ -209,13 +207,13 @@
         Scenario testScenario =
                 Scenario.newBuilder()
                         .setAt("00:00:00")
-                        .setJourney(SampleProfileSuite.PassingTest.class.getName())
+                        .setJourney(SampleTimedProfileSuite.PassingTest.class.getName())
                         .setAfterTest(AfterTest.STAY_IN_APP)
                         .build();
         ScheduledScenarioRunner runner =
                 spy(
                         new ScheduledScenarioRunner(
-                                SampleProfileSuite.PassingTest.class,
+                                SampleTimedProfileSuite.PassingTest.class,
                                 testScenario,
                                 timeoutMs,
                                 true));
@@ -226,8 +224,7 @@
         verify(runner, times(1))
                 .performIdleBeforeTeardown(
                         getWithinMarginMatcher(
-                                timeoutMs - 2 * ScheduledScenarioRunner.TEARDOWN_LEEWAY_MS,
-                                TIMING_LEEWAY_MS));
+                                timeoutMs - 2 * runner.getTeardownLeeway(), TIMING_LEEWAY_MS));
         // Test should have passed.
         verify(mRunNotifier, never()).fireTestFailure(any(Failure.class));
     }
@@ -241,13 +238,13 @@
         Scenario testScenario =
                 Scenario.newBuilder()
                         .setAt("00:00:00")
-                        .setJourney(SampleProfileSuite.PassingTest.class.getName())
+                        .setJourney(SampleTimedProfileSuite.PassingTest.class.getName())
                         .setAfterTest(AfterTest.EXIT)
                         .build();
         ScheduledScenarioRunner runner =
                 spy(
                         new ScheduledScenarioRunner(
-                                SampleProfileSuite.PassingTest.class,
+                                SampleTimedProfileSuite.PassingTest.class,
                                 testScenario,
                                 timeoutMs,
                                 true));
@@ -268,17 +265,17 @@
         Scenario testScenario =
                 Scenario.newBuilder()
                         .setAt("00:00:00")
-                        .setJourney(SampleProfileSuite.PassingTest.class.getName())
+                        .setJourney(SampleTimedProfileSuite.PassingTest.class.getName())
                         .setAfterTest(AfterTest.EXIT)
                         .build();
         Bundle ignores = new Bundle();
         ignores.putString(
                 LongevityClassRunner.FILTER_OPTION,
-                SampleProfileSuite.PassingTest.class.getCanonicalName());
+                SampleTimedProfileSuite.PassingTest.class.getCanonicalName());
         ScheduledScenarioRunner runner =
                 spy(
                         new ScheduledScenarioRunner(
-                                SampleProfileSuite.PassingTest.class,
+                                SampleTimedProfileSuite.PassingTest.class,
                                 testScenario,
                                 timeoutMs,
                                 true,
@@ -304,13 +301,13 @@
         Scenario testScenario =
                 Scenario.newBuilder()
                         .setAt("00:00:00")
-                        .setJourney(SampleProfileSuite.PassingTest.class.getName())
+                        .setJourney(SampleTimedProfileSuite.PassingTest.class.getName())
                         .setAfterTest(AfterTest.STAY_IN_APP)
                         .build();
         ScheduledScenarioRunner runner =
                 spy(
                         new ScheduledScenarioRunner(
-                                SampleProfileSuite.PassingTest.class,
+                                SampleTimedProfileSuite.PassingTest.class,
                                 testScenario,
                                 TimeUnit.SECONDS.toMillis(6),
                                 false));
@@ -389,6 +386,23 @@
         Assert.assertTrue(abs(actualSleepDuration - expectedSleepMillis) <= TIMING_LEEWAY_MS);
     }
 
+    /** Test that the teardown leeway override works. */
+    @Test
+    public void testTeardownLeewayOverride() throws Throwable {
+        Bundle args = new Bundle();
+        long leewayOverride = 1000L;
+        args.putString(
+                ScheduledScenarioRunner.TEARDOWN_LEEWAY_OPTION, String.valueOf(leewayOverride));
+        ScheduledScenarioRunner runner =
+                new ScheduledScenarioRunner(
+                        ArgumentTest.class,
+                        Scenario.newBuilder().build(),
+                        TimeUnit.SECONDS.toMillis(6),
+                        false,
+                        args);
+        Assert.assertEquals(leewayOverride, runner.getTeardownLeeway());
+    }
+
     /**
      * Helper method to get an argument matcher that checks whether the input value is equal to
      * expected value within a margin.
@@ -399,7 +413,7 @@
 
     /**
      * Verify that no test failure is fired because of an assertion failure in the stubbed methods.
-     * If the verfication fails, check whether it's due the injected assertions failing. If yes,
+     * If the verification fails, check whether it's due the injected assertions failing. If yes,
      * throw that exception out; otherwise, throw the first exception.
      */
     private void verifyForAssertionFailures(final RunNotifier notifier) throws Throwable {
@@ -423,7 +437,7 @@
 
     /**
      * Helper method to check whether two {@link Bundle}s are equal since the built-in {@code
-     * equals} is not properly overriden.
+     * equals} is not properly overridden.
      */
     private boolean bundlesContainSameStringKeyValuePairs(Bundle b1, Bundle b2) {
         if (b1.size() != b2.size()) {
diff --git a/libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/samples/testing/SampleProfileSuite.java b/libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/samples/testing/SampleBasicProfileSuite.java
similarity index 64%
copy from libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/samples/testing/SampleProfileSuite.java
copy to libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/samples/testing/SampleBasicProfileSuite.java
index 2b1bf10..2c0cd5f 100644
--- a/libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/samples/testing/SampleProfileSuite.java
+++ b/libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/samples/testing/SampleBasicProfileSuite.java
@@ -16,53 +16,38 @@
 
 package android.platform.test.longevity.samples.testing;
 
-import android.os.SystemClock;
 import android.platform.test.longevity.ProfileSuite;
 import android.platform.test.scenario.annotation.Scenario;
 
-import org.junit.AfterClass;
 import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 import org.junit.runners.Suite.SuiteClasses;
 
-import java.util.concurrent.TimeUnit;
-
 @RunWith(ProfileSuite.class)
 @SuiteClasses({
-    SampleProfileSuite.LongIdleTest.class,
-    SampleProfileSuite.PassingTest.class,
+    SampleBasicProfileSuite.PassingTest1.class,
+    SampleBasicProfileSuite.PassingTest2.class,
 })
 /** Sample device-side test cases using a profile. */
-public class SampleProfileSuite {
+public class SampleBasicProfileSuite {
+
     @Scenario
     @RunWith(JUnit4.class)
-    public static class LongIdleTest {
+    public static class PassingTest1 {
         @Test
-        public void testLongIdle() {
-            SystemClock.sleep(TimeUnit.SECONDS.toMillis(10));
-        }
-
-        // Simulates a quick teardown step.
-        @AfterClass
-        public static void dummyTearDown() {
-            SystemClock.sleep(100);
+        public void testPassing() {
+            Assert.assertEquals(1, 1);
         }
     }
 
     @Scenario
     @RunWith(JUnit4.class)
-    public static class PassingTest {
+    public static class PassingTest2 {
         @Test
         public void testPassing() {
-            Assert.assertEquals(1, 1);
-        }
-
-        // Simulates a quick teardown step.
-        @AfterClass
-        public static void dummyTearDown() {
-            SystemClock.sleep(100);
+            Assert.assertEquals(2, 2);
         }
     }
 }
diff --git a/libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/samples/testing/SampleProfileSuite.java b/libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/samples/testing/SampleTimedProfileSuite.java
similarity index 93%
rename from libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/samples/testing/SampleProfileSuite.java
rename to libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/samples/testing/SampleTimedProfileSuite.java
index 2b1bf10..16686ee 100644
--- a/libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/samples/testing/SampleProfileSuite.java
+++ b/libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/samples/testing/SampleTimedProfileSuite.java
@@ -31,11 +31,11 @@
 
 @RunWith(ProfileSuite.class)
 @SuiteClasses({
-    SampleProfileSuite.LongIdleTest.class,
-    SampleProfileSuite.PassingTest.class,
+    SampleTimedProfileSuite.LongIdleTest.class,
+    SampleTimedProfileSuite.PassingTest.class,
 })
 /** Sample device-side test cases using a profile. */
-public class SampleProfileSuite {
+public class SampleTimedProfileSuite {
     @Scenario
     @RunWith(JUnit4.class)
     public static class LongIdleTest {
diff --git a/libraries/launcher-helper/src/android/support/test/launcherhelper/AutoLauncherStrategy.java b/libraries/launcher-helper/src/android/support/test/launcherhelper/AutoLauncherStrategy.java
index dcbe7e1..c547f0d 100644
--- a/libraries/launcher-helper/src/android/support/test/launcherhelper/AutoLauncherStrategy.java
+++ b/libraries/launcher-helper/src/android/support/test/launcherhelper/AutoLauncherStrategy.java
@@ -338,11 +338,11 @@
                 mDevice.waitForIdle();
             }
 
-            UiObject2 app = mDevice.findObject(appSelector);
+            UiObject2 app = mDevice.wait(Until.findObject(appSelector), UI_WAIT_TIMEOUT);
             while (app == null && down.isEnabled()) {
                 down.click();
                 mDevice.waitForIdle();
-                app = mDevice.findObject(appSelector);
+                app = mDevice.wait(Until.findObject(appSelector), UI_WAIT_TIMEOUT);
             }
             return app;
         } else {
diff --git a/scripts/perfetto-setup/Android.mk b/scripts/perfetto-setup/Android.mk
index 180ddb2..ab478ec 100644
--- a/scripts/perfetto-setup/Android.mk
+++ b/scripts/perfetto-setup/Android.mk
@@ -31,6 +31,22 @@
 include $(BUILD_PREBUILT)
 
 include $(CLEAR_VARS)
+LOCAL_MODULE := trace_config.textproto
+LOCAL_MODULE_CLASS := ETC
+LOCAL_MODULE_TAGS := optional
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA)/local/tmp
+LOCAL_PREBUILT_MODULE_FILE := prebuilts/tools/linux-x86_64/perfetto/configs/trace_config.textproto
+include $(BUILD_PREBUILT)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := trace_config_experimental.textproto
+LOCAL_MODULE_CLASS := ETC
+LOCAL_MODULE_TAGS := optional
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA)/local/tmp
+LOCAL_PREBUILT_MODULE_FILE := prebuilts/tools/linux-x86_64/perfetto/configs/trace_config_experimental.textproto
+include $(BUILD_PREBUILT)
+
+include $(CLEAR_VARS)
 LOCAL_MODULE := perfetto_trace_processor_shell
 LOCAL_MODULE_CLASS := EXECUTABLES
 LOCAL_MODULE_TAGS := optional
diff --git a/tests/health/scenarios/src/android/platform/test/scenario/system/ScreenOff.java b/tests/health/scenarios/src/android/platform/test/scenario/system/ScreenOff.java
index 1f5e234..a2e1290 100644
--- a/tests/health/scenarios/src/android/platform/test/scenario/system/ScreenOff.java
+++ b/tests/health/scenarios/src/android/platform/test/scenario/system/ScreenOff.java
@@ -57,8 +57,11 @@
     @After
     public void tearDown() throws RemoteException {
         if (mTurnScreenBackOn.get()) {
-            mDevice.wakeUp();
+            // Wake up the display. wakeUp() is not used here as when the duration is short, the
+            // device might register a double power button press and launch camera.
+            mDevice.pressMenu();
             mDevice.waitForIdle();
+            // Unlock the screen.
             mDevice.pressMenu();
             mDevice.waitForIdle();
         }
diff --git a/tests/health/scenarios/tests/Android.bp b/tests/health/scenarios/tests/Android.bp
index 9ac9a37..076a26c 100644
--- a/tests/health/scenarios/tests/Android.bp
+++ b/tests/health/scenarios/tests/Android.bp
@@ -59,6 +59,8 @@
     name: "PlatformCommonScenarioTests",
     min_sdk_version: "24",
     static_libs: [
+        "android-support-test",
+        "collector-device-lib-platform",
         "common-platform-scenarios",
         "common-profile-text-protos",
         "guava",
diff --git a/tests/health/scenarios/tests/src/android/platform/test/scenario/generic/OpenAppMicrobenchmark.java b/tests/health/scenarios/tests/src/android/platform/test/scenario/generic/OpenAppMicrobenchmark.java
index 4b15e88..03b9da6 100644
--- a/tests/health/scenarios/tests/src/android/platform/test/scenario/generic/OpenAppMicrobenchmark.java
+++ b/tests/health/scenarios/tests/src/android/platform/test/scenario/generic/OpenAppMicrobenchmark.java
@@ -18,6 +18,7 @@
 import android.platform.test.microbenchmark.Microbenchmark;
 import android.platform.test.rule.CompilationFilterRule;
 import android.platform.test.rule.DropCachesRule;
+import android.platform.test.rule.IorapCompilationRule;
 import android.platform.test.rule.KillAppsRule;
 import android.platform.test.rule.PressHomeRule;
 
@@ -33,5 +34,6 @@
             RuleChain.outerRule(new KillAppsRule(sPkgOption.get()))
                     .around(new DropCachesRule())
                     .around(new CompilationFilterRule(sPkgOption.get()))
-                    .around(new PressHomeRule());
+                    .around(new PressHomeRule())
+                    .around(new IorapCompilationRule(sPkgOption.get()));
 }
diff --git a/tests/jank/UbSystemUiJankTests/AndroidTest.xml b/tests/jank/UbSystemUiJankTests/AndroidTest.xml
index 3fe63fc..0923ff2 100644
--- a/tests/jank/UbSystemUiJankTests/AndroidTest.xml
+++ b/tests/jank/UbSystemUiJankTests/AndroidTest.xml
@@ -23,7 +23,7 @@
     </target_preparer>
 
     <option name="test-tag" value="UbSystemUiJankTests" />
-    <test class="com.android.tradefed.testtype.InstrumentationTest" >
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="android.platform.systemui.tests.jank" />
         <option name="runner" value="android.test.InstrumentationTestRunner" />
         <option name="hidden-api-checks" value="false" />
diff --git a/tests/jank/uibench/AndroidManifest.xml b/tests/jank/uibench/AndroidManifest.xml
index 56d3e4a..da61649 100644
--- a/tests/jank/uibench/AndroidManifest.xml
+++ b/tests/jank/uibench/AndroidManifest.xml
@@ -16,14 +16,14 @@
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.uibench.janktests">
+    <uses-sdk android:minSdkVersion="23" android:targetSdkVersion="23" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+
     <application>
         <uses-library android:name="android.test.runner" />
     </application>
 
-    <uses-sdk android:minSdkVersion="19"
-          android:targetSdkVersion="23"/>
-
     <instrumentation
             android:name="android.support.test.runner.AndroidJUnitRunner"
             android:targetPackage="com.android.uibench.janktests"
diff --git a/tests/microbenchmarks/uibench/AndroidManifest.xml b/tests/microbenchmarks/uibench/AndroidManifest.xml
index 0265f2b..074f0d8 100644
--- a/tests/microbenchmarks/uibench/AndroidManifest.xml
+++ b/tests/microbenchmarks/uibench/AndroidManifest.xml
@@ -18,6 +18,7 @@
     package="com.android.uibench.microbenchmark">
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.REAL_GET_TASKS" />
+    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
 
     <application>
         <uses-library android:name="android.test.runner" />
diff --git a/tests/perf/PerfTransitionTest/src/com/android/apptransition/tests/AppTransitionTests.java b/tests/perf/PerfTransitionTest/src/com/android/apptransition/tests/AppTransitionTests.java
index 6d5136f..1df767c 100644
--- a/tests/perf/PerfTransitionTest/src/com/android/apptransition/tests/AppTransitionTests.java
+++ b/tests/perf/PerfTransitionTest/src/com/android/apptransition/tests/AppTransitionTests.java
@@ -121,18 +121,18 @@
         }
 
         createLaunchIntentMappings();
-        String mAppsList = mArgs.getString(LAUNCH_APPS);
-        mPreAppsList = mArgs.getString(PRE_LAUNCH_APPS);
+
+        String appsList = mArgs.getString(LAUNCH_APPS, "");
+        mPreAppsList = mArgs.getString(PRE_LAUNCH_APPS, "");
         mLaunchIterations = Integer.parseInt(mArgs.getString(KEY_LAUNCH_ITERATIONS,
                 DEFAULT_LAUNCH_COUNT));
         mPostLaunchTimeout = Integer.parseInt(mArgs.getString(KEY_POST_LAUNCH_TIMEOUT,
                 DEFAULT_POST_LAUNCH_TIMEOUT));
-        if (null == mAppsList && mAppsList.isEmpty()) {
+        if (null == appsList || appsList.isEmpty()) {
             throw new IllegalArgumentException("Need atleast one app to do the"
                     + " app transition from launcher");
         }
-        mAppsList = mAppsList.replaceAll("%"," ");
-        mAppListArray = mAppsList.split(DELIMITER);
+        mAppListArray = appsList.split(DELIMITER);
 
         // Parse the trace parameters
         mTraceDirectoryStr = mArgs.getString(KEY_TRACE_DIRECTORY);
@@ -266,11 +266,10 @@
         if (isTracesEnabled()) {
             createTraceDirectory("testAppToRecents");
         }
-        if (null == mPreAppsList && mPreAppsList.isEmpty()) {
+        if (null == mPreAppsList || mPreAppsList.isEmpty()) {
             throw new IllegalArgumentException("Need atleast few apps in the "
                     + "recents before starting the test");
         }
-        mPreAppsList = mPreAppsList.replaceAll("%"," ");
         mPreAppsListArray = mPreAppsList.split(DELIMITER);
         mPreAppsComponentName.clear();
         populateRecentsList();
@@ -311,11 +310,10 @@
         if (isTracesEnabled()) {
             createTraceDirectory("testHotLaunchFromRecents");
         }
-        if (null == mPreAppsList && mPreAppsList.isEmpty()) {
+        if (null == mPreAppsList || mPreAppsList.isEmpty()) {
             throw new IllegalArgumentException("Need atleast few apps in the"
                     + " recents before starting the test");
         }
-        mPreAppsList = mPreAppsList.replaceAll("%", " ");
         mPreAppsListArray = mPreAppsList.split(DELIMITER);
         mPreAppsComponentName.clear();
         populateRecentsList();
@@ -567,7 +565,8 @@
      * @param appNames
      */
     private void closeApps(String[] appNames) {
-        for (int i = 0; i < appNames.length; i++) {
+        int length = appNames == null ? 0 : appNames.length;
+        for (int i = 0; i < length; i++) {
             Intent startIntent = mAppLaunchIntentsMapping.get(appNames[i]);
             if (startIntent != null) {
                 String packageName = startIntent.getComponent().getPackageName();