Merge "Move common-compat lib tests near the code"
diff --git a/.gitignore b/.gitignore
deleted file mode 100644
index 4a60582..0000000
--- a/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-*.iml
-.idea
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index a74063e..0bfbc43 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -16,3 +16,4 @@
                       libraries/system-helpers
                       libraries/timeresult-helpers
 
+ktlint_hook = ${REPO_ROOT}/prebuilts/ktlint/ktlint.py -f ${PREUPLOAD_FILES}
\ No newline at end of file
diff --git a/build/tasks/tests/instrumentation_metric_test_list.mk b/build/tasks/tests/instrumentation_metric_test_list.mk
index ce7a0d6..a9185f8 100644
--- a/build/tasks/tests/instrumentation_metric_test_list.mk
+++ b/build/tasks/tests/instrumentation_metric_test_list.mk
@@ -13,10 +13,16 @@
 # limitations under the License.
 
 instrumentation_metric_tests := \
+    ActivityManagerPerfTests \
+    ActivityManagerPerfTestsTestApp \
+    ActivityManagerPerfTestsStubApp1 \
+    ActivityManagerPerfTestsStubApp2 \
+    ActivityManagerPerfTestsStubApp3 \
     AutofillPerfTests \
     BlobStorePerfTests \
     crashcollector \
     CorePerfTests \
+    ContentCapturePerfTests \
     DocumentsUIAppPerfTests \
     MtpServicePerfTests \
     RsBlasBenchmark \
@@ -26,9 +32,62 @@
     MultiUserPerfTests \
     NeuralNetworksApiBenchmark \
     PackageManagerPerfTests \
+    QueriesAll0 \
+    QueriesAll1 \
+    QueriesAll2 \
+    QueriesAll3 \
+    QueriesAll4 \
+    QueriesAll5 \
+    QueriesAll6 \
+    QueriesAll7 \
+    QueriesAll8 \
+    QueriesAll9 \
+    QueriesAll10 \
+    QueriesAll11 \
+    QueriesAll12 \
+    QueriesAll13 \
+    QueriesAll14 \
+    QueriesAll15 \
+    QueriesAll16 \
+    QueriesAll17 \
+    QueriesAll18 \
+    QueriesAll19 \
+    QueriesAll20 \
+    QueriesAll21 \
+    QueriesAll22 \
+    QueriesAll23 \
+    QueriesAll24 \
+    QueriesAll25 \
+    QueriesAll26 \
+    QueriesAll27 \
+    QueriesAll28 \
+    QueriesAll29 \
+    QueriesAll30 \
+    QueriesAll31 \
+    QueriesAll32 \
+    QueriesAll33 \
+    QueriesAll34 \
+    QueriesAll35 \
+    QueriesAll36 \
+    QueriesAll37 \
+    QueriesAll38 \
+    QueriesAll39 \
+    QueriesAll40 \
+    QueriesAll41 \
+    QueriesAll42 \
+    QueriesAll43 \
+    QueriesAll44 \
+    QueriesAll45 \
+    QueriesAll46 \
+    QueriesAll47 \
+    QueriesAll48 \
+    QueriesAll49 \
     TextClassifierPerfTests \
     WmPerfTests \
-    trace_config_detailed.textproto
+    ImePerfTests \
+    trace_config_detailed.textproto \
+    trace_config_multi_user.textproto \
+    perfetto_trace_processor_shell
 
     # TODO(b/72332760): Uncomment when fixed
     #DocumentsUIPerfTests
diff --git a/build/tasks/tests/instrumentation_test_list.mk b/build/tasks/tests/instrumentation_test_list.mk
index 0c4282c..7d93e59 100644
--- a/build/tasks/tests/instrumentation_test_list.mk
+++ b/build/tasks/tests/instrumentation_test_list.mk
@@ -29,6 +29,7 @@
     FrameworksServicesTests \
     FrameworksMockingServicesTests \
     WmTests \
+    WmPerfTests \
     JobTestApp \
     SuspendTestApp \
     FrameworksUtilTests \
@@ -39,6 +40,7 @@
     SystemUITests \
     TestablesTests \
     FrameworksWifiApiTests \
+    FrameworksWifiNonUpdatableApiTests \
     FrameworksWifiTests \
     FrameworksTelephonyTests \
     ContactsProviderTests \
@@ -67,13 +69,23 @@
     PresencePollingTests \
     ImsCommonTests \
     SettingsProviderTest \
-    FrameworksLocationTests \
     FrameworksPrivacyLibraryTests \
     SettingsUITests \
     SettingsPerfTests \
-    ExtServicesUnitTests\
-    FrameworksNetSmokeTests\
+    ExtServicesUnitTests \
+    FrameworksNetSmokeTests \
+    FlickerTests \
+    FlickerTestApp \
+    WMShellFlickerTests \
+    WMShellFlickerTestApp \
+    WMShellUnitTests \
+    trace_config_detailed.textproto \
+    perfetto_trace_processor_shell \
+    CarDeveloperOptionsUnitTests
 
+ifneq ($(strip $(BOARD_PERFSETUP_SCRIPT)),)
+instrumentation_tests += perf-setup
+endif
 
 # Storage Manager may not exist on device
 ifneq ($(filter StorageManager, $(PRODUCT_PACKAGES)),)
diff --git a/build/tasks/tests/native_metric_test_list.mk b/build/tasks/tests/native_metric_test_list.mk
index 119e158..45ae1ca 100644
--- a/build/tasks/tests/native_metric_test_list.mk
+++ b/build/tasks/tests/native_metric_test_list.mk
@@ -21,12 +21,15 @@
     hwuimicro \
     inputflinger_benchmarks \
     libandroidfw_benchmarks \
+    libhwbinder_benchmark \
     libjavacore-benchmarks \
+    libpowermanager_benchmarks \
+    libvibratorservice_benchmarks \
     minikin_perftests \
     mmapPerf \
     netd_benchmark \
     skia_nanobench \
-    libhwbinder_benchmark
+    VibratorHalIntegrationBenchmark
 
 ifneq ($(strip $(BOARD_PERFSETUP_SCRIPT)),)
 native_metric_tests += perf-setup
diff --git a/build/tasks/tests/platform_test_list.mk b/build/tasks/tests/platform_test_list.mk
index a66bf16..b5fa06c 100644
--- a/build/tasks/tests/platform_test_list.mk
+++ b/build/tasks/tests/platform_test_list.mk
@@ -5,12 +5,11 @@
     ActivityManagerPerfTestsStubApp3 \
     ActivityManagerPerfTestsTestApp \
     AndroidTVJankTests \
+    AndroidXComposeStartupApp \
     ApiDemos \
     AppCompatibilityTest \
     AppLaunch \
     AppLaunchWear \
-    AppLinkFunctionalTests \
-    AppLinkTestApp \
     AppTransitionTests \
     AutoLocTestApp \
     AutoLocVersionedTestApp_v1 \
@@ -84,7 +83,9 @@
     PlatformCommonScenarioTests \
     PowerPerfTest \
     SettingsUITests \
-    SimpleServiceTestApp \
+    SimpleServiceTestApp1 \
+    SimpleServiceTestApp2 \
+    SimpleServiceTestApp3 \
     SimpleTestApp \
     skia_dm \
     skia_nanobench \
@@ -96,6 +97,7 @@
     trace_config.textproto \
     trace_config_detailed.textproto \
     trace_config_experimental.textproto \
+    trace_config_multi_user_cuj_tests.textproto \
     UbSystemUiJankTests \
     UbWebViewJankTests \
     UiBench \
diff --git a/emu_test/cts.module.1 b/emu_test/cts.module.1
index a78704e..d3df1d7 100644
--- a/emu_test/cts.module.1
+++ b/emu_test/cts.module.1
@@ -92,6 +92,7 @@
 cts.CtsNetSecPolicyUsesCleartextTrafficTrueTestCases
 cts.CtsSustainedPerformanceHostTestCases
 cts.CtsAttentionServiceDeviceTestCases
+cts.CtsAppSearchTestCases
 cts.CtsLegacyNotification28TestCases
 cts.CtsNativeMediaXaTestCases
 cts.CtsApexTestCases
diff --git a/emu_test/run_test_suite.sh b/emu_test/run_test_suite.sh
index 0c8dc1c..17ca4b9 100755
--- a/emu_test/run_test_suite.sh
+++ b/emu_test/run_test_suite.sh
@@ -96,6 +96,7 @@
 # Directory where tradefed-make tools are cloned
 TRADEFED_MAKE_DIR="$WORK_DIR/tradefed-make"
 git clone \
+  --branch v1 \
   https://team.googlesource.com/android-devtools-emulator/tradefed-make \
   $TRADEFED_MAKE_DIR
 
diff --git a/libraries/annotations/src/android/platform/test/annotations/AsbSecurityTest.java b/libraries/annotations/src/android/platform/test/annotations/AsbSecurityTest.java
new file mode 100644
index 0000000..dd9b36a
--- /dev/null
+++ b/libraries/annotations/src/android/platform/test/annotations/AsbSecurityTest.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2021 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.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks the type of test with purpose of evaluating security vulnerabilities against the Android
+ * Security Bulletin (ASB).
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.TYPE})
+public @interface AsbSecurityTest {
+    /** The CVE bug id which contains the ASA hotlists. */
+    long[] cveBugId();
+}
+
diff --git a/libraries/annotations/src/android/platform/test/annotations/RequiresDevice.java b/libraries/annotations/src/android/platform/test/annotations/RequiresDevice.java
index 852582c..456f100 100644
--- a/libraries/annotations/src/android/platform/test/annotations/RequiresDevice.java
+++ b/libraries/annotations/src/android/platform/test/annotations/RequiresDevice.java
@@ -23,11 +23,10 @@
 
 /**
  * Indicates that a specific host side test should not be run against an emulator.
- * <p/>
- * It will be executed only if the test is running against a physical android device. <br>
- * For device side tests, annotate with android.support.test.filters.RequiresDevice
+ *
+ * <p>It will be executed only if the test is running against a physical android device. <br>
+ * For device side tests, annotate with androidx.test.filters.RequiresDevice
  */
 @Retention(RetentionPolicy.RUNTIME)
 @Target({ElementType.METHOD, ElementType.TYPE})
-public @interface RequiresDevice {
-}
+public @interface RequiresDevice {}
diff --git a/libraries/annotations/src/android/platform/test/annotations/RootPermissionTest.java b/libraries/annotations/src/android/platform/test/annotations/RootPermissionTest.java
index f520ef6..b0c3f33 100644
--- a/libraries/annotations/src/android/platform/test/annotations/RootPermissionTest.java
+++ b/libraries/annotations/src/android/platform/test/annotations/RootPermissionTest.java
@@ -31,13 +31,17 @@
 public @interface RootPermissionTest {
 
     // Denotes the patch level when the test was introduced
-    // TODO : Remove the default value. Need one in the interim whilst we undertake
-    // the effort to annotate the existing tests with a corresponding patch-level.
+    /** @deprecated @see android.platform.test.annotations.AsbSecurityTest */
+    @Deprecated
     String minPatchLevel() default "";
 
     // Denotes the CVE ID(s), comma-separated, to which this test applies.
+    /** @deprecated @see android.platform.test.annotations.AsbSecurityTest */
+    @Deprecated
     String cve() default "";
 
     // Denotes the scope (platform/kernel/vendor) to which this test applies.
+    /** @deprecated @see android.platform.test.annotations.AsbSecurityTest */
+    @Deprecated
     String scope() default "";
 }
diff --git a/libraries/annotations/src/android/platform/test/annotations/SecurityTest.java b/libraries/annotations/src/android/platform/test/annotations/SecurityTest.java
index 1154c6f..a8ee222 100644
--- a/libraries/annotations/src/android/platform/test/annotations/SecurityTest.java
+++ b/libraries/annotations/src/android/platform/test/annotations/SecurityTest.java
@@ -30,13 +30,17 @@
 public @interface SecurityTest {
 
     // Denotes the patch level when the test was introduced
-    // TODO : Remove the default value. Need one in the interim whilst we undertake
-    // the effort to annotate the existing tests with a corresponding patch-level.
+    /** @deprecated @see android.platform.test.annotations.AsbSecurityTest */
+    @Deprecated
     String minPatchLevel() default "";
 
     // Denotes the CVE ID(s), comma-separated, to which this test applies.
+    /** @deprecated @see android.platform.test.annotations.AsbSecurityTest */
+    @Deprecated
     String cve() default "";
 
     // Denotes the scope (platform/kernel/vendor) to which this test applies.
+    /** @deprecated @see android.platform.test.annotations.AsbSecurityTest */
+    @Deprecated
     String scope() default "";
 }
diff --git a/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoAppGridHelper.java b/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoAppGridHelper.java
index 52a1856..f82317b 100644
--- a/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoAppGridHelper.java
+++ b/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoAppGridHelper.java
@@ -30,4 +30,25 @@
      * Check if device is currently at the bottom of app grid.
      */
     public boolean isBottom();
+
+    /**
+     * Setup expectations: In App grid.
+     *
+     * <p>Scroll up on page.
+     */
+    boolean scrollUpOnePage();
+
+    /**
+     * Setup expectations: In App grid.
+     *
+     * <p>Scroll down on page.
+     */
+    boolean scrollDownOnePage();
+
+    /**
+     * Setup expectations: In App grid.
+     *
+     * <p>Find and open an application.
+     */
+    void openApp(String appName);
 }
diff --git a/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoAppInfoSettingsHelper.java b/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoAppInfoSettingsHelper.java
index cc6532b..024cd52 100644
--- a/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoAppInfoSettingsHelper.java
+++ b/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoAppInfoSettingsHelper.java
@@ -92,13 +92,4 @@
      * @param packageName - package of the application to be checked.
      */
     boolean isApplicationDisabled(String packageName);
-
-    /**
-     * Setup expectation: None
-     *
-     * <p>This method is to check open an application.
-     *
-     * @param appName - Name of the app to be opened.
-     */
-    void openApp(String packageName);
 }
diff --git a/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoCarEvsServiceHelper.java b/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoCarEvsServiceHelper.java
new file mode 100644
index 0000000..6c58694
--- /dev/null
+++ b/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoCarEvsServiceHelper.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2021 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.helpers;
+
+public interface IAutoCarEvsServiceHelper extends IAppHelper {
+    /** Start Android EVS reference app by injecting VHAL event */
+    void startEvsPreview();
+
+    /** Stop Android EVS reference app by injecting VHAL event */
+    void stopEvsPreview();
+}
diff --git a/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoDialHelper.java b/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoDialHelper.java
index 37cd8d8..a25324d 100644
--- a/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoDialHelper.java
+++ b/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoDialHelper.java
@@ -236,11 +236,4 @@
      * <p>This method is used to open contact list
      */
     void openContacts();
-
-    /**
-     * This method is used to open the Phone App
-     *
-     * @param No parameters.
-     */
-    void OpenPhoneApp();
 }
diff --git a/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoGooglePlayHelper.java b/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoGooglePlayHelper.java
index 331eec3..d3330db 100644
--- a/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoGooglePlayHelper.java
+++ b/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoGooglePlayHelper.java
@@ -19,13 +19,6 @@
 public interface IAutoGooglePlayHelper extends IAppHelper, Scrollable {
 
     /**
-     * Setup expectations: Launch Google Play Store app.
-     *
-     * <p>This method is used to Open Google Play Store app.
-     */
-    void openGooglePlayStore();
-
-    /**
      * Setup expectations: Google Play app is open.
      *
      * <p>This method is used to search an app and click it in Google Play.
@@ -67,11 +60,4 @@
      */
     @Deprecated
     void openApp();
-
-    /**
-     * Setup expectations: None.
-     *
-     * <p>This method is used to check if the given application package is installed.
-     */
-    boolean checkIfApplicationExists(String packageName);
 }
diff --git a/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoHomeHelper.java b/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoHomeHelper.java
index 7ea0284..2f67107 100644
--- a/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoHomeHelper.java
+++ b/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoHomeHelper.java
@@ -28,28 +28,14 @@
     /**
      * Setup expectations: Should be on home screen.
      *
-     * Checks if exists a weather widget.
-     */
-    boolean hasWeatherWidget();
-
-    /**
-     * Setup expectations: Should be on home screen.
-     *
-     * @return to get current user name shown on home screen.
-     */
-    String getUserName();
-
-    /**
-     * Setup expectations: Should be on home screen.
-     *
-     * @return to get current date in LocalDate format.
-     */
-    String getDate();
-
-    /**
-     * Setup expectations: Should be on home screen.
-     *
      * <p>Checks if exists a media widget.
      */
     boolean hasMediaWidget();
+
+    /**
+     * Setup expectations: Should be on home screen.
+     *
+     * <p>Checks if exists a assistant widget.
+     */
+    boolean hasAssistantWidget();
 }
diff --git a/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoMediaCenterHelper.java b/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoMediaCenterHelper.java
index 81d53d8..92180a6 100644
--- a/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoMediaCenterHelper.java
+++ b/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoMediaCenterHelper.java
@@ -110,11 +110,4 @@
      * @param - menuOptions used to pass multiple level of menu options in one go.
      */
     void selectMediaTrack(String... menuOptions);
-
-    /**
-     * Setup expectations: Launch Media App.
-     *
-     * @param - String packagename - Android media package to be opened.
-     */
-    void openMediaApp(String packagename);
 }
diff --git a/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoMediaHelper.java b/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoMediaHelper.java
index 960921f..569c5a3 100644
--- a/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoMediaHelper.java
+++ b/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoMediaHelper.java
@@ -128,11 +128,4 @@
      * else returns false
      */
     boolean isPlaying();
-
-    /**
-     * Setup expectations: Launch Media App.
-     *
-     * @param - String packagename - Android media package to be opened.
-     */
-    void openMediaApp(String packagename);
 }
diff --git a/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoNotificationHelper.java b/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoNotificationHelper.java
index b5fd0f7..2e7dabe 100644
--- a/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoNotificationHelper.java
+++ b/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoNotificationHelper.java
@@ -16,7 +16,7 @@
 
 package android.platform.helpers;
 
-public interface IAutoNotificationHelper extends INotificationHelper, Scrollable, IAppHelper {
+public interface IAutoNotificationHelper extends Scrollable, IAppHelper {
     /**
      * Setup expectations: Notification app is open and scrolled to the bottom.
      *
diff --git a/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoNotificationMockingHelper.java b/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoNotificationMockingHelper.java
index 5d14d8e..330d214 100644
--- a/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoNotificationMockingHelper.java
+++ b/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoNotificationMockingHelper.java
@@ -1,10 +1,52 @@
 package android.platform.helpers;
 
-public interface IAutoNotificationMockingHelper extends INotificationHelper, IAppHelper {
+public interface IAutoNotificationMockingHelper extends IAppHelper {
     /**
      * Setup expectations: No.
      *
      * <p>Clear all notifications generated.
      */
     void clearAllNotification();
+
+    /**
+     * Setup Expectations: None
+     *
+     * <p>Posts a number of notifications to the device. Successive calls to this should post new
+     * notifications to those previously posted. Note that this may fail if the helper has surpassed
+     * the system-defined limit for per-package notifications.
+     *
+     * @param count The number of notifications to post.
+     */
+    public default void postNotifications(int count) {
+        throw new UnsupportedOperationException("Not yet implemented.");
+    }
+
+    /**
+     * Setup Expectations: None
+     *
+     * <p>Posts a number of notifications to the device with a package to launch. Successive calls
+     * to this should post new notifications in addition to those previously posted. Note that this
+     * may fail if the helper has surpassed the system-defined limit for per-package notifications.
+     *
+     * @param count The number of notifications to post.
+     * @param pkg The application that will be launched by notifications.
+     */
+    public default void postNotifications(int count, String pkg) {
+        throw new UnsupportedOperationException("Not yet implemented.");
+    }
+
+    /**
+     * Setup Expectations: None
+     *
+     * <p>Posts a number of notifications to the device with a package to launch. Successive calls
+     * to this should post new notifications in addition to those previously posted. Note that this
+     * may fail if the helper has surpassed the system-defined limit for per-package notifications.
+     *
+     * @param count The number of notifications to post.
+     * @param pkg The application that will be launched by notifications.
+     * @param interrupting If notification should make sounds and be on top section of the shade.
+     */
+    public default void postNotifications(int count, String pkg, boolean interrupting) {
+        throw new UnsupportedOperationException("Not yet implemented.");
+    }
 }
diff --git a/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoProfileHelper.java b/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoProfileHelper.java
new file mode 100644
index 0000000..6bff0c4
--- /dev/null
+++ b/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoProfileHelper.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2021 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.helpers;
+
+public interface IAutoProfileHelper extends IAppHelper {
+
+    /**
+     * Setup expectation: Profiles and Accounts setting is open.
+     *
+     * <p>This method is to add an a new user.
+     */
+    void addProfile();
+
+    /**
+     * Setup expectation: Profiles and Accounts setting is open.
+     *
+     * <p>This method is to add an a new user.
+     */
+    void addProfileQuickSettings(String userName);
+
+    /**
+     * Setup expectation: Profiles and Accounts setting is open.
+     *
+     * <p>This method is to delete an existing user.
+     */
+    void deleteProfile(String user);
+
+    /**
+     * Setup expectation: Profiles and Accounts setting is open.
+     *
+     * <p>This method is to delete user's own profile.
+     */
+    void deleteCurrentProfile();
+
+    /**
+     * Setup expectation: Profiles and Accounts setting is open.
+     *
+     * <p>This method is to check if user already exists.
+     */
+    boolean isProfilePresent(String userName);
+
+    /**
+     * Setup expectation: Profiles and Accounts setting is open.
+     *
+     * <p>This method is to switch between existing users.
+     */
+    void switchProfile(String userFrom, String userTo);
+
+    /**
+     * Setup expectation: Profiles and Accounts setting is open.
+     *
+     * <p>This method is to make an existing user admin.
+     */
+    void makeUserAdmin(String user);
+}
diff --git a/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoSettingHelper.java b/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoSettingHelper.java
index 73ffb18..4751969 100644
--- a/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoSettingHelper.java
+++ b/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoSettingHelper.java
@@ -143,24 +143,6 @@
     void openMenuWith(String... menuOptions);
 
     /**
-     * Setup expectations: settings app is open and settings menu is selected
-     *
-     * Checks if the toggle switch for the given index is checked.
-     * @param index of toggle switch.
-     * index should be passed as 0 if only one toggle switch is present on screen.
-     */
-    boolean isToggleSwitchChecked(int index);
-
-    /**
-     * Setup expectations: settings app is open and settings menu is selected
-     *
-     * Clicks the toggle switch for the given index
-     * @param index of toggle switch.
-     * index should be passed as 0 if only one toggle switch is present on screen.
-     */
-    void clickToggleSwitch(int index);
-
-    /**
      * Setup expectations: settings app is open.
      *
      * gets the value of the setting.
diff --git a/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/utility/IAutoNavigationBarHelper.java b/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/utility/IAutoNavigationBarHelper.java
deleted file mode 100644
index 227d29f..0000000
--- a/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/utility/IAutoNavigationBarHelper.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * 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.helpers;
-
-public interface IAutoNavigationBarHelper extends IAppHelper {
-    /**
-     * Setup expectation: None.
-     *
-     * <p>This method is to open Home page using Facet.
-     */
-    void openHomeFacet();
-    /**
-     * Setup expectation: None.
-     *
-     * <p>This method is to open Maps application using Facet.
-     */
-    void openMapsFacet();
-    /**
-     * Setup expectation: None.
-     *
-     * <p>This method is to open Media application using Facet.
-     */
-    void openMediaFacet();
-    /**
-     * Setup expectation: Media Application is open.
-     *
-     * <p>This method is to open given Media application.
-     */
-    void openMediaFacet(String appName);
-    /**
-     * Setup expectation: None.
-     *
-     * <p>This method is to open Dial application using Facet.
-     */
-    void openDialFacet();
-    /**
-     * Setup expectation: None.
-     *
-     * <p>This method is to open App Grid using Facet.
-     */
-    void openAppGridFacet();
-    /**
-     * Setup expectation: None.
-     *
-     * <p>This method is to open Notifications using Facet.
-     */
-    void openNotificationsFacet();
-    /**
-     * Setup expectation: None.
-     *
-     * <p>This method is to open Assistant application using Facet.
-     */
-    void openAssistantFacet();
-    /**
-     * Setup expectation: None.
-     *
-     * <p>This method is to open Quick Settings.
-     */
-    void openQuickSettings();
-}
diff --git a/libraries/app-helpers/interfaces/common/src/android/platform/helpers/IMapsHelper.java b/libraries/app-helpers/interfaces/common/src/android/platform/helpers/IMapsHelper.java
index ceca73f..894a861 100644
--- a/libraries/app-helpers/interfaces/common/src/android/platform/helpers/IMapsHelper.java
+++ b/libraries/app-helpers/interfaces/common/src/android/platform/helpers/IMapsHelper.java
@@ -53,18 +53,36 @@
     /**
      * Setup expectation: Destination is selected.
      *
-     * Goes to the details screen for the selected destination.
+     * <p>Goes to the details screen for the selected destination.
      */
-    default public void openAddressDetails() {
+    public default void openAddressDetails() {
         throw new UnsupportedOperationException("Not yet implemented.");
     }
 
     /**
      * Setup expectation: On the destination details screen.
      *
-     * Flings down the destination details screen.
+     * <p>Flings the destination details screen (must be UP or DOWN).
      */
-    default public void flingAddressDetails() {
+    public default void flingAddressDetails(Direction dir) {
+        throw new UnsupportedOperationException("Not yet implemented.");
+    }
+
+    /**
+     * Setup expectation: On the destination details screen.
+     *
+     * <p>Scrolls the destination details screen (must be UP or DOWN).
+     */
+    public default void scrollAddressDetails(Direction dir) {
+        throw new UnsupportedOperationException("Not yet implemented.");
+    }
+
+    /**
+     * Setup expectation: On the destination details screen.
+     *
+     * <p>Closes the destination details screen.
+     */
+    public default void closeAddressDetails() {
         throw new UnsupportedOperationException("Not yet implemented.");
     }
 
@@ -99,10 +117,10 @@
     /**
      * Setup expectation: On the search screen.
      *
-     * <p>Best effort attempt to go to the query screen (if not currently there),
-     * selects the results.
+     * <p>Best effort attempt to go to the query screen (if not currently there), selects the result
+     * that matches the String argument.
      */
-    public default void startSearch() {
+    public default void startSearch(String query) {
         throw new UnsupportedOperationException("Not yet implemented.");
     }
 
diff --git a/libraries/app-helpers/interfaces/common/src/android/platform/helpers/ISettingsIntelligenceHelper.java b/libraries/app-helpers/interfaces/common/src/android/platform/helpers/ISettingsIntelligenceHelper.java
index bb26e95..e07b800 100644
--- a/libraries/app-helpers/interfaces/common/src/android/platform/helpers/ISettingsIntelligenceHelper.java
+++ b/libraries/app-helpers/interfaces/common/src/android/platform/helpers/ISettingsIntelligenceHelper.java
@@ -19,12 +19,44 @@
 public interface ISettingsIntelligenceHelper extends IAppHelper {
 
     public static final String PAGE_ACTION_HOME = "";
+    public static final String PAGE_ACTION_ABOUT_PHONE = "android.settings.DEVICE_INFO_SETTINGS";
+    public static final String PAGE_ACTION_ACCESSIBILITY =
+            "android.settings.ACCESSIBILITY_SETTINGS";
+    public static final String PAGE_ACTION_ACCOUNT = "android.settings.SYNC_SETTINGS";
     public static final String PAGE_ACTION_APPLICATION = "android.settings.APPLICATION_SETTINGS";
+    public static final String PAGE_ACTION_APP_INFO =
+            "android.settings.APPLICATION_DETAILS_SETTINGS -d package:com.android.settings";
+    public static final String PAGE_ACTION_APP_NOTIFICATIONS =
+            "android.settings.NOTIFICATION_SETTINGS";
+    public static final String PAGE_ACTION_AUTO_ROTATE_SCREEN =
+            "android.settings.AUTO_ROTATE_SETTINGS";
     public static final String PAGE_ACTION_BATTERY = "android.intent.action.POWER_USAGE_SUMMARY";
     public static final String PAGE_ACTION_BLUETOOTH = "android.settings.BLUETOOTH_SETTINGS";
+    public static final String PAGE_ACTION_CHOOSE_SCREEN_LOCK = "android.settings.BIOMETRIC_ENROLL";
+    public static final String PAGE_ACTION_CONNECTED_CAST = "android.settings.CAST_SETTINGS";
+    public static final String PAGE_ACTION_DARK_THEME = "android.settings.DARK_THEME_SETTINGS";
+    public static final String PAGE_ACTION_DATA_SAVER = "android.settings.DATA_SAVER_SETTINGS";
+    public static final String PAGE_ACTION_DIGITAL_WELLBEING =
+            "com.google.android.apps.wellbeing/.settings.TopLevelSettingsActivity";
+    public static final String PAGE_ACTION_DISPLAY = "android.settings.DISPLAY_SETTINGS";
+    public static final String PAGE_ACTION_DO_NOT_DISTURB = "android.settings.ZEN_MODE_SETTINGS";
+    public static final String PAGE_ACTION_HOTSPOT_AND_TETHERING =
+            "android.settings.TETHER_SETTINGS";
     public static final String PAGE_ACTION_LOCATION = "android.settings.LOCATION_SOURCE_SETTINGS";
+    public static final String PAGE_ACTION_MULTIPLE_USERS = "android.settings.USER_SETTINGS";
+    public static final String PAGE_ACTION_NETWORK_INTERNET = "android.settings.WIRELESS_SETTINGS";
+    public static final String PAGE_ACTION_NIGHT_LIGHT = "android.settings.NIGHT_DISPLAY_SETTINGS";
+    public static final String PAGE_ACTION_PRIVACY = "android.settings.PRIVACY_SETTINGS";
+    public static final String PAGE_ACTION_SEARCH = "android.settings.APP_SEARCH_SETTINGS";
+    public static final String PAGE_ACTION_SECURITY = "android.settings.SECURITY_SETTINGS";
+    public static final String PAGE_ACTION_SOUND = "android.settings.SOUND_SETTINGS";
+    public static final String PAGE_ACTION_SOUND_RINGTONE = "android.intent.action.RINGTONE_PICKER";
     public static final String PAGE_ACTION_STORAGE = "android.settings.INTERNAL_STORAGE_SETTINGS";
+    public static final String PAGE_ACTION_SYSTEM_UPDATE =
+            "com.google.android.gms/.update.SystemUpdateActivity";
     public static final String PAGE_ACTION_WIFI = "android.settings.WIFI_SETTINGS";
+    public static final String PAGE_ACTION_WIFI_TETHERING_HOTSPOT =
+            "com.android.settings.WIFI_TETHER_SETTINGS";
 
     /**
      * Sets the action representing the Settings page to open when open() is called.
diff --git a/libraries/app-helpers/interfaces/common/src/android/platform/helpers/IYouTubeHelper.java b/libraries/app-helpers/interfaces/common/src/android/platform/helpers/IYouTubeHelper.java
index a71ac15..fb84f63 100644
--- a/libraries/app-helpers/interfaces/common/src/android/platform/helpers/IYouTubeHelper.java
+++ b/libraries/app-helpers/interfaces/common/src/android/platform/helpers/IYouTubeHelper.java
@@ -17,18 +17,22 @@
 package android.platform.helpers;
 
 import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiObject2;
 
 public interface IYouTubeHelper extends IAppHelper {
 
     public enum VideoQuality {
-        QUALITY_AUTO ("Auto"),
-        QUALITY_144p ("144p"),
-        QUALITY_240p ("240p"),
-        QUALITY_360p ("360p"),
-        QUALITY_480p ("480p"),
-        QUALITY_720p ("720p"),
+        QUALITY_AUTO("Auto"),
+        QUALITY_144p("144p"),
+        QUALITY_240p("240p"),
+        QUALITY_360p("360p"),
+        QUALITY_480p("480p"),
+        QUALITY_720p("720p"),
         QUALITY_1080p("1080p"),
-        QUALITY_MAX("max");
+        QUALITY_MAX("max"),
+        QUALITY_HIGHER_QUALITY("Higher picture quality"),
+        QUALITY_DATA_SAVER("Data saver"),
+        QUALITY_ADVANCED("Advanced");
 
         private final String text;
 
@@ -44,21 +48,29 @@
     /**
      * Setup expectations: YouTube app is open.
      *
-     * This method keeps pressing the back button until YouTube is on the home page.
+     * <p>This method keeps pressing the back button until YouTube is on the home page.
      */
     public void goToHomePage();
 
     /**
+     * Setup expectations: YouTube app is open.
+     *
+     * <p>This method keeps pressing the back button until library page button is accessible and
+     * clicked.
+     */
+    public void goToLibraryPage();
+
+    /**
      * Setup expectations: YouTube is on the home page.
      *
-     * This method scrolls to the top of the home page and clicks the search button.
+     * <p>This method scrolls to the top of the home page and clicks the search button.
      */
     public void goToSearchPage();
 
     /**
      * Setup expectations: YouTube is on the non-fullscreen video player page.
      *
-     * This method changes the video player to fullscreen mode. Has no effect if the video player
+     * <p>This method changes the video player to fullscreen mode. Has no effect if the video player
      * is already in fullscreen mode.
      */
     public void goToFullscreenMode();
@@ -66,21 +78,21 @@
     /**
      * Setup expectations: YouTube is on the home page.
      *
-     * This method selects a video on the home page and blocks until the video is playing.
+     * <p>This method selects a video on the home page and blocks until the video is playing.
      */
     public void playHomePageVideo();
 
     /**
      * Setup expectations: YouTube is on the search results page.
      *
-     * This method selects a search result video and blocks until the video is playing.
+     * <p>This method selects a search result video and blocks until the video is playing.
      */
     public void playSearchResultPageVideo();
 
     /**
      * Setup expectations: Recently opened a video in the YouTube app.
      *
-     * This method blocks until the video has loaded.
+     * <p>This method blocks until the video has loaded.
      *
      * @param timeout wait timeout in milliseconds
      * @return true if video loaded within the timeout, false otherwise
@@ -90,7 +102,7 @@
     /**
      * Setup expectations: Recently initiated a search query in the YouTube app.
      *
-     * This method blocks until search results appear.
+     * <p>This method blocks until search results appear.
      *
      * @param timeout wait timeout in milliseconds
      * @return true if search results appeared within timeout, false otherwise
@@ -100,9 +112,9 @@
     /**
      * Setup expectations: YouTube is on the video player page.
      *
-     * This method changes the video quality of the current video.
+     * <p>This method changes the video quality of the current video.
      *
-     * @param quality   the desired video quality
+     * @param quality the desired video quality
      * @see IYouTubeHelper.VideoQuality
      */
     public void setVideoQuality(VideoQuality quality);
@@ -110,22 +122,24 @@
     /**
      * Setup expectations: YouTube is on the video player page.
      *
-     * This method pauses the video if it is playing.
+     * <p>This method pauses the video if it is playing.
      */
     public void pauseVideo();
 
     /**
      * Setup expectations: YouTube is on the video player page.
      *
-     * This method resumes the video if it is paused.
+     * <p>This method resumes the video if it is paused.
      */
     public void resumeVideo();
 
     /**
      * Setup expectations: Search page is open.
+     *
+     * <p>This method inputs the search keyword and clicks search button.
+     *
      * <p>
-     * This method inputs the search keyword and clicks search button.
-     * <p>
+     *
      * @param query The keyword to search.
      */
     public default void searchVideo(String query) {
@@ -134,16 +148,78 @@
 
     /**
      * Setup expectations: YouTube is on the fullscreen video player page.
-     * <p>
-     * This method changes the video player to non-fullscreen mode.
+     *
+     * <p>This method changes the video player to non-fullscreen mode.
      */
     public default void exitFullScreenMode() {
         throw new UnsupportedOperationException("Not yet implemented.");
     }
+
     /**
      * Setup expectation: YouTube is open on home page.
      *
      * <p>Scroll the home page by specified direction.
+     *
+     * @param direction The direction of the scroll, must be UP or DOWN.
      */
     public void scrollHomePage(Direction direction);
+
+    /**
+     * Setup expectations: YouTube app is open on library page.
+     *
+     * <p>This method will scroll the library page in the specified direction.
+     *
+     * @param direction The direction of the scroll, must be UP or DOWN.
+     */
+    public void scrollLibraryPage(Direction direction);
+
+    /**
+     * Setup expectations: YouTube app is open.
+     *
+     * <p>This method will get the current page for scrolling. It's usually the video list.
+     *
+     * <p>The UiObject2 for YouTube to get scroll view.
+     */
+    public UiObject2 getScrollView();
+
+    /**
+     * Setup expectations: YouTube app is open.
+     *
+     * <p>This method will swipe the page in the specified direction.
+     *
+     * @param container UiObject2 of scroll container from getScrollView().
+     * @param dir Direction in which to scroll, must be UP or DOWN.
+     * @param percent Percent of page to swipe.
+     */
+    public default void swipePage(UiObject2 container, Direction dir, float percent) {
+        throw new UnsupportedOperationException("Not yet implemented.");
+    }
+
+    /**
+     * Setup expectations: YouTube is open on a page.
+     *
+     * <p>This method will fling the page as directed and block until idle.
+     */
+    public abstract void flingPage(Direction dir);
+
+    /**
+     * Setup expectations: Youtube is open.
+     *
+     * <p>This method back to top on the homepage.
+     */
+    public void backToTopOnHomepage();
+
+    /**
+     * Setup expectation: YouTube is open on home page.
+     *
+     * <p>Press home button to initiate PIP.
+     */
+    public void goToYouTubePip();
+
+    /**
+     * Setup expectation: YouTube is in PiP.
+     *
+     * <p>It presses YouTube PiP view twice to return to the main app.
+     */
+    public void backFromYouTubeFromPip();
 }
diff --git a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/ICalculatorHelper.java b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/ICalculatorHelper.java
index 72098d1..8c49ea4 100644
--- a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/ICalculatorHelper.java
+++ b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/ICalculatorHelper.java
@@ -17,4 +17,42 @@
 package android.platform.helpers;
 
 /** An App Helper interface for Calculator. */
-public interface ICalculatorHelper extends IAppHelper {}
+public interface ICalculatorHelper extends IAppHelper {
+    /*
+     * Setup expectations: Calculator is open and visible.
+     */
+    void longPressDelete();
+
+    /*
+     * Setup expectations: Calculator is open and visible.
+     *
+     * @param digits Digits (0-9) in a form of a string.
+     * @param delayBetweenKeyPresses delay between key presses in milliseconds
+     */
+    void typeDigits(String digits, long delayBetweenKeyPresses);
+
+    /*
+     * Setup expectations: Calculator is open and visible.
+     */
+    void selectEqual();
+
+    /*
+     * Setup expectations: Calculator is open and visible.
+     */
+    void selectMultiply();
+
+    /*
+     * Setup expectations: Calculator is open and visible.
+     */
+    void selectSubtract();
+
+    /*
+     * Setup expectations: Calculator is open and visible.
+     */
+    void selectAdd();
+
+    /*
+     * Setup expectations: Calculator is open and visible.
+     */
+    void selectDivide();
+}
diff --git a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/ICamera2Helper.java b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/ICamera2Helper.java
new file mode 100644
index 0000000..b12269c
--- /dev/null
+++ b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/ICamera2Helper.java
@@ -0,0 +1,127 @@
+/*
+ * 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.helpers;
+
+/** An App Helper interface for Camera2 */
+public interface ICamera2Helper extends IAppHelper {
+    /**
+     * Setup expectations: Camera2 app is open and skipped the initial dialogs.
+     *
+     * <p>Opens a stream preview of the specified camera and mode combination.
+     *
+     * @param mode - Enum value for the requested stream mode.
+     * @param camera - Enum value for the requested camera.
+     */
+    public void goToPreview(ModeType mode, CameraType camera);
+
+    /** Available modes for the stream set up. */
+    public enum ModeType {
+        VIDEO("video"),
+        STILL_CAPTURE("still-capture");
+
+        private String name;
+
+        /** Stores the provided name as an enum field. */
+        ModeType(String name) {
+            this.name = name;
+        }
+
+        /** Returns a programmer-friendly name for the {@code ModeType}. */
+        @Override
+        public String toString() {
+            return name;
+        }
+
+        /**
+         * Searches for a {@code ModeType} matching the provided name.
+         *
+         * @param name The name to search for.
+         */
+        public static ModeType fromString(String name) {
+            for (ModeType mode : values()) {
+                if (mode.toString().equals(name)) return mode;
+            }
+            throw new IllegalArgumentException();
+        }
+    };
+
+    /** Available cameras for the stream set up. */
+    public enum CameraType {
+        FRONT("front"),
+        BACK("back");
+
+        private String name;
+
+        /** Stores the provided name as an enum field. */
+        CameraType(String name) {
+            this.name = name;
+        }
+
+        /** Returns a programmer-friendly name for the {@CameraType}. */
+        @Override
+        public String toString() {
+            return name;
+        }
+
+        /**
+         * Searches for a {@CameraType} matching the provided name.
+         *
+         * @param name The name to search for.
+         */
+        public static CameraType fromString(String name) {
+            for (CameraType camera : values()) {
+                if (camera.toString().equals(name)) return camera;
+            }
+            throw new IllegalArgumentException();
+        }
+    };
+
+    /** Names for option keys that can be re-used between tests */
+    public enum TestOptionKey {
+        CAMERA_FACING("camera-facing"),
+        STREAM_DURATION("stream-duration"),
+        CAPTURE_MODE("capture-mode"),
+        RECORD_VIDEO("record-video");
+
+        private String name;
+
+        /** Stores the provided name as an enum field. */
+        TestOptionKey(String name) {
+            this.name = name;
+        }
+
+        /** Returns a more programmer-friendly name for the {@TestOptionKey}. */
+        @Override
+        public String toString() {
+            return name;
+        }
+    }
+
+    /**
+     * Setup expectations: On any video preview page and not recording a video.
+     *
+     * <p>Start recording a video
+     */
+    public void startRecording();
+
+    /**
+     * Setup expectations: Recording a video.
+     *
+     * <p>Stop recording a video
+     */
+    public void stopRecording();
+}
diff --git a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IChromeHelper.java b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IChromeHelper.java
index 0e5585b..d617276 100644
--- a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IChromeHelper.java
+++ b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IChromeHelper.java
@@ -22,6 +22,7 @@
     public enum MenuItem {
         BOOKMARKS("Bookmarks"),
         NEW_TAB("New tab"),
+        CLOSE_ALL_TABS("Close all tabs"),
         DOWNLOADS("Downloads"),
         HISTORY("History"),
         SETTINGS("Settings");
@@ -98,6 +99,34 @@
     }
 
     /**
+     * Setup expectations: Chrome is open on a page.
+     *
+     * <p>This method will add a new tab and land on the webpage of given url.
+     */
+    public abstract void addNewTab(String url);
+
+    /**
+     * Setup expectations: Chrome is open on a page.
+     *
+     * <p>This method will go to tab switcher by clicking tab switcher button.
+     */
+    public abstract void openTabSwitcher();
+
+    /**
+     * Setup expectations: Chrome is open on a page or in tab switcher.
+     *
+     * <p>This method will switch to the tab at tabIndex.
+     */
+    public abstract void switchTab(int tabIndex);
+
+    /**
+     * Setup expectations: Chrome has at least one tab.
+     *
+     * <p>This method will close all tabs.
+     */
+    public abstract void closeAllTabs();
+
+    /**
      * Setup expectations: Chrome is open on a page and the tabs are treated as apps.
      *
      * <p>This method will change the settings to treat tabs inside of Chrome and block until Chrome
diff --git a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IGmailHelper.java b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IGmailHelper.java
index aafca21..551c4ab 100644
--- a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IGmailHelper.java
+++ b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IGmailHelper.java
@@ -229,6 +229,20 @@
     public void openEmailLink(String target);
 
     /**
+     * Setup expectations: Gmail is open on any page that has a search bar on top.
+     *
+     * @param searchString string to search for in all emails
+     */
+    public void search(String searchString);
+
+    /**
+     * Setup expectations: Gmail is open and an email is open.
+     *
+     * <p>This method clicks the delete icon in an open email.
+     */
+    public void deleteCurrentEmail();
+
+    /**
      * Setup expectations: Gmail is open and an email is open.
      *
      * This method swipes the current email.
diff --git a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IGoogleCameraHelper.java b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IGoogleCameraHelper.java
index b628320..26a85a3 100644
--- a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IGoogleCameraHelper.java
+++ b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IGoogleCameraHelper.java
@@ -78,6 +78,26 @@
     public void goToFrontCamera();
 
     /**
+     * Setup expectations: GoogleCamera is open and idle in either camera/portrait/video mode.
+     *
+     * <p>This method will change to nightsight mode and block until the transition is complete.
+     */
+    public default void goToNightSightMode() {
+        throw new UnsupportedOperationException("Not yet implemented.");
+    }
+    ;
+
+    /**
+     * Setup expectations: GoogleCamera is open and idle in either camera/portrait/video mode.
+     *
+     * <p>This method will change to slow motion mode and block until the transition is complete.
+     */
+    public default void goToSlowMotionMode() {
+        throw new UnsupportedOperationException("Not yet implemented.");
+    }
+    ;
+
+    /**
      * Setup expectation: in Camera/Portrait mode with the capture button present.
      *
      * This method will capture a photo and block until the transaction is complete.
@@ -85,6 +105,18 @@
     public void capturePhoto();
 
     /**
+     * Setup expectation: in Camera mode with the capture button present.
+     *
+     * <p>This method will capture a long shot and block until the transaction is complete.
+     *
+     * @param durationMs duration of video in milliseconds
+     */
+    public default void captureLongShot(long durationMs) {
+        throw new UnsupportedOperationException("Not yet implemented.");
+    }
+    ;
+
+    /**
      * Setup expectation: in Video mode with the capture button present.
      *
      * This method will capture a video of length timeInMs and block until the transaction is
@@ -94,6 +126,18 @@
     public void captureVideo(long time);
 
     /**
+     * Clicks the shutter button without synchronizing with UI animations a number of times. Can
+     * click without waiting for button to become clickable/enabled.
+     *
+     * @param shouldWaitClickable whether to wait until button becomes clickable
+     * @param times number of times to mash button
+     */
+    public default void mashShutterButton(boolean shouldWaitClickable, int times) {
+        throw new UnsupportedOperationException("Not yet implemented.");
+    }
+    ;
+
+    /**
      * Setup expectation:
      *   1. in Video mode with the capture button present.
      *   2. videoTime > snapshotStartTime
@@ -224,4 +268,13 @@
      * @param direction scroll direction, either LEFT or RIGHT
      */
     public void scrollAlbum(Direction direction);
+
+    /**
+     * Setup expectations: Finished capture long shot in camera.
+     *
+     * <p>Clicks thumbnail of the longshot and view the top shot.
+     */
+    public default void viewTopShot(long timeout) {
+        throw new UnsupportedOperationException("Not yet implemented.");
+    }
 }
diff --git a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IGoogleHelper.java b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IGoogleHelper.java
index f6172d3..32ebe1f 100644
--- a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IGoogleHelper.java
+++ b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IGoogleHelper.java
@@ -21,6 +21,47 @@
 
 public interface IGoogleHelper extends IAppHelper {
 
+    public enum NavigationTab {
+        DISCOVER("Discover"),
+        SNAPSHOT("Snapshot"),
+        SEARCH("Search"),
+        COLLECTIONS("Collections"),
+        MORE("More");
+
+        private final String text;
+
+        NavigationTab(String text) {
+            this.text = text;
+        }
+
+        @Override
+        public String toString() {
+            return text;
+        }
+    };
+
+    public enum SearchResultTab {
+        ALL("All"),
+        IMAGES("Images"),
+        VIDEOS("Videos"),
+        NEWS("News"),
+        MAPS("Maps"),
+        BOOKS("Books"),
+        SHOPPING("Shopping"),
+        FLIGHTS("Flights");
+
+        private final String mDisplayName;
+
+        SearchResultTab(String displayName) {
+            mDisplayName = displayName;
+        }
+
+        @Override
+        public String toString() {
+            return mDisplayName;
+        }
+    }
+
     /**
      * Setup expectations: Google app open
      *
@@ -38,6 +79,13 @@
     public String getSearchQuery();
 
     /**
+     * Setup expectations: Google app open.
+     *
+     * <p>This method goes to the specified tab, such as Discover, Search, Collections.
+     */
+    public void navigateToTab(NavigationTab tab);
+
+    /**
      * Setup expectations: In home and with search bar.
      *
      * <p>This method starts search from the search bar in home.
@@ -78,6 +126,17 @@
     }
 
     /**
+     * Setup expectations: Google app open to a search result.
+     *
+     * <p>This method scroll the search results.
+     *
+     * @param dir The direction of the fling, must be UP or DOWN.
+     */
+    public default void scrollSearchResults(Direction dir) {
+        throw new UnsupportedOperationException("Not yet implemented.");
+    }
+
+    /**
      * Setup expectations: In home.
      *
      * <p>This method flings right to Google Feed.
@@ -126,6 +185,15 @@
     public void scrollFeed(UiObject2 container, Direction dir, int speed);
 
     /**
+     * Setup expectations: Google app open and has done a search.
+     *
+     * <p>This method will go to the specified search result tab.
+     *
+     * @param tab one of the tabs of the search result page.
+     */
+    public abstract void openSearchResultTab(IGoogleHelper.SearchResultTab tab);
+
+    /**
      * Setup expectations: In home.
      *
      * <p>This method clear search result.
diff --git a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/INotificationHelper.java b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/INotificationHelper.java
index 704874a..4f65454 100644
--- a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/INotificationHelper.java
+++ b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/INotificationHelper.java
@@ -31,12 +31,14 @@
     String UI_NOTIFICATION_ID = "status_bar_latest_event_content";
     String NOTIFICATION_TITLE_TEXT = "TEST NOTIFICATION";
     String NOTIFICATION_CONTENT_TEXT = "Test notification content";
-    String NOTIFICATION_BIG_TEXT = "lorem ipsum dolor sit amet\n"
-            + "lorem ipsum dolor sit amet\n"
-            + "lorem ipsum dolor sit amet\n"
-            + "lorem ipsum dolor sit amet";
+    String NOTIFICATION_BIG_TEXT =
+            "lorem ipsum dolor sit amet\n"
+                    + "lorem ipsum dolor sit amet\n"
+                    + "lorem ipsum dolor sit amet\n"
+                    + "lorem ipsum dolor sit amet";
     String NOTIFICATION_CHANNEL_NAME = "Test Channel";
     String EXPAND_BUTTON_ID = "expand_button";
+    String BUBBLE_BUTTON = "bubble_button";
     String APP_ICON_ID = "icon";
 
     /**
@@ -53,9 +55,9 @@
     /**
      * Setup Expectations: None
      *
-     * <p>Posts a number of notifications to the device. Successive calls to this should post
-     * new notifications to those previously posted. Note that this may fail if the helper has
-     * surpassed the system-defined limit for per-package notifications.
+     * <p>Posts a number of notifications to the device. Successive calls to this should post new
+     * notifications to those previously posted. Note that this may fail if the helper has surpassed
+     * the system-defined limit for per-package notifications.
      *
      * @param count The number of notifications to post.
      */
@@ -99,6 +101,28 @@
     /**
      * Setup Expectations: Shade is open
      *
+     * <p>Posts a bubble notification. This notification is associated with a conversation shortcut,
+     * a BubbleMetadata, and in {@link android.app.Notification.MessagingStyle}.
+     *
+     * @param senderName Name of notification sender.
+     * @param count How many bubble notifications to send.
+     */
+    default void postBubbleNotification(String senderName, int count) {
+        throw new UnsupportedOperationException("Not yet implemented.");
+    }
+
+    /**
+     * Return notification if found by text.
+     *
+     * @param text Text that notification contains.
+     */
+    default UiObject2 getNotificationByText(String text) {
+        throw new UnsupportedOperationException("Not yet implemented.");
+    }
+
+    /**
+     * Setup Expectations: Shade is open
+     *
      * <p>Posts a conversation notification. This notification is associated with a conversation
      * shortcut and in {@link android.app.Notification.MessagingStyle}.
      *
@@ -158,6 +182,31 @@
     }
 
     /**
+     * Setup Expectations: Notification shade opened.
+     *
+     * <p>Scrolls to the bottom of the notification shade and taps the "clear all" button if
+     * present.
+     */
+    default void clearAllNotifications() {
+        throw new UnsupportedOperationException("Not yet implemented.");
+    }
+
+    /**
+     * Setup Expectations: Notification snoozing is enabled.
+     *
+     * <p>Empty the notification shade by first cancelling all of the test app's notifications, then
+     * snoozing all other notifications temporarily. Fails if any notifications are left in the
+     * shade.
+     *
+     * <p>Because unsnoozing can fail from command line, snoozing is implemented with a fixed time,
+     * so tests will take at least as long as the time limit given, plus some buffer built into this
+     * utility, to ensure that this test's snoozing will not interfere with other tests.
+     */
+    default void runWithEmptyNotificationShade(Runnable task, long taskTimeLimit) {
+        throw new UnsupportedOperationException("Not yet implemented.");
+    }
+
+    /**
      * Setup expectations: Notification shade opened.
      *
      * <p>Opens the first notification by the specified title and checks if the expected application
@@ -174,8 +223,8 @@
     /**
      * Setup expectations: Notification shade opened.
      *
-     * <p>Taps the chevron or swipes down on the specified notification and checks if the
-     * expanded view contains the expected text.
+     * <p>Taps the chevron or swipes down on the specified notification and checks if the expanded
+     * view contains the expected text.
      *
      * @param notification Notification that should be expanded.
      * @param dragging By swiping down when {@code true}, by tapping the chevron otherwise.
@@ -185,6 +234,15 @@
     }
 
     /**
+     * Sets expected app name for notifications.
+     *
+     * @param appName Package name.
+     */
+    default void setAppName(String appName) {
+        throw new UnsupportedOperationException("Not yet implemented.");
+    }
+
+    /**
      * Long press on notification to show its hidden menu (a.k.a. guts)
      *
      * @param notification Notification.
@@ -222,6 +280,16 @@
     }
 
     /**
+     * Setup expectation: On the expanding notification screen.
+     *
+     * <p>Get the UiObject2 of Quick Settings container. Quick settings container works both in
+     * expanded and collapsed state, that is contains both QuickQuickSettings and QuickSettings
+     */
+    default UiObject2 getQuickSettingsContainer() {
+        throw new UnsupportedOperationException("Not yet implemented.");
+    }
+
+    /**
      * Scroll feeds on Notifications screen
      *
      * <p>Setup expectations: Notification is open with lots of notifications.
@@ -234,7 +302,7 @@
     }
 
     /**
-     * Scroll feeds on Notifications screen
+     * Scroll feeds on Notifications screen and implement it via "fling" API.
      *
      * <p>Setup expectations: Notification is open with lots of notifications.
      *
@@ -245,4 +313,26 @@
     default void flingFeed(UiObject2 container, Direction dir, int speed) {
         throw new UnsupportedOperationException("Not yet implemented.");
     }
+
+    /**
+     * Setup expectation: on the launcher home screen.
+     *
+     * <p>Open the notification shade by swiping on the home screen.
+     */
+    default void swipeToOpen() {
+        throw new UnsupportedOperationException("Not yet implemented.");
+    }
+
+    /**
+     * Scroll feeds on Notifications screen and implement it by "swipe" API to control the distance.
+     *
+     * <p>Setup expectations: Notification drawer is open with lots of notifications.
+     *
+     * @param container the container with scrollable elements.
+     * @param dir the direction to scroll, must be UP or DOWN.
+     * @param percent The distance to scroll as a percentage of the page's visible size.
+     */
+    default void scrollFeed(UiObject2 container, Direction dir, float percent) {
+        throw new UnsupportedOperationException("Not yet implemented.");
+    }
 }
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 b167511..91c5186 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
@@ -60,6 +60,13 @@
     public void goToMainScreen();
 
     /**
+     * Setup expectation: Photos is open.
+     *
+     * <p>This method will go to the top of the main screen.
+     */
+    public void goToTopOfMainScreen();
+
+    /**
      * Setup expectations: Photos is open.
      *
      * <p>This method will go to device folder screen.
@@ -144,6 +151,19 @@
             UiObject2 container, Direction direction, float percentOfPhotoSize, int speed);
 
     /**
+     * Setup expectations: Photos is open and a page contains pictures or albums is open.
+     *
+     * <p>This method will swipe to scroll the page in the specified direction.
+     *
+     * <p>This method needs to check the UI object additionally.
+     *
+     * @param container The container with scrollable elements.
+     * @param direction The direction of the scroll, must be UP or DOWN.
+     * @param percent The distance of scroll as a percentage of Photo size.
+     */
+    public void scrollPhotosGridBySwiping(UiObject2 container, Direction direction, float percent);
+
+    /**
      * Setup expectation: Photos is open and a page contains pictures or albums is open.
      *
      * <p>Get the UiObject2 of photo scroll view pattern.
diff --git a/libraries/audio-test-harness/OWNERS b/libraries/audio-test-harness/OWNERS
new file mode 100644
index 0000000..6da066e
--- /dev/null
+++ b/libraries/audio-test-harness/OWNERS
@@ -0,0 +1,3 @@
+# Audio Test Harness is owned by the Android Media Framework EngProd team (amf-engprod@).
+jingweiguo@google.com
+karlshaffer@google.com
diff --git a/libraries/audio-test-harness/client-lib/Android.bp b/libraries/audio-test-harness/client-lib/Android.bp
new file mode 100644
index 0000000..118ce81
--- /dev/null
+++ b/libraries/audio-test-harness/client-lib/Android.bp
@@ -0,0 +1,84 @@
+/*
+ * 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.
+ */
+
+// Targets for the Audio Test Harness Client Libraries.
+// Clients can be used host or device side to communicate with an Audio
+// Test Harness Server.
+// LIBRARIES ==============================================================
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+    name: "audiotestharness-client-clientlib",
+    host_supported: true,
+    srcs: [
+        "src/main/java/com/android/media/audiotestharness/client/*.java",
+    ],
+    sdk_version: "current",
+}
+
+java_library {
+    name: "audiotestharness-client-corelib",
+    host_supported: true,
+    srcs: [
+        "src/main/java/com/android/media/audiotestharness/client/core/*.java",
+    ],
+    static_libs: [
+        "audiotestharness-commonlib-lite",
+        "audiotestharness-commonprotolib-lite",
+    ],
+    sdk_version: "current",
+}
+
+java_library {
+    name: "audiotestharness-client-grpclib",
+    host_supported: true,
+    srcs: [
+        "src/main/java/com/android/media/audiotestharness/client/grpc/*.java",
+    ],
+    static_libs: [
+        "audiotestharness-client-corelib",
+        "grpc-java-okhttp-client-lite",
+        "audiotestharness-servicegrpclib-lite",
+        "guava",
+    ],
+    sdk_version: "current",
+}
+
+// TESTS ==============================================================
+
+java_test {
+    name: "audiotestharness-client-grpclib-tests",
+    test_suites: ["general-tests"],
+    host_supported: true,
+    srcs: [
+        "src/test/java/com/android/media/audiotestharness/client/grpc/*.java",
+    ],
+    static_libs: [
+        "audiotestharness-client-grpclib",
+        "grpc-java-core-inprocess",
+        "junit",
+        "junit-params",
+        "mockito",
+        "objenesis",
+    ],
+    sdk_version: "current",
+    test_options: {
+        unit_test: true,
+    },
+}
diff --git a/libraries/audio-test-harness/client-lib/src/main/java/com/android/media/audiotestharness/client/core/AudioCaptureStream.java b/libraries/audio-test-harness/client-lib/src/main/java/com/android/media/audiotestharness/client/core/AudioCaptureStream.java
new file mode 100644
index 0000000..064970a
--- /dev/null
+++ b/libraries/audio-test-harness/client-lib/src/main/java/com/android/media/audiotestharness/client/core/AudioCaptureStream.java
@@ -0,0 +1,37 @@
+/*
+ * 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.media.audiotestharness.client.core;
+
+import com.android.media.audiotestharness.common.Defaults;
+import com.android.media.audiotestharness.proto.AudioFormatOuterClass.AudioFormat;
+
+import java.io.InputStream;
+
+/**
+ * {@link InputStream} that provides access to raw audio samples returned by a Audio Capture Session
+ * while also providing access to helper methods to interact with the session.
+ */
+public abstract class AudioCaptureStream extends InputStream {
+
+    /**
+     * Returns the {@link AudioFormat} corresponding to this {@link AudioCaptureStream}, thus the
+     * raw data exposed by this stream will be raw PCM samples matching this format.
+     */
+    public AudioFormat getAudioFormat() {
+        return Defaults.AUDIO_FORMAT;
+    }
+}
diff --git a/libraries/audio-test-harness/client-lib/src/main/java/com/android/media/audiotestharness/client/core/AudioTestHarnessClient.java b/libraries/audio-test-harness/client-lib/src/main/java/com/android/media/audiotestharness/client/core/AudioTestHarnessClient.java
new file mode 100644
index 0000000..efc2f75
--- /dev/null
+++ b/libraries/audio-test-harness/client-lib/src/main/java/com/android/media/audiotestharness/client/core/AudioTestHarnessClient.java
@@ -0,0 +1,58 @@
+/*
+ * 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.media.audiotestharness.client.core;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * API for the Audio Test Harness infrastructure.
+ *
+ * <p>Exposes methods for starting interactions with the infrastructure, then the returned can be
+ * used for interaction with each created session.
+ *
+ * <p>Implementing classes should manage the resources for both the standard connection to the
+ * host-side server but also any resources for the underlying sessions created within the system.
+ */
+public abstract class AudioTestHarnessClient implements AutoCloseable {
+
+    /**
+     * {@link Set} of all {@link AudioCaptureStream}s created by this client so that when the client
+     * is closed, all of the associated streams can also be closed.
+     */
+    protected Set<AudioCaptureStream> mAudioCaptureStreams;
+
+    protected AudioTestHarnessClient() {
+        mAudioCaptureStreams = new HashSet<>();
+    }
+
+    /**
+     * Starts an Audio Capture Session and returns a {@link AudioCaptureStream} for interacting with
+     * that session.
+     */
+    public abstract AudioCaptureStream startCapture();
+
+    /**
+     * Tears down the Audio Test Harness by disconnecting from the host-side service and tearing
+     * down any associated resources managed by this class.
+     *
+     * <p>Additionally, any open sessions created by one of the start methods should be closed at
+     * this point as well.
+     */
+    @Override
+    public abstract void close();
+}
diff --git a/libraries/audio-test-harness/client-lib/src/main/java/com/android/media/audiotestharness/client/grpc/GrpcAudioCaptureStream.java b/libraries/audio-test-harness/client-lib/src/main/java/com/android/media/audiotestharness/client/grpc/GrpcAudioCaptureStream.java
new file mode 100644
index 0000000..4c7b909
--- /dev/null
+++ b/libraries/audio-test-harness/client-lib/src/main/java/com/android/media/audiotestharness/client/grpc/GrpcAudioCaptureStream.java
@@ -0,0 +1,85 @@
+/*
+ * 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.media.audiotestharness.client.grpc;
+
+import com.android.media.audiotestharness.client.core.AudioCaptureStream;
+import com.android.media.audiotestharness.proto.AudioTestHarnessGrpc;
+
+import java.io.IOException;
+
+/** {@link AudioCaptureStream} that utilizes gRPC as its transfer mechanism. */
+public class GrpcAudioCaptureStream extends AudioCaptureStream {
+
+    // TODO(b/168817017): Implement this class to utlize gRPC and Piped I/O streams to provide an
+    // InputStream from which the audio samples can be read.
+
+    private final AudioTestHarnessGrpc.AudioTestHarnessStub mAudioTestHarnessStub;
+
+    private GrpcAudioCaptureStream(AudioTestHarnessGrpc.AudioTestHarnessStub audioTestHarnessStub) {
+        mAudioTestHarnessStub = audioTestHarnessStub;
+    }
+
+    static GrpcAudioCaptureStream create(
+            AudioTestHarnessGrpc.AudioTestHarnessStub audioTestHarnessStub) {
+        return new GrpcAudioCaptureStream(audioTestHarnessStub);
+    }
+
+    @Override
+    public int read(byte[] b) throws IOException {
+        return super.read(b);
+    }
+
+    @Override
+    public int read(byte[] b, int off, int len) throws IOException {
+        return super.read(b, off, len);
+    }
+
+    @Override
+    public long skip(long n) throws IOException {
+        return super.skip(n);
+    }
+
+    @Override
+    public int available() throws IOException {
+        return super.available();
+    }
+
+    @Override
+    public void close() throws IOException {
+        super.close();
+    }
+
+    @Override
+    public synchronized void mark(int readlimit) {
+        super.mark(readlimit);
+    }
+
+    @Override
+    public synchronized void reset() throws IOException {
+        super.reset();
+    }
+
+    @Override
+    public boolean markSupported() {
+        return super.markSupported();
+    }
+
+    @Override
+    public int read() throws IOException {
+        return 0;
+    }
+}
diff --git a/libraries/audio-test-harness/client-lib/src/main/java/com/android/media/audiotestharness/client/grpc/GrpcAudioCaptureStreamFactory.java b/libraries/audio-test-harness/client-lib/src/main/java/com/android/media/audiotestharness/client/grpc/GrpcAudioCaptureStreamFactory.java
new file mode 100644
index 0000000..5d2fd46
--- /dev/null
+++ b/libraries/audio-test-harness/client-lib/src/main/java/com/android/media/audiotestharness/client/grpc/GrpcAudioCaptureStreamFactory.java
@@ -0,0 +1,36 @@
+/*
+ * 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.media.audiotestharness.client.grpc;
+
+import com.android.media.audiotestharness.proto.AudioTestHarnessGrpc;
+
+import com.google.common.base.Preconditions;
+
+/** Factory for the {@link GrpcAudioCaptureStream}. */
+public class GrpcAudioCaptureStreamFactory {
+    private GrpcAudioCaptureStreamFactory() {}
+
+    public static GrpcAudioCaptureStreamFactory create() {
+        return new GrpcAudioCaptureStreamFactory();
+    }
+
+    GrpcAudioCaptureStream newStream(
+            AudioTestHarnessGrpc.AudioTestHarnessStub audioTestHarnessStub) {
+        Preconditions.checkNotNull(audioTestHarnessStub, "audioTestHarnessStub cannot be null");
+        return GrpcAudioCaptureStream.create(audioTestHarnessStub);
+    }
+}
diff --git a/libraries/audio-test-harness/client-lib/src/main/java/com/android/media/audiotestharness/client/grpc/GrpcAudioTestHarnessClient.java b/libraries/audio-test-harness/client-lib/src/main/java/com/android/media/audiotestharness/client/grpc/GrpcAudioTestHarnessClient.java
new file mode 100644
index 0000000..b8fa93b
--- /dev/null
+++ b/libraries/audio-test-harness/client-lib/src/main/java/com/android/media/audiotestharness/client/grpc/GrpcAudioTestHarnessClient.java
@@ -0,0 +1,147 @@
+/*
+ * 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.media.audiotestharness.client.grpc;
+
+import com.android.media.audiotestharness.client.core.AudioCaptureStream;
+import com.android.media.audiotestharness.client.core.AudioTestHarnessClient;
+import com.android.media.audiotestharness.proto.AudioTestHarnessGrpc;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+
+import io.grpc.ManagedChannel;
+import io.grpc.ManagedChannelBuilder;
+
+import java.io.IOException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/** {@link AudioTestHarnessClient} that uses gRPC as its communication method. */
+public class GrpcAudioTestHarnessClient extends AudioTestHarnessClient {
+    private static final Logger LOGGER =
+            Logger.getLogger(GrpcAudioTestHarnessClient.class.getName());
+
+    private static final int MIN_PORT = 0;
+    private static final int MAX_PORT = 65535;
+    private static final int DEFAULT_NUM_THREADS = 8;
+
+    private final ManagedChannel mManagedChannel;
+    private final GrpcAudioCaptureStreamFactory mGrpcAudioCaptureStreamFactory;
+
+    private GrpcAudioTestHarnessClient(
+            GrpcAudioCaptureStreamFactory grpcAudioCaptureStreamFactory,
+            ManagedChannel managedChannel) {
+        mManagedChannel = managedChannel;
+        mGrpcAudioCaptureStreamFactory = grpcAudioCaptureStreamFactory;
+    }
+
+    public static GrpcAudioTestHarnessClient.Builder builder() {
+        return new Builder().setCaptureStreamFactory(GrpcAudioCaptureStreamFactory.create());
+    }
+
+    @Override
+    public AudioCaptureStream startCapture() {
+        AudioCaptureStream newStream =
+                mGrpcAudioCaptureStreamFactory.newStream(
+                        AudioTestHarnessGrpc.newStub(mManagedChannel));
+
+        mAudioCaptureStreams.add(newStream);
+
+        return newStream;
+    }
+
+    @Override
+    public void close() {
+        mAudioCaptureStreams.forEach(
+                stream -> {
+                    try {
+                        stream.close();
+                    } catch (IOException e) {
+                        LOGGER.log(Level.WARNING, "Failed to close AudioCaptureStream", e);
+                    }
+                });
+
+        mManagedChannel.shutdown();
+    }
+
+    /**
+     * Builder for {@link GrpcAudioTestHarnessClient}s that allows for the injection of certain
+     * members for testing purposes.
+     */
+    public static class Builder {
+
+        private String mHostname;
+        private int mPort;
+        private Executor mExecutor;
+        private GrpcAudioCaptureStreamFactory mGrpcAudioCaptureStreamFactory;
+        private ManagedChannel mManagedChannel;
+
+        private Builder() {}
+
+        public Builder setAddress(String hostname, int port) {
+            Preconditions.checkNotNull(hostname, "Hostname cannot be null");
+            Preconditions.checkArgument(
+                    port >= MIN_PORT && port <= MAX_PORT,
+                    String.format("Port expected in range [%d, %d]", MIN_PORT, MAX_PORT));
+
+            mHostname = hostname;
+            mPort = port;
+
+            return this;
+        }
+
+        public Builder setExecutor(Executor executor) {
+            mExecutor = executor;
+            return this;
+        }
+
+        @VisibleForTesting
+        Builder setCaptureStreamFactory(
+                GrpcAudioCaptureStreamFactory grpcAudioCaptureStreamFactory) {
+            Preconditions.checkNotNull(
+                    grpcAudioCaptureStreamFactory, "grpcAudioCaptureStreamFactory cannot be null");
+            mGrpcAudioCaptureStreamFactory = grpcAudioCaptureStreamFactory;
+            return this;
+        }
+
+        @VisibleForTesting
+        Builder setManagedChannel(ManagedChannel managedChannel) {
+            mManagedChannel = managedChannel;
+            return this;
+        }
+
+        public GrpcAudioTestHarnessClient build() {
+            if (mManagedChannel == null) {
+                Preconditions.checkState(mHostname != null, "Address must be set.");
+
+                if (mExecutor == null) {
+                    mExecutor = Executors.newFixedThreadPool(DEFAULT_NUM_THREADS);
+                }
+
+                mManagedChannel =
+                        ManagedChannelBuilder.forAddress(mHostname, mPort)
+                                .executor(mExecutor)
+                                .usePlaintext()
+                                .build();
+            }
+
+            return new GrpcAudioTestHarnessClient(mGrpcAudioCaptureStreamFactory, mManagedChannel);
+        }
+    }
+}
diff --git a/libraries/audio-test-harness/client-lib/src/test/java/com/android/media/audiotestharness/client/grpc/GrpcAudioCaptureStreamTests.java b/libraries/audio-test-harness/client-lib/src/test/java/com/android/media/audiotestharness/client/grpc/GrpcAudioCaptureStreamTests.java
new file mode 100644
index 0000000..b490169
--- /dev/null
+++ b/libraries/audio-test-harness/client-lib/src/test/java/com/android/media/audiotestharness/client/grpc/GrpcAudioCaptureStreamTests.java
@@ -0,0 +1,40 @@
+/*
+ * 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.media.audiotestharness.client.grpc;
+
+import static org.junit.Assert.assertNotNull;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class GrpcAudioCaptureStreamTests {
+
+    private GrpcAudioCaptureStreamFactory mGrpcAudioCaptureStreamFactory;
+
+    @Before
+    public void setUp() throws Exception {
+        mGrpcAudioCaptureStreamFactory = GrpcAudioCaptureStreamFactory.create();
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void create_throwsNullPointerException_nullStub() throws Exception {
+        assertNotNull(mGrpcAudioCaptureStreamFactory.newStream(/* audioTestHarnessStub= */ null));
+    }
+}
diff --git a/libraries/audio-test-harness/client-lib/src/test/java/com/android/media/audiotestharness/client/grpc/GrpcAudioTestHarnessClientTests.java b/libraries/audio-test-harness/client-lib/src/test/java/com/android/media/audiotestharness/client/grpc/GrpcAudioTestHarnessClientTests.java
new file mode 100644
index 0000000..0beb234
--- /dev/null
+++ b/libraries/audio-test-harness/client-lib/src/test/java/com/android/media/audiotestharness/client/grpc/GrpcAudioTestHarnessClientTests.java
@@ -0,0 +1,143 @@
+/*
+ * 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.media.audiotestharness.client.grpc;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.android.media.audiotestharness.proto.AudioTestHarnessGrpc;
+
+import io.grpc.Channel;
+import io.grpc.ManagedChannel;
+import io.grpc.ManagedChannelBuilder;
+
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.concurrent.Executors;
+
+@RunWith(JUnitParamsRunner.class)
+public class GrpcAudioTestHarnessClientTests {
+
+    @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+    @Mock GrpcAudioCaptureStreamFactory mGrpcAudioCaptureStreamFactory;
+
+    @Mock GrpcAudioCaptureStream mGrpcAudioCaptureStream;
+
+    @Mock ManagedChannel mManagedChannel;
+
+    /** Tests for the {@link GrpcAudioTestHarnessClient.Builder} */
+    @Test(expected = NullPointerException.class)
+    public void setAddress_throwsNullPointerException_nullHostname() throws Exception {
+        GrpcAudioTestHarnessClient.builder().setAddress(/* hostname= */ null, /* port= */ 8080);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void setAddress_throwsIllegalArgumentException_badPort() throws Exception {
+        GrpcAudioTestHarnessClient.builder().setAddress("localhost", /* port= */ -123);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void build_throwsIllegalStateException_hostNotSet() throws Exception {
+        GrpcAudioTestHarnessClient.builder().build();
+    }
+
+    @Test
+    @Parameters(method = "getBuildParameters")
+    public void build_returnsNonNullInstance_validParameters(
+            GrpcAudioTestHarnessClient.Builder builder) throws Exception {
+        assertNotNull(builder.build());
+    }
+
+    public static Object[] getBuildParameters() {
+        return new Object[][] {
+            {GrpcAudioTestHarnessClient.builder().setAddress("localhost", /* port= */ 8080)},
+            {
+                GrpcAudioTestHarnessClient.builder()
+                        .setAddress("service.google.com", 49152)
+                        .setExecutor(Executors.newSingleThreadExecutor())
+            },
+            {
+                GrpcAudioTestHarnessClient.builder()
+                        .setManagedChannel(
+                                ManagedChannelBuilder.forAddress("localhost", /* port= */ 8080)
+                                        .usePlaintext()
+                                        .build())
+            }
+        };
+    }
+
+    @Test
+    public void startCapture_returnsNonNullAudioCaptureStream() throws Exception {
+        GrpcAudioTestHarnessClient client = initMocksAndClient();
+        assertNotNull(client.startCapture());
+    }
+
+    @Test
+    public void startCapture_createsNewStubWithProperChannel() throws Exception {
+        ArgumentCaptor<AudioTestHarnessGrpc.AudioTestHarnessStub>
+                audioTestHarnessStubArgumentCaptor =
+                        ArgumentCaptor.forClass(AudioTestHarnessGrpc.AudioTestHarnessStub.class);
+        GrpcAudioTestHarnessClient client = initMocksAndClient();
+
+        client.startCapture();
+
+        verify(mGrpcAudioCaptureStreamFactory)
+                .newStream(audioTestHarnessStubArgumentCaptor.capture());
+        Channel channel = audioTestHarnessStubArgumentCaptor.getValue().getChannel();
+        assertEquals(mManagedChannel, channel);
+    }
+
+    @Test
+    public void close_closesManagedChannel() throws Exception {
+        GrpcAudioTestHarnessClient client = initMocksAndClient();
+
+        client.close();
+
+        verify(mManagedChannel).shutdown();
+    }
+
+    @Test
+    public void close_closesCreatedAudioCaptureStreams() throws Exception {
+        GrpcAudioTestHarnessClient client = initMocksAndClient();
+        client.startCapture();
+
+        client.close();
+
+        verify(mGrpcAudioCaptureStream).close();
+    }
+
+    public GrpcAudioTestHarnessClient initMocksAndClient() {
+        when(mGrpcAudioCaptureStreamFactory.newStream(any())).thenReturn(mGrpcAudioCaptureStream);
+        return GrpcAudioTestHarnessClient.builder()
+                .setManagedChannel(mManagedChannel)
+                .setCaptureStreamFactory(mGrpcAudioCaptureStreamFactory)
+                .build();
+    }
+}
diff --git a/libraries/audio-test-harness/common/Android.bp b/libraries/audio-test-harness/common/Android.bp
new file mode 100644
index 0000000..77dc748
--- /dev/null
+++ b/libraries/audio-test-harness/common/Android.bp
@@ -0,0 +1,53 @@
+/*
+ * 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.
+ */
+
+// Targets for the Audio Test Harness Common Libraries.
+// These are libraries that can be used either host-side or device-side.
+// LIBRARIES ==============================================================
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+    name: "audiotestharness-commonlib-lite",
+    host_supported: true,
+    srcs: [
+        "src/main/java/com/android/media/audiotestharness/common/Defaults.java",
+    ],
+    libs: [
+        "audiotestharness-commonprotolib-lite",
+    ],
+    java_version: "1.8",
+    sdk_version: "current",
+    proto: {
+        type: "lite",
+    },
+}
+
+java_library_host {
+    name: "audiotestharness-commonlib-full",
+    srcs: [
+        "src/main/java/com/android/media/audiotestharness/common/Defaults.java",
+    ],
+    libs: [
+        "audiotestharness-commonprotolib-full",
+    ],
+    java_version: "1.8",
+    proto: {
+        type: "full",
+    },
+}
diff --git a/libraries/audio-test-harness/common/src/main/java/com/android/media/audiotestharness/common/Defaults.java b/libraries/audio-test-harness/common/src/main/java/com/android/media/audiotestharness/common/Defaults.java
new file mode 100644
index 0000000..7ee2a8a
--- /dev/null
+++ b/libraries/audio-test-harness/common/src/main/java/com/android/media/audiotestharness/common/Defaults.java
@@ -0,0 +1,54 @@
+/*
+ * 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.media.audiotestharness.common;
+
+import com.android.media.audiotestharness.proto.AudioDeviceOuterClass.AudioDevice;
+import com.android.media.audiotestharness.proto.AudioFormatOuterClass.AudioFormat;
+
+/**
+ * Contains all the defaults for the Audio Test Harness system shared between the client and server
+ * libraries.
+ */
+public final class Defaults {
+
+    /**
+     * The default {@link AudioFormat} used for capture.
+     *
+     * <p>Currently set to CD format and quality, thus audio samples are captured as single-channel,
+     * signed, little-endian samples at 16-bit 44100hz.
+     */
+    public static final AudioFormat AUDIO_FORMAT =
+            AudioFormat.newBuilder()
+                    .setSampleRate(44100.0f)
+                    .setSampleSizeBits(16)
+                    .setChannels(1)
+                    .setSigned(true)
+                    .setBigEndian(false)
+                    .build();
+
+    /**
+     * The default {@link AudioDevice} used for capture.
+     *
+     * <p>Currently set to a Dayton UMM-6 reference microphone which are commonly used by existing
+     * audio boxes.
+     */
+    public static final AudioDevice AUDIO_DEVICE =
+            AudioDevice.newBuilder()
+                    .setName("UMM6 [plughw:0,0]")
+                    .addCapabilities(AudioDevice.Capability.CAPTURE)
+                    .build();
+}
diff --git a/libraries/audio-test-harness/proto/Android.bp b/libraries/audio-test-harness/proto/Android.bp
new file mode 100644
index 0000000..631618d
--- /dev/null
+++ b/libraries/audio-test-harness/proto/Android.bp
@@ -0,0 +1,156 @@
+/*
+ * 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.
+ */
+
+// Proto Targets for the Audio Test Harness System.
+// These protos are used on both the host and device side and thus are
+// included in both lite and full variants.
+// DEFINITIONS ==============================================================
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+PROTO_TOOLS = [
+    "aprotoc",
+    "protoc-gen-grpc-java-plugin",
+    "soong_zip",
+]
+FULL_PROTO_CMD = "mkdir -p $(genDir)/gen && " +
+    "$(location aprotoc) -Iplatform_testing/libraries -Iexternal/protobuf/src " +
+    "--plugin=protoc-gen-grpc-java=$(location protoc-gen-grpc-java-plugin) --grpc-java_out=$(genDir)/gen $(in) && " +
+    "$(location soong_zip) -o $(out) -C $(genDir)/gen -D $(genDir)/gen"
+LITE_PROTO_CMD = "mkdir -p $(genDir)/gen && " +
+    "$(location aprotoc) --java_opt=annotate_code=false -Iplatform_testing/libraries -Iexternal/protobuf/src " +
+    "--plugin=protoc-gen-grpc-java=$(location protoc-gen-grpc-java-plugin) --grpc-java_out=lite:$(genDir)/gen $(in) && " +
+    "$(location soong_zip) -o $(out) -C $(genDir)/gen -D $(genDir)/gen"
+
+// PROTO SOURCE ==============================================================
+
+// Common protos shared between the server and client libraries as model classes.
+filegroup {
+    name: "audiotestharness-commonproto",
+    srcs: [
+        "audio_device.proto",
+        "audio_format.proto",
+    ],
+}
+
+// The protos for the Audio Test Harness service definition.
+filegroup {
+    name: "audiotestharness-serviceproto",
+    srcs: [
+        "audio_test_harness_service.proto",
+    ],
+}
+
+// GRPC GENERATED ==============================================================
+
+// The generated gRPC Stub based on the above service definition.
+genrule {
+    name: "audiotestharness-servicestub-full",
+    tools: PROTO_TOOLS,
+    cmd: FULL_PROTO_CMD,
+    srcs: [
+        ":audiotestharness-serviceproto",
+    ],
+    out: [
+        "protos.srcjar",
+    ],
+}
+
+genrule {
+    name: "audiotestharness-servicestub-lite",
+    tools: PROTO_TOOLS,
+    cmd: LITE_PROTO_CMD,
+    srcs: [
+        ":audiotestharness-serviceproto",
+    ],
+    out: [
+        "protos.srcjar",
+    ],
+}
+
+// LIBRARIES ==============================================================
+
+java_library_host {
+    name: "audiotestharness-commonprotolib-full",
+    srcs: [
+        ":audiotestharness-commonproto",
+    ],
+    static_libs: [
+        "libprotobuf-java-full",
+    ],
+    proto: {
+        type: "full",
+    },
+}
+
+java_library {
+    name: "audiotestharness-commonprotolib-lite",
+    host_supported: true,
+    srcs: [
+        ":audiotestharness-commonproto",
+    ],
+    static_libs: [
+        "libprotobuf-java-lite",
+    ],
+    proto: {
+        type: "lite",
+    },
+    sdk_version: "current",
+}
+
+java_library_host {
+    name: "audiotestharness-servicegrpclib-full",
+    srcs: [
+        ":audiotestharness-servicestub-full",
+        ":audiotestharness-serviceproto",
+    ],
+    libs: [
+        "javax_annotation-api_1.3.2",
+    ],
+    static_libs: [
+        "libprotobuf-java-full",
+        "grpc-java",
+        "opencensus-java-api",
+        "opencensus-java-contrib-grpc-metrics",
+    ],
+    proto: {
+        type: "full",
+    },
+}
+
+java_library {
+    name: "audiotestharness-servicegrpclib-lite",
+    host_supported: true,
+    srcs: [
+        ":audiotestharness-servicestub-lite",
+        ":audiotestharness-serviceproto",
+    ],
+    libs: [
+        "javax_annotation-api_1.3.2",
+    ],
+    static_libs: [
+        "libprotobuf-java-lite",
+        "grpc-java-okhttp-client-lite",
+        "opencensus-java-api",
+        "opencensus-java-contrib-grpc-metrics",
+    ],
+    proto: {
+        type: "lite",
+    },
+    sdk_version: "current",
+}
diff --git a/libraries/audio-test-harness/proto/audio_device.proto b/libraries/audio-test-harness/proto/audio_device.proto
new file mode 100644
index 0000000..6aedf32
--- /dev/null
+++ b/libraries/audio-test-harness/proto/audio_device.proto
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+
+syntax = "proto3";
+
+package media.audiotestharness.service;
+
+option java_package = "com.android.media.audiotestharness.proto";
+
+// Models an audio device within the Audio Test Harness service.
+message AudioDevice {
+  // Capabilities that an audio device can have.
+  enum Capability {
+    UNKNOWN = 0;
+
+    // Indicates that a given device can capture audio.
+    CAPTURE = 1;
+  }
+
+  // The name of the device.
+  string name = 1;
+
+  // The serial number of the device, if available.
+  string serial_number = 2;
+
+  // The capabilities of the device.
+  repeated Capability capabilities = 3;
+}
\ No newline at end of file
diff --git a/libraries/audio-test-harness/proto/audio_format.proto b/libraries/audio-test-harness/proto/audio_format.proto
new file mode 100644
index 0000000..de3f02d
--- /dev/null
+++ b/libraries/audio-test-harness/proto/audio_format.proto
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+
+syntax = "proto3";
+
+package media.audiotestharness.service;
+
+option java_package = "com.android.media.audiotestharness.proto";
+
+// Models the low-level audio-format of a given raw audio stream.
+message AudioFormat {
+  // The desired sample rate, by default 44,100.
+  float sampleRate = 1;
+
+  // The desired size of each sample, by default 16.
+  int32 sampleSizeBits = 2;
+
+  // The desired number of channels to capture, by default 1.
+  int32 channels = 3;
+
+  // Whether the samples should be recorded signed or unsigned, by default true
+  // meaning that the samples are signed.
+  bool signed = 4;
+
+  // Whether the samples should be big endian or little endian, by default this
+  // is false meaning that the audio is little endian.
+  bool bigEndian = 5;
+}
\ No newline at end of file
diff --git a/libraries/audio-test-harness/proto/audio_test_harness_service.proto b/libraries/audio-test-harness/proto/audio_test_harness_service.proto
new file mode 100644
index 0000000..7d68903
--- /dev/null
+++ b/libraries/audio-test-harness/proto/audio_test_harness_service.proto
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+
+syntax = "proto3";
+
+package media.audiotestharness;
+
+option java_package = "com.android.media.audiotestharness.proto";
+
+// The device-host Audio Test Harness service that allows devices-under-test to
+// access audio devices on the host as if they were local device. This allows
+// for capture and playback by the DUT using external, independent devices on
+// the test host.
+service AudioTestHarness {
+  // Initiates a capture from a host audio device and streams back the resulting
+  // raw audio samples to the device.
+  rpc Capture(CaptureRequest) returns (stream CaptureChunk) {}
+}
+
+// A request from the client to start a new capture.
+//
+// If the capture is started successfully, the host will return a series of
+// CaptureChunks containing raw sample data.
+message CaptureRequest {}
+
+// A chunk of capture data sent by the host to the client.
+message CaptureChunk {
+  // The raw audio data that has been captured.
+  bytes data = 1;
+}
\ No newline at end of file
diff --git a/libraries/audio-test-harness/server/Android.bp b/libraries/audio-test-harness/server/Android.bp
new file mode 100644
index 0000000..59f9985
--- /dev/null
+++ b/libraries/audio-test-harness/server/Android.bp
@@ -0,0 +1,198 @@
+/*
+ * 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.
+ */
+
+// Targets for the Audio Test Harness Server Components.
+// The Server allows for communication with host-side Audio Devices from
+// any connected gRPC clients either on another host or a device.
+// BINARIES ==============================================================
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_binary_host {
+    name: "audiotestharness-clicapturer",
+    srcs: [
+        "src/main/java/com/android/media/audiotestharness/server/examples/AudioTestHarnessCliCapturer.java",
+    ],
+    static_libs: [
+        "audiotestharness-javasoundlib",
+        "audiotestharness-commonlib-lite",
+        "audiotestharness-commonprotolib-lite",
+        "audiotestharness-corelib",
+        "guava",
+    ],
+    manifest: "cli-capturer-manifest.inf",
+}
+
+java_binary_host {
+    name: "audiotestharness-server",
+    static_libs: [
+        "audiotestharness-serverlib",
+    ],
+    manifest: "server-manifest.inf",
+}
+
+// LIBRARIES ==============================================================
+
+java_library_host {
+    name: "audiotestharness-serverlib",
+    srcs: [
+        "src/main/java/com/android/media/audiotestharness/server/AudioTestHarnessGrpcServer.java",
+        "src/main/java/com/android/media/audiotestharness/server/AudioTestHarnessServerModule.java",
+    ],
+    static_libs: [
+        "audiotestharness-servicelib",
+        "audiotestharness-utilitylib",
+        "audiotestharness-javasoundlib",
+        "audiotestharness-servicegrpclib-full",
+        "grpc-java-netty-shaded",
+    ],
+}
+
+java_library_host {
+    name: "audiotestharness-corelib",
+    srcs: [
+        "src/main/java/com/android/media/audiotestharness/server/core/*.java",
+    ],
+    static_libs: [
+        "audiotestharness-commonlib-full",
+        "audiotestharness-commonprotolib-full",
+        "guava",
+    ],
+}
+
+java_library_host {
+    name: "audiotestharness-servicelib",
+    srcs: [
+        "src/main/java/com/android/media/audiotestharness/server/service/*.java",
+    ],
+    static_libs: [
+        "audiotestharness-commonlib-full",
+        "audiotestharness-commonprotolib-full",
+        "audiotestharness-servicegrpclib-full",
+        "audiotestharness-corelib",
+        "guava",
+        "guice",
+    ],
+}
+
+java_library_host {
+    name: "audiotestharness-javasoundlib",
+    srcs: [
+        "src/main/java/com/android/media/audiotestharness/server/javasound/*.java",
+    ],
+    static_libs: [
+        "audiotestharness-corelib",
+        "audiotestharness-commonlib-full",
+        "audiotestharness-commonprotolib-full",
+        "guava",
+        "guice",
+    ],
+}
+
+java_library_host {
+    name: "audiotestharness-utilitylib",
+    srcs: [
+        "src/main/java/com/android/media/audiotestharness/server/utility/*.java",
+    ],
+}
+
+// TESTS ==============================================================
+
+java_test_host {
+    name: "audiotestharness-serverlib-tests",
+    test_suites: ["general-tests"],
+    srcs: [
+        "src/test/java/com/android/media/audiotestharness/server/AudioTestHarnessServerModuleTests.java",
+        "src/test/java/com/android/media/audiotestharness/server/AudioTestHarnessGrpcServerTests.java",
+        "src/test/java/com/android/media/audiotestharness/server/AudioTestHarnessTestImpl.java",
+    ],
+    static_libs: [
+        "audiotestharness-serverlib",
+        "junit",
+        "junit-params",
+
+        "mockito",
+        "objenesis",
+
+        "audiotestharness-servicegrpclib-full",
+        "grpc-java-netty-shaded",
+    ],
+    test_options: {
+        unit_test: true,
+    },
+}
+
+java_test_host {
+    name: "audiotestharness-servicelib-tests",
+    test_suites: ["general-tests"],
+    srcs: [
+        "src/test/java/com/android/media/audiotestharness/server/service/*.java",
+    ],
+    static_libs: [
+        "audiotestharness-servicelib",
+        "audiotestharness-corelib",
+        "audiotestharness-servicegrpclib-full",
+        "junit",
+        "junit-params",
+        "grpc-java-testing",
+        "guava",
+        "mockito",
+        "objenesis",
+    ],
+    test_options: {
+        unit_test: true,
+    },
+}
+
+java_test_host {
+    name: "audiotestharness-javasoundlib-tests",
+    test_suites: ["general-tests"],
+    srcs: [
+        "src/test/java/com/android/media/audiotestharness/server/javasound/*.java",
+    ],
+    static_libs: [
+        "audiotestharness-corelib",
+        "audiotestharness-javasoundlib",
+        "audiotestharness-commonlib-full",
+        "audiotestharness-commonprotolib-full",
+
+        "guava",
+        "junit-host",
+
+        "mockito",
+        "objenesis",
+    ],
+    test_options: {
+        unit_test: true,
+    },
+}
+
+java_test_host {
+    name: "audiotestharness-utilitylib-tests",
+    test_suites: ["general-tests"],
+    srcs: [
+        "src/test/java/com/android/media/audiotestharness/server/utility/*.java",
+    ],
+    static_libs: [
+        "audiotestharness-utilitylib",
+        "junit",
+    ],
+    test_options: {
+        unit_test: true,
+    },
+}
diff --git a/libraries/audio-test-harness/server/cli-capturer-manifest.inf b/libraries/audio-test-harness/server/cli-capturer-manifest.inf
new file mode 100644
index 0000000..7aa6b2f
--- /dev/null
+++ b/libraries/audio-test-harness/server/cli-capturer-manifest.inf
@@ -0,0 +1 @@
+Main-Class: com.android.media.audiotestharness.server.examples.AudioTestHarnessCliCapturer
\ No newline at end of file
diff --git a/libraries/audio-test-harness/server/server-manifest.inf b/libraries/audio-test-harness/server/server-manifest.inf
new file mode 100644
index 0000000..5520625
--- /dev/null
+++ b/libraries/audio-test-harness/server/server-manifest.inf
@@ -0,0 +1 @@
+Main-Class: com.android.media.audiotestharness.server.AudioTestHarnessGrpcServer
\ No newline at end of file
diff --git a/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/AudioTestHarnessGrpcServer.java b/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/AudioTestHarnessGrpcServer.java
new file mode 100644
index 0000000..6ac9ee4
--- /dev/null
+++ b/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/AudioTestHarnessGrpcServer.java
@@ -0,0 +1,190 @@
+/*
+ * 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.media.audiotestharness.server;
+
+import com.android.media.audiotestharness.proto.AudioTestHarnessGrpc;
+import com.android.media.audiotestharness.server.utility.PortUtility;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.inject.ConfigurationException;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+
+import io.grpc.Server;
+import io.grpc.ServerBuilder;
+
+import java.io.IOException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * gRPC Server to the Audio Test Harness Infrastructure.
+ *
+ * <p>This class is designed in such a way that the spin up and tear down of the gRPC server,
+ * including all dependencies are abstracted away from any users of Audio Test Harness. It is
+ * possible to use the service implementation directly and customize the deployment of the service,
+ * however, this is meant as an easy alternative for users who just want a working system and don't
+ * care how it works under the hood.
+ */
+public final class AudioTestHarnessGrpcServer implements AutoCloseable {
+
+    private static final int TESTING_PORT = 8080;
+
+    private static final Logger LOGGER =
+            Logger.getLogger(AudioTestHarnessGrpcServer.class.getName());
+
+    /** Number of threads used by the {@link #mExecutor} for task execution. */
+    private static final int THREAD_COUNT = 16;
+
+    /** The port on which the gRPC Server should be executed on. */
+    private final int mPort;
+
+    /**
+     * {@link Executor} used for background task execution. These tasks can be the handling of gRPC
+     * calls or other background tasks in the system such as the publishing of audio sample data by
+     * a {@link com.android.media.audiotestharness.server.core.AudioCapturer}.
+     *
+     * <p>This {@link ExecutorService} utilizes a fixed-thread pool with the number of threads set
+     * by the {@link #THREAD_COUNT} constant.
+     */
+    private final ExecutorService mExecutor;
+
+    /** {@link Injector} used for dependency management and injection. */
+    private final Injector mInjector;
+
+    /** Underlying {@link Server} that manages gRPC calls. */
+    private Server mServer;
+
+    private AudioTestHarnessGrpcServer(int port, ExecutorService executor, Injector injector) {
+        LOGGER.finest(String.format("new AudioTestHarnessGrpcServer on Port (%d)", port));
+
+        this.mPort = port;
+        this.mExecutor = executor;
+        this.mInjector = injector;
+    }
+
+    /**
+     * Creates a new {@link AudioTestHarnessGrpcServer} with the provided port, {@link
+     * ExecutorService}, and {@link Injector}.
+     *
+     * <p>This method is primarily available for unit testing. In production use, the {@link
+     * #createOnPort(int)} or {@link #createWithDefault()} methods should be used instead.
+     *
+     * @throws NullPointerException if either the executor or injector is null.
+     */
+    @VisibleForTesting
+    static AudioTestHarnessGrpcServer create(
+            int port, ExecutorService executor, Injector injector) {
+        return new AudioTestHarnessGrpcServer(
+                port,
+                Preconditions.checkNotNull(executor, "Executor cannot be null."),
+                Preconditions.checkNotNull(injector, "Injector cannot be null."));
+    }
+
+    /** Creates a new {@link AudioTestHarnessGrpcServer} with the provided port. */
+    public static AudioTestHarnessGrpcServer createOnPort(int port) {
+        LOGGER.finest(
+                String.format(
+                        "Using default Fixed Thread Pool Executor with %d threads and default"
+                                + " Injector using %s",
+                        THREAD_COUNT, AudioTestHarnessServerModule.class.getName()));
+
+        ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);
+        Injector injector = Guice.createInjector(AudioTestHarnessServerModule.create(executor));
+
+        return create(port, executor, injector);
+    }
+
+    /**
+     * Creates a new {@link AudioTestHarnessGrpcServer} on an available port in the dynamic port
+     * range.
+     */
+    public static AudioTestHarnessGrpcServer createWithDefault() {
+        return createOnPort(PortUtility.nextAvailablePort());
+    }
+
+    public static void main(String[] args) {
+        try (AudioTestHarnessGrpcServer server = createOnPort(TESTING_PORT)) {
+            server.open();
+
+            // Ensure that resources are cleanly stopped upon JVM shutdown.
+            Runtime.getRuntime().addShutdownHook(new Thread(server::close));
+
+            // Wait for server execution to complete. So that CTRL + C or some other mechanism must
+            // be used to kill the server.
+            server.mServer.awaitTermination();
+
+        } catch (Exception e) {
+            LOGGER.log(Level.SEVERE, "Unable to start the Audio Test Harness gRPC Server.", e);
+        }
+    }
+
+    /**
+     * Starts the Audio Test Harness gRPC Server listening on {@link #mPort}.
+     *
+     * @throws IOException if any errors occur while provisioning or starting the server.
+     */
+    public void open() throws IOException {
+        Preconditions.checkState(mServer == null, "Server has already been opened.");
+
+        ServerBuilder<?> serverBuilder = ServerBuilder.forPort(mPort).executor(mExecutor);
+
+        try {
+            serverBuilder.addService(
+                    mInjector.getInstance(AudioTestHarnessGrpc.AudioTestHarnessImplBase.class));
+        } catch (ConfigurationException ce) {
+            throw new IOException(
+                    "Unable to create new Audio Test Harness gRPC Server, failed to resolve"
+                            + " required dependency.",
+                    ce);
+        }
+
+        mServer = serverBuilder.build();
+        LOGGER.fine("Successfully created Audio Test Harness gRPC Server.");
+
+        mServer.start();
+        LOGGER.info(
+                String.format(
+                        "Started Audio Test Harness gRPC Server Listening on Port %d", mPort));
+    }
+
+    /**
+     * Stops the Audio Test Harness gRPC Server immediately and closes any underlying resources
+     * being used by the server.
+     */
+    @Override
+    public void close() {
+
+        LOGGER.info("Stopping Audio Test Harness gRPC Server");
+        if (mServer != null) {
+            mServer.shutdownNow();
+        } else {
+            LOGGER.warning(
+                    "mServer is null indicating that the Audio Test Harness gRPC Server was never"
+                            + " started.");
+        }
+
+        mExecutor.shutdownNow();
+    }
+
+    public int getPort() {
+        return mPort;
+    }
+}
diff --git a/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/AudioTestHarnessServerModule.java b/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/AudioTestHarnessServerModule.java
new file mode 100644
index 0000000..1122263
--- /dev/null
+++ b/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/AudioTestHarnessServerModule.java
@@ -0,0 +1,71 @@
+/*
+ * 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.media.audiotestharness.server;
+
+import com.android.media.audiotestharness.proto.AudioTestHarnessGrpc;
+import com.android.media.audiotestharness.server.javasound.JavaSoundModule;
+import com.android.media.audiotestharness.server.service.AudioCaptureSessionFactory;
+import com.android.media.audiotestharness.server.service.AudioCaptureSessionFactoryImpl;
+import com.android.media.audiotestharness.server.service.AudioTestHarnessImpl;
+import com.android.media.audiotestharness.server.service.StreamObserverOutputStreamFactory;
+
+import com.google.common.base.Preconditions;
+import com.google.inject.AbstractModule;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Guice {@link AbstractModule} that defines all of the core dependencies for the AudioTestHarness
+ * system.
+ *
+ * <p>While most dependencies are provided dynamically as needed, certain dependencies such as the
+ * {@link java.util.concurrent.Executor} used for scheduling system tasks are defined in a singleton
+ * format and are thus injected here upon creation.
+ */
+public final class AudioTestHarnessServerModule extends AbstractModule {
+
+    private final Executor mExecutor;
+
+    private AudioTestHarnessServerModule(Executor executor) {
+        this.mExecutor = executor;
+    }
+
+    /**
+     * Creates a new {@link AudioTestHarnessServerModule}.
+     *
+     * @param executor the {@link Executor} used for all background tasks executed throughout the
+     *     system. This is used to ensure that at server startup and teardown any background tasks
+     *     are properly stopped or cancelled.
+     */
+    public static AudioTestHarnessServerModule create(Executor executor) {
+        Preconditions.checkNotNull(executor, "Executor cannot be null.");
+        return new AudioTestHarnessServerModule(executor);
+    }
+
+    @Override
+    protected void configure() {
+        bind(Executor.class).toInstance(mExecutor);
+
+        // Tie the gRPC Server to the JavaSound implementation.
+        install(JavaSoundModule.create());
+
+        // Audio Test Harness gRPC Service Implementation
+        bind(AudioTestHarnessGrpc.AudioTestHarnessImplBase.class).to(AudioTestHarnessImpl.class);
+        bind(StreamObserverOutputStreamFactory.class);
+        bind(AudioCaptureSessionFactory.class).to(AudioCaptureSessionFactoryImpl.class);
+    }
+}
diff --git a/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/core/AudioCapturer.java b/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/core/AudioCapturer.java
new file mode 100644
index 0000000..aec2c36
--- /dev/null
+++ b/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/core/AudioCapturer.java
@@ -0,0 +1,62 @@
+/*
+ * 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.media.audiotestharness.server.core;
+
+import com.android.media.audiotestharness.proto.AudioDeviceOuterClass.AudioDevice;
+import com.android.media.audiotestharness.proto.AudioFormatOuterClass.AudioFormat;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * Provides methods for interacting with an {@link AudioDevice} to start capturing, stop capturing,
+ * close the capture device, and get metadata corresponding to the capture device or capture format.
+ */
+public interface AudioCapturer extends AutoCloseable {
+
+    /**
+     * Starts capturing from the underlying audio device and publishes the resulting raw audio
+     * samples to the attached outputs.
+     */
+    void open() throws IOException;
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>Stops capturing audio from the underlying audio device and frees the resources allocated
+     * for capture. After this method is called the Capturer is 'disposed' and cannot be reused,
+     * instead a new Capturer should be allocated by the {@link AudioSystemService}.
+     */
+    @Override
+    void close() throws IOException;
+
+    /** Attaches a specified {@link File} as an output for this capturer. */
+    void attachOutput(File file);
+
+    /** Attaches a specified {@link OutputStream} as an output for this capturer. */
+    void attachOutput(OutputStream outputStream);
+
+    /**
+     * Returns the {@link AudioFormat} corresponding to the raw audio samples produced by this
+     * capturer.
+     */
+    AudioFormat getAudioFormat();
+
+    /** Returns the {@link AudioDevice} that this capturer captures from. */
+    AudioDevice getAudioDevice();
+}
diff --git a/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/core/AudioSystemService.java b/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/core/AudioSystemService.java
new file mode 100644
index 0000000..7649219
--- /dev/null
+++ b/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/core/AudioSystemService.java
@@ -0,0 +1,60 @@
+/*
+ * 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.media.audiotestharness.server.core;
+
+import com.android.media.audiotestharness.common.Defaults;
+import com.android.media.audiotestharness.proto.AudioDeviceOuterClass.AudioDevice;
+import com.android.media.audiotestharness.proto.AudioFormatOuterClass.AudioFormat;
+
+import com.google.common.collect.ImmutableSet;
+
+import java.io.IOException;
+
+/** Provides an interface for the system to a native audio system devices. */
+public interface AudioSystemService {
+
+    /**
+     * Gets a {@link ImmutableSet} of {@link AudioDevice}s registered within the system.
+     *
+     * @throws IOException if unable to communicate with the underlying audio system.
+     */
+    ImmutableSet<AudioDevice> getDevices() throws IOException;
+
+    /**
+     * Creates a new {@link AudioCapturer} for a provided {@link AudioDevice} to capture raw audio
+     * data in the specified {@link AudioFormat}.
+     *
+     * <p>For the provided {@link AudioDevice} the closes possible matching {@link AudioDevice} is
+     * used. For example, if an {@link AudioDevice} with just the capability field set to [CAPTURE]
+     * is provided then the first device found that can capture audio will be used.
+     *
+     * @throws IOException if unable to communicate with the underlying audio system or any errors
+     *     occur while attempting to allocate resources for the {@link AudioCapturer}.
+     */
+    AudioCapturer createCapturerFor(AudioDevice device, AudioFormat audioFormat) throws IOException;
+
+    /**
+     * Creates a new {@link AudioCapturer} for the default {@link AudioDevice} and default {@link
+     * AudioFormat}.
+     *
+     * @throws IOException if unable to communicate with the underlying audio system or any errors
+     *     occur while attempting to allocate resources for the {@link AudioCapturer}.
+     */
+    default AudioCapturer createDefaultCapturer() throws IOException {
+        return createCapturerFor(Defaults.AUDIO_DEVICE, Defaults.AUDIO_FORMAT);
+    }
+}
diff --git a/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/examples/AudioTestHarnessCliCapturer.java b/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/examples/AudioTestHarnessCliCapturer.java
new file mode 100644
index 0000000..512f098
--- /dev/null
+++ b/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/examples/AudioTestHarnessCliCapturer.java
@@ -0,0 +1,69 @@
+/*
+ * 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.media.audiotestharness.server.examples;
+
+import com.android.media.audiotestharness.server.core.AudioCapturer;
+import com.android.media.audiotestharness.server.core.AudioSystemService;
+import com.android.media.audiotestharness.server.javasound.JavaAudioCapturerFactory;
+import com.android.media.audiotestharness.server.javasound.JavaAudioSystem;
+import com.android.media.audiotestharness.server.javasound.JavaAudioSystemService;
+
+import java.io.File;
+import java.time.Duration;
+import java.time.temporal.ChronoUnit;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.logging.Logger;
+
+/**
+ * Simple Audio Capture Binary that makes use of the Audio Test Harness libraries to capture raw
+ * audio samples to a file named {@link #FILENAME}.
+ */
+public class AudioTestHarnessCliCapturer {
+    private static final Logger LOGGER =
+            Logger.getLogger(AudioTestHarnessCliCapturer.class.getName());
+
+    /** Output file for the recording. */
+    private static final String FILENAME = "output.raw";
+
+    /** Duration that the recording will go until stopping. */
+    private static final Duration RECORD_TIME = Duration.of(10, ChronoUnit.SECONDS);
+
+    public static void main(String[] args) throws Exception {
+        ExecutorService executorService = Executors.newFixedThreadPool(4);
+        AudioSystemService audioSystemService =
+                new JavaAudioSystemService(
+                        JavaAudioSystem.getInstance(),
+                        new JavaAudioCapturerFactory(executorService));
+
+        try (AudioCapturer capturer = audioSystemService.createDefaultCapturer()) {
+            LOGGER.info("Opening AudioCapturer...");
+            capturer.attachOutput(new File(FILENAME));
+            capturer.open();
+
+            // Put main thread to sleep since the capturer operates on a separate thread provided by
+            // the
+            // above executor service.
+            LOGGER.info(String.format("Sleeping for %s...", RECORD_TIME));
+            Thread.sleep(RECORD_TIME.toMillis());
+
+            LOGGER.info("Completed Capture. Cleaning up...");
+        }
+
+        executorService.shutdown();
+    }
+}
diff --git a/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/javasound/JavaAudioCapturer.java b/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/javasound/JavaAudioCapturer.java
new file mode 100644
index 0000000..60967fa
--- /dev/null
+++ b/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/javasound/JavaAudioCapturer.java
@@ -0,0 +1,247 @@
+/*
+ * 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.media.audiotestharness.server.javasound;
+
+import com.android.media.audiotestharness.proto.AudioDeviceOuterClass.AudioDevice;
+import com.android.media.audiotestharness.proto.AudioFormatOuterClass.AudioFormat;
+import com.android.media.audiotestharness.server.core.AudioCapturer;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.sound.sampled.TargetDataLine;
+
+/**
+ * {@link AudioCapturer} that makes use of the {@link javax.sound.sampled} libraries to capture
+ * audio from an attached audio device.
+ */
+public final class JavaAudioCapturer implements AudioCapturer {
+
+    public static final Logger LOGGER = Logger.getLogger(JavaAudioCapturer.class.getName());
+
+    /** Size of the byte buffer used in reading audio samples from the audio */
+    public static final int BUFFER_SIZE = 2048;
+
+    /** Set containing all of the currently registered outputs. */
+    private final Set<OutputStream> mOutputs;
+
+    private final AudioDevice mAudioDevice;
+
+    private final AudioFormat mAudioFormat;
+
+    /** The underlying {@link TargetDataLine} from which raw audio data is read. */
+    private final TargetDataLine mTargetDataLine;
+
+    /**
+     * The {@link ExecutorService} that should be used for running the
+     * TargetDataLineWatchingPublisher background tasks.
+     */
+    private final Executor mExecutorService;
+
+    /**
+     * The {@link TargetDataLineWatchingPublisher} that publishes data read from the {@link
+     * TargetDataLine} to the various registered outputs.
+     */
+    private final TargetDataLineWatchingPublisher mPublisher;
+
+    /**
+     * Flag variable that determines whether the {@link #close()} method has been called and thus
+     * this Capturer's data line has already been closed and cannot be reused.
+     */
+    private boolean mDisposed = false;
+
+    private JavaAudioCapturer(
+            AudioDevice audioDevice,
+            AudioFormat audioFormat,
+            TargetDataLine targetDataLine,
+            Executor executor) {
+        LOGGER.finest("new JavaAudioCapturer()");
+        mAudioDevice = audioDevice;
+        mAudioFormat = audioFormat;
+        mTargetDataLine = targetDataLine;
+        mExecutorService = executor;
+
+        // Used for thread safety, in general, the set will be iterated over more than written to
+        // since the TargetDataLineWatchingPublisher will continually iterate over this set
+        // to write to current outputs.
+        //
+        // This allows for new outputs to be added after the publishers is already running.
+        mOutputs = new CopyOnWriteArraySet<>();
+        mPublisher = new TargetDataLineWatchingPublisher(mTargetDataLine, mOutputs);
+    }
+
+    public static JavaAudioCapturer create(
+            AudioDevice audioDevice,
+            AudioFormat audioFormat,
+            TargetDataLine targetDataLine,
+            Executor executor) {
+        Preconditions.checkArgument(
+                targetDataLine.isOpen(),
+                "Provided TargetDataLine should already be opened when passed to the"
+                        + " JavaAudioCapturer");
+        return new JavaAudioCapturer(audioDevice, audioFormat, targetDataLine, executor);
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>This method starts the underlying target data line which should trigger the data line to
+     * start filling its internal buffer and thus, publishing data to attached outputs.
+     *
+     * <p>This assumes that the data line has already been opened and thus all resources have been
+     * reserved for capture.
+     */
+    @Override
+    public void open() {
+        Preconditions.checkState(
+                !mDisposed,
+                "Cannot reopen a disposed AudioCapturer, a new one should be requested instead.");
+        Preconditions.checkState(
+                !mTargetDataLine.isRunning(), "The AudioCapturer is already open.");
+
+        mTargetDataLine.start();
+        mExecutorService.execute(mPublisher);
+
+        LOGGER.info("AudioCapturer Opened");
+    }
+
+    @Override
+    public void attachOutput(File file) {
+        try {
+            attachOutput(new FileOutputStream(file));
+        } catch (FileNotFoundException fnfe) {
+            LOGGER.warning(
+                    String.format("Failed to attach file %s as output to JavaAudioCapturer", file));
+        }
+    }
+
+    @Override
+    public void attachOutput(OutputStream outputStream) {
+        Preconditions.checkNotNull(outputStream, "Cannot attach a null output");
+        mOutputs.add(outputStream);
+        LOGGER.fine(String.format("Attatched new Output - %s", outputStream));
+    }
+
+    @Override
+    public AudioFormat getAudioFormat() {
+        return mAudioFormat;
+    }
+
+    @Override
+    public AudioDevice getAudioDevice() {
+        return mAudioDevice;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>Stops the publishing of data to attached outputs, stops the {@link TargetDataLine}, and
+     * then closes the line thus freeing the resources for use.
+     */
+    @Override
+    public void close() {
+        mPublisher.stop();
+
+        mTargetDataLine.stop();
+        mTargetDataLine.close();
+
+        mDisposed = true;
+        LOGGER.info("AudioCapturer Closed");
+    }
+
+    /**
+     * {@link Runnable} that watches the {@link TargetDataLine}, reading raw audio data from it, and
+     * then publishing it to any of the attached outputs.
+     */
+    @VisibleForTesting
+    static class TargetDataLineWatchingPublisher implements Runnable {
+        private static final Logger LOGGER =
+                Logger.getLogger(TargetDataLineWatchingPublisher.class.getName());
+
+        private final Set<OutputStream> mOutputs;
+        private final TargetDataLine mTargetDataLine;
+        private final byte[] mAudioBuffer = new byte[BUFFER_SIZE];
+        private volatile boolean mRunning;
+
+        private TargetDataLineWatchingPublisher(
+                TargetDataLine targetDataLine, Set<OutputStream> outputs) {
+            mTargetDataLine = targetDataLine;
+            mOutputs = outputs;
+            mRunning = true;
+        }
+
+        @Override
+        public void run() {
+            LOGGER.info("Publisher running");
+
+            int read;
+            do {
+
+                // Read exactly BUFFER_SIZE bytes from the TargetDataLine. This should always return
+                // 2048 bytes unless the targetDataLine is closed at which point it will return the
+                // remaining bytes left in the DataLine's internal buffer.
+                //
+                // This call will block until exactly 2048 bytes are read.
+                read =
+                        mTargetDataLine.read(
+                                mAudioBuffer, /* off= */ 0, /* len= */ mAudioBuffer.length);
+                LOGGER.finest(
+                        String.format("Successfully read %d bytes from mTargetDataLine", read));
+                for (OutputStream output : mOutputs) {
+                    try {
+
+                        // Verify that we are still running since there is a chance that the
+                        // state could have changed while waiting on the data to be read from the
+                        // TargetDataLine.
+                        if (mRunning) {
+                            output.write(mAudioBuffer, 0, read);
+                        }
+                    } catch (IOException ioe) {
+                        LOGGER.log(
+                                Level.WARNING,
+                                String.format(
+                                        "Failed to write raw audio data to output. Data may have"
+                                                + " been lost (Buffer Size: %d, Output: %s)",
+                                        BUFFER_SIZE, output),
+                                ioe);
+                    }
+                }
+            } while (mRunning);
+        }
+
+        public void stop() {
+            mRunning = false;
+            LOGGER.info("Publisher stopped");
+        }
+
+        public boolean isRunning() {
+            return mRunning;
+        }
+    }
+}
diff --git a/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/javasound/JavaAudioCapturerFactory.java b/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/javasound/JavaAudioCapturerFactory.java
new file mode 100644
index 0000000..66bf629
--- /dev/null
+++ b/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/javasound/JavaAudioCapturerFactory.java
@@ -0,0 +1,55 @@
+/*
+ * 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.media.audiotestharness.server.javasound;
+
+import com.android.media.audiotestharness.proto.AudioDeviceOuterClass.AudioDevice;
+import com.android.media.audiotestharness.proto.AudioFormatOuterClass.AudioFormat;
+
+import com.google.common.base.Preconditions;
+import com.google.inject.Inject;
+
+import java.util.concurrent.Executor;
+
+import javax.sound.sampled.TargetDataLine;
+
+/** Factory for {@link JavaAudioCapturer} instances. */
+public class JavaAudioCapturerFactory {
+
+    private final Executor mExecutor;
+
+    @Inject
+    public JavaAudioCapturerFactory(Executor executor) {
+        mExecutor = executor;
+    }
+
+    /**
+     * Builds a new {@link JavaAudioCapturer} for a provided {@link AudioDevice}, {@link
+     * AudioFormat}, and {@link TargetDataLine}.
+     */
+    public JavaAudioCapturer build(
+            AudioDevice audioDevice, AudioFormat audioFormat, TargetDataLine targetDataLine) {
+        Preconditions.checkNotNull(audioDevice, "audioDevice cannot be null.");
+        Preconditions.checkNotNull(audioFormat, "audioFormat cannot be null.");
+        Preconditions.checkNotNull(targetDataLine, "targetDataLine cannot be null");
+        Preconditions.checkArgument(
+                targetDataLine.isOpen(),
+                "targetDataLine must already be open before passed to the build() for the"
+                        + " AudioCapturer");
+
+        return JavaAudioCapturer.create(audioDevice, audioFormat, targetDataLine, mExecutor);
+    }
+}
diff --git a/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/javasound/JavaAudioSystem.java b/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/javasound/JavaAudioSystem.java
new file mode 100644
index 0000000..c60c940
--- /dev/null
+++ b/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/javasound/JavaAudioSystem.java
@@ -0,0 +1,74 @@
+/*
+ * 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.media.audiotestharness.server.javasound;
+
+import javax.sound.sampled.AudioFormat;
+import javax.sound.sampled.AudioSystem;
+import javax.sound.sampled.Line;
+import javax.sound.sampled.LineUnavailableException;
+import javax.sound.sampled.Mixer;
+import javax.sound.sampled.TargetDataLine;
+
+/**
+ * Wrapper for the {@link AudioSystem} class that provides the same API that can be easily injected
+ * into other classes that required it. Not all methods provided by the {@link AudioSystem} are
+ * included here, just the ones that are required by the system.
+ *
+ * <p>The methods in this class simply wrap all of the static methods in the Java {@link
+ * AudioSystem} as instance methods maintaining the same signature.
+ */
+public class JavaAudioSystem {
+
+    private static final JavaAudioSystem INSTANCE = new JavaAudioSystem();
+
+    private JavaAudioSystem() {}
+
+    public static JavaAudioSystem getInstance() {
+        return INSTANCE;
+    }
+
+    /** @see AudioSystem#getLine(Line.Info) */
+    public Line getLine(Line.Info info) throws LineUnavailableException {
+        return AudioSystem.getLine(info);
+    }
+
+    /** @see AudioSystem#getMixer(Mixer.Info) */
+    public Mixer getMixer(Mixer.Info info) {
+        return AudioSystem.getMixer(info);
+    }
+
+    /** @see AudioSystem#getMixerInfo() */
+    public Mixer.Info[] getMixerInfo() {
+        return AudioSystem.getMixerInfo();
+    }
+
+    /** @see AudioSystem#getTargetDataLine(AudioFormat) */
+    public TargetDataLine getTargetDataLine(AudioFormat format) throws LineUnavailableException {
+        return AudioSystem.getTargetDataLine(format);
+    }
+
+    /** @see AudioSystem#getTargetDataLine(AudioFormat, Mixer.Info) */
+    public TargetDataLine getTargetDataLine(AudioFormat format, Mixer.Info mixerinfo)
+            throws LineUnavailableException {
+        return AudioSystem.getTargetDataLine(format, mixerinfo);
+    }
+
+    /** @see AudioSystem#getTargetLineInfo(Line.Info) */
+    public Line.Info[] getTargetLineInfo(Line.Info info) {
+        return AudioSystem.getTargetLineInfo(info);
+    }
+}
diff --git a/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/javasound/JavaAudioSystemService.java b/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/javasound/JavaAudioSystemService.java
new file mode 100644
index 0000000..646a786
--- /dev/null
+++ b/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/javasound/JavaAudioSystemService.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 com.android.media.audiotestharness.server.javasound;
+
+import com.android.media.audiotestharness.proto.AudioDeviceOuterClass.AudioDevice;
+import com.android.media.audiotestharness.proto.AudioFormatOuterClass.AudioFormat;
+import com.android.media.audiotestharness.server.core.AudioCapturer;
+import com.android.media.audiotestharness.server.core.AudioSystemService;
+
+import com.google.common.collect.ImmutableSet;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.logging.Logger;
+
+import javax.inject.Inject;
+import javax.sound.sampled.DataLine;
+import javax.sound.sampled.LineUnavailableException;
+import javax.sound.sampled.Mixer;
+import javax.sound.sampled.TargetDataLine;
+
+public class JavaAudioSystemService implements AudioSystemService {
+
+    private static final Logger LOGGER = Logger.getLogger(JavaAudioSystemService.class.getName());
+
+    private final JavaAudioSystem mAudioSystem;
+
+    private final JavaAudioCapturerFactory mAudioCapturerFactory;
+
+    /**
+     * Cache of {@link com.android.media.audiotestharness.proto.AudioDeviceOuterClass.AudioDevice}
+     * that were provided by the {@link #getDevices()} method to the {@link Mixer.Info} they
+     * correspond to.
+     *
+     * <p>This cache is reset upon ever call to the {@link #getDevices()} method, and is used
+     * primarily to simplify the {@link #createCapturerFor(AudioDevice, AudioFormat)} method
+     * significantly.
+     */
+    private final Map<AudioDevice, Mixer.Info> mDeviceMixerMap;
+
+    @Inject
+    public JavaAudioSystemService(
+            JavaAudioSystem javaAudioSystem, JavaAudioCapturerFactory javaAudioCapturerFactory) {
+        LOGGER.finest("new JavaAudioSystemService");
+
+        mAudioSystem = javaAudioSystem;
+        mAudioCapturerFactory = javaAudioCapturerFactory;
+
+        mDeviceMixerMap = new HashMap<>();
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>Makes use of the Java Sound API to get currently configured devices. Devices are marked as
+     * having the {@link AudioDevice.Capability#CAPTURE} capability if they have a {@link
+     * javax.sound.sampled.TargetDataLine} available, meaning that audio data can be read from them.
+     * Currently only sets the {@link AudioDevice}'s name to the name provided by the {@link
+     * Mixer.Info} corresponding to the device.
+     */
+    @Override
+    public ImmutableSet<AudioDevice> getDevices() {
+        rebuildMixerCache();
+        return ImmutableSet.copyOf(mDeviceMixerMap.keySet());
+    }
+
+    @Override
+    public AudioCapturer createCapturerFor(AudioDevice device, AudioFormat audioFormat)
+            throws IOException {
+        LOGGER.info(
+                String.format(
+                        "Creating new Capturer for Device (%s) using Format (%s)",
+                        device, audioFormat));
+
+        // Build the cache so direct lookup is easy if the device is exact.
+        if (mDeviceMixerMap.isEmpty()) {
+            rebuildMixerCache();
+        }
+
+        Mixer.Info mixerInfo;
+        mixerInfo = mDeviceMixerMap.get(device);
+
+        // If there is no exact match for the provided device, attempt to find the nearest match.
+        // By "nearest match" we mean a mixer that matches all of the fields in the provided mixer.
+        // In this case, we only look for partial matches for the "name" field.
+        if (mixerInfo == null) {
+            mixerInfo = findClosestMatchingMixer(device, /* exactName= */ false);
+        }
+
+        if (mixerInfo == null) {
+            throw new IOException("Unable to find mixer matching provided device");
+        }
+
+        Mixer mixer = mAudioSystem.getMixer(mixerInfo);
+
+        TargetDataLine targetDataLine;
+        try {
+            targetDataLine =
+                    (TargetDataLine)
+                            mixer.getLine(
+                                    new DataLine.Info(
+                                            TargetDataLine.class,
+                                            JavaSoundUtility.audioFormatFrom(audioFormat)));
+        } catch (LineUnavailableException | IllegalArgumentException e) {
+            throw new IOException(
+                    "Unable to build player for specified device and audio format", e);
+        }
+
+        try {
+            targetDataLine.open(JavaSoundUtility.audioFormatFrom(audioFormat));
+        } catch (LineUnavailableException lue) {
+            throw new IOException(
+                    "Failed to reserve audio system resources for specified device and audio"
+                            + " format",
+                    lue);
+        }
+
+        return mAudioCapturerFactory.build(device, audioFormat, targetDataLine);
+    }
+
+    /**
+     * Rebuilds the {@link #mDeviceMixerMap} so that {@link AudioDevice} objects are properly mapped
+     * to their corresponding {@link Mixer.Info}
+     */
+    private void rebuildMixerCache() {
+        LOGGER.finest("Rebuildling Mixer Cache...");
+        mDeviceMixerMap.clear();
+        for (Mixer.Info mixerInfo : mAudioSystem.getMixerInfo()) {
+            AudioDevice.Builder audioDeviceBuilder = AudioDevice.newBuilder();
+            Mixer mixer = mAudioSystem.getMixer(mixerInfo);
+
+            audioDeviceBuilder.setName(mixer.getMixerInfo().getName());
+
+            if (mixer.getTargetLineInfo().length > 0) {
+                audioDeviceBuilder.addCapabilities(AudioDevice.Capability.CAPTURE);
+            }
+
+            AudioDevice device = audioDeviceBuilder.build();
+
+            mDeviceMixerMap.put(device, mixerInfo);
+        }
+        LOGGER.finest(
+                String.format(
+                        "Successfully Rebuilt Mixer Cache, found %d Mixers",
+                        mDeviceMixerMap.size()));
+    }
+
+    /**
+     * Finds the closest matching {@link Mixer.Info} corresponding to a given {@link AudioDevice} or
+     * null if none can be found that matches.
+     *
+     * @param audioDevice the device to search for.
+     * @param exactName if true, then an exact match is required for the name of the audio device,
+     *     if false, then a partial match to a mixer name is sufficient.
+     */
+    private Mixer.Info findClosestMatchingMixer(AudioDevice audioDevice, boolean exactName) {
+        LOGGER.finest(
+                String.format("Searching Mixer Results for closest match to: %s", audioDevice));
+        Optional<Mixer.Info> firstMatch =
+                Arrays.stream(mAudioSystem.getMixerInfo())
+
+                        // Filter by AudioDevice name either exactly or partially.
+                        .filter(
+                                (info) -> {
+                                    if (audioDevice.getName() != null
+                                            && !audioDevice.getName().isEmpty()) {
+                                        if (exactName) {
+                                            return info.getName().equals(audioDevice.getName());
+                                        } else {
+                                            return info.getName().contains(audioDevice.getName());
+                                        }
+                                    }
+
+                                    return true;
+                                })
+
+                        // Filter by if the device should support capture or not.
+                        .filter(
+                                (info) -> {
+                                    if (audioDevice
+                                            .getCapabilitiesList()
+                                            .contains(AudioDevice.Capability.CAPTURE)) {
+                                        return mAudioSystem
+                                                        .getMixer(info)
+                                                        .getTargetLineInfo()
+                                                        .length
+                                                > 0;
+                                    }
+
+                                    return true;
+                                })
+                        .findFirst();
+
+        LOGGER.finest(
+                firstMatch.isPresent()
+                        ? String.format("Found match: %s", firstMatch.get())
+                        : "Did not find match");
+        return firstMatch.orElse(null);
+    }
+}
diff --git a/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/javasound/JavaSoundModule.java b/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/javasound/JavaSoundModule.java
new file mode 100644
index 0000000..d3dbaa6
--- /dev/null
+++ b/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/javasound/JavaSoundModule.java
@@ -0,0 +1,38 @@
+/*
+ * 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.media.audiotestharness.server.javasound;
+
+import com.android.media.audiotestharness.server.core.AudioSystemService;
+
+import com.google.inject.AbstractModule;
+
+/** {@link AbstractModule} for the Java Sound implementation of the Audio Test Harness. */
+public final class JavaSoundModule extends AbstractModule {
+
+    private JavaSoundModule() {}
+
+    public static JavaSoundModule create() {
+        return new JavaSoundModule();
+    }
+
+    @Override
+    protected void configure() {
+        bind(AudioSystemService.class).to(JavaAudioSystemService.class);
+        bind(JavaAudioSystem.class).toInstance(JavaAudioSystem.getInstance());
+        bind(JavaAudioCapturerFactory.class);
+    }
+}
diff --git a/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/javasound/JavaSoundUtility.java b/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/javasound/JavaSoundUtility.java
new file mode 100644
index 0000000..ae0a56e
--- /dev/null
+++ b/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/javasound/JavaSoundUtility.java
@@ -0,0 +1,40 @@
+/*
+ * 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.media.audiotestharness.server.javasound;
+
+import com.android.media.audiotestharness.proto.AudioFormatOuterClass;
+
+import javax.sound.sampled.AudioFormat;
+
+/** Utility methods used throughout the Java Sound Library. */
+public final class JavaSoundUtility {
+
+    private JavaSoundUtility() {}
+
+    /**
+     * Converts a {@link com.android.media.audiotestharness.proto.AudioFormatOuterClass.AudioFormat}
+     * proto into the Java Sound API {@link AudioFormat}.
+     */
+    public static AudioFormat audioFormatFrom(AudioFormatOuterClass.AudioFormat sourceFormat) {
+        return new AudioFormat(
+                sourceFormat.getSampleRate(),
+                sourceFormat.getSampleSizeBits(),
+                sourceFormat.getChannels(),
+                sourceFormat.getSigned(),
+                sourceFormat.getBigEndian());
+    }
+}
diff --git a/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/service/AudioCaptureSession.java b/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/service/AudioCaptureSession.java
new file mode 100644
index 0000000..a6a65c7
--- /dev/null
+++ b/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/service/AudioCaptureSession.java
@@ -0,0 +1,145 @@
+/*
+ * 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.media.audiotestharness.server.service;
+
+import com.android.media.audiotestharness.server.core.AudioCapturer;
+
+import com.google.common.base.Preconditions;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Manages the lifecycle of an {@link AudioCapturer} for the {@link AudioTestHarnessImpl}'s handling
+ * of the Capture gRPC procedure.
+ */
+public class AudioCaptureSession {
+    private static final Logger LOGGER = Logger.getLogger(AudioCaptureSession.class.getName());
+
+    /** The identifier for this session which should be used in logging and errors. */
+    private final int mSessionId;
+
+    /** The underlying {@link AudioCapturer} for this session. */
+    private final AudioCapturer mAudioCapturer;
+
+    /**
+     * The {@link CaptureChunkStreamObserverOutputStream} that is outputting to the gRPC {@link
+     * io.grpc.stub.StreamObserver} from the gRPC call.
+     */
+    private final CaptureChunkStreamObserverOutputStream mCaptureChunkStreamObserverOutputStream;
+
+    private AudioCaptureSession(
+            int sessionId,
+            AudioCapturer audioCapturer,
+            CaptureChunkStreamObserverOutputStream captureChunkStreamObserverOutputStream) {
+        LOGGER.finest(
+                String.format(
+                        "new AudioCaptureSession(id=%d, audioCapturer=%s,"
+                                + " captureChunkStreamObserverOutputStream=%s)",
+                        sessionId, audioCapturer, captureChunkStreamObserverOutputStream));
+        mSessionId = sessionId;
+        mAudioCapturer = audioCapturer;
+        mCaptureChunkStreamObserverOutputStream = captureChunkStreamObserverOutputStream;
+    }
+
+    /** Creates a new {@link AudioCaptureSession}. */
+    public static AudioCaptureSession create(
+            int id,
+            AudioCapturer audioCapturer,
+            CaptureChunkStreamObserverOutputStream captureChunkStreamObserverOutputStream) {
+        Preconditions.checkNotNull(audioCapturer);
+        Preconditions.checkNotNull(captureChunkStreamObserverOutputStream);
+
+        LOGGER.info(
+                String.format(
+                        "id=%d - Attaching CaptureChunkStreamObserverOutputStream to AudioCapturer",
+                        id));
+        audioCapturer.attachOutput(captureChunkStreamObserverOutputStream);
+
+        return new AudioCaptureSession(id, audioCapturer, captureChunkStreamObserverOutputStream);
+    }
+
+    /**
+     * Starts the capture of audio.
+     *
+     * @throws IOException if any errors occur while opening the underlying {@link AudioCapturer}
+     */
+    public void start() throws IOException {
+        try {
+            mAudioCapturer.open();
+        } catch (IOException ioe) {
+            throw new IOException(
+                    String.format(
+                            "Failed to open AudioCapturer for Audio Capture Session %d",
+                            mSessionId),
+                    ioe);
+        }
+        LOGGER.info(String.format("id=%d - Capture Session Started", mSessionId));
+    }
+
+    /** Stops the capture of audio and cleans up any associated resources. */
+    public void stop() {
+        try {
+            mAudioCapturer.close();
+        } catch (IOException ioe) {
+            LOGGER.log(
+                    Level.WARNING,
+                    String.format(
+                            "Failed to close AudioCapturer, there may be a resource leak for Audio"
+                                    + " Capture Session %d",
+                            mSessionId));
+        }
+        mCaptureChunkStreamObserverOutputStream.close();
+        LOGGER.info(String.format("id=%d - Capture Session Stopped", mSessionId));
+    }
+
+    /**
+     * Puts the current thread to sleep until the session is stopped or the provided timeout is
+     * reached at which point the method returns.
+     */
+    public void awaitStop(long timeout, TimeUnit timeUnit) throws IOException {
+        LOGGER.info(
+                String.format(
+                        "id=%d - Waiting for Stop up to %d %s", mSessionId, timeout, timeUnit));
+
+        boolean didFinish;
+        try {
+            didFinish = mCaptureChunkStreamObserverOutputStream.awaitClose(timeout, timeUnit);
+        } catch (InterruptedException ie) {
+            throw new IOException(
+                    String.format(
+                            "Interrupted while waiting for capture to finish for Audio Capture"
+                                    + " Session %d",
+                            mSessionId),
+                    ie);
+        }
+
+        if (!didFinish) {
+            LOGGER.warning(
+                    String.format(
+                            "id=%d - Capture session reached maximum timeout duration of %d %s",
+                            mSessionId, timeout, timeUnit));
+            stop();
+        }
+    }
+
+    public int getSessionId() {
+        return mSessionId;
+    }
+}
diff --git a/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/service/AudioCaptureSessionFactory.java b/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/service/AudioCaptureSessionFactory.java
new file mode 100644
index 0000000..6a1076e
--- /dev/null
+++ b/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/service/AudioCaptureSessionFactory.java
@@ -0,0 +1,34 @@
+/*
+ * 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.media.audiotestharness.server.service;
+
+import com.android.media.audiotestharness.proto.AudioTestHarnessService;
+import com.android.media.audiotestharness.server.core.AudioCapturer;
+
+import io.grpc.stub.ServerCallStreamObserver;
+
+/** Factory for {@link AudioCaptureSession}s. */
+public interface AudioCaptureSessionFactory {
+
+    /**
+     * Creates a new {@link AudioCaptureSession} for the provided {@link AudioCapturer} with the
+     * {@link ServerCallStreamObserver} added as an input to the {@link AudioCapturer}.
+     */
+    AudioCaptureSession createCaptureSession(
+            ServerCallStreamObserver<AudioTestHarnessService.CaptureChunk> chunkStreamObserver,
+            AudioCapturer audioCapturer);
+}
diff --git a/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/service/AudioCaptureSessionFactoryImpl.java b/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/service/AudioCaptureSessionFactoryImpl.java
new file mode 100644
index 0000000..71fb829
--- /dev/null
+++ b/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/service/AudioCaptureSessionFactoryImpl.java
@@ -0,0 +1,66 @@
+/*
+ * 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.media.audiotestharness.server.service;
+
+import com.android.media.audiotestharness.proto.AudioTestHarnessService;
+import com.android.media.audiotestharness.server.core.AudioCapturer;
+
+import com.google.inject.Inject;
+
+import io.grpc.stub.ServerCallStreamObserver;
+
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.logging.Logger;
+
+/** Base factory for {@link AudioCaptureSession}s. */
+public class AudioCaptureSessionFactoryImpl implements AudioCaptureSessionFactory {
+
+    /**
+     * Incrementing value to create identifiers for capture streams for logging and debugging
+     * purposes. This value doesn't need to be unique across instances of the server, just unique
+     * per run, thus an incrementing integer is used.
+     *
+     * <p>Since different calls to the Capture procedure could come across threads, and {@link
+     * AtomicInteger} is used.
+     */
+    private static AtomicInteger sIdBase = new AtomicInteger(/* initialValue= */ 1);
+
+    private static final Logger LOGGER =
+            Logger.getLogger(AudioCaptureSessionFactoryImpl.class.getName());
+
+    private final StreamObserverOutputStreamFactory mStreamObserverOutputStreamFactory;
+
+    @Inject
+    public AudioCaptureSessionFactoryImpl(
+            StreamObserverOutputStreamFactory streamObserverOutputStreamFactory) {
+        mStreamObserverOutputStreamFactory = streamObserverOutputStreamFactory;
+    }
+
+    @Override
+    public AudioCaptureSession createCaptureSession(
+            ServerCallStreamObserver<AudioTestHarnessService.CaptureChunk> chunkStreamObserver,
+            AudioCapturer audioCapturer) {
+        int id = sIdBase.getAndIncrement();
+        LOGGER.info(String.format("Starting new capture session with id %d", id));
+
+        return AudioCaptureSession.create(
+                id,
+                audioCapturer,
+                mStreamObserverOutputStreamFactory.createNewCaptureChunkStreamObserverOutputStream(
+                        chunkStreamObserver));
+    }
+}
diff --git a/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/service/AudioTestHarnessImpl.java b/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/service/AudioTestHarnessImpl.java
new file mode 100644
index 0000000..78b6d10
--- /dev/null
+++ b/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/service/AudioTestHarnessImpl.java
@@ -0,0 +1,99 @@
+/*
+ * 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.media.audiotestharness.server.service;
+
+import com.android.media.audiotestharness.proto.AudioTestHarnessGrpc;
+import com.android.media.audiotestharness.proto.AudioTestHarnessService;
+import com.android.media.audiotestharness.server.core.AudioCapturer;
+import com.android.media.audiotestharness.server.core.AudioSystemService;
+
+import com.google.inject.Inject;
+
+import io.grpc.Status;
+import io.grpc.stub.ServerCallStreamObserver;
+import io.grpc.stub.StreamObserver;
+
+import java.io.IOException;
+import java.time.Duration;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * {@inheritDoc}
+ *
+ * <p>Core service implementation for the Audio Test Harness that utilizes that Java Sound API to
+ * expose audio devices connected to a host to client devices for capture and playback.
+ */
+public final class AudioTestHarnessImpl extends AudioTestHarnessGrpc.AudioTestHarnessImplBase {
+
+    private static final Logger LOGGER = Logger.getLogger(AudioTestHarnessImpl.class.getName());
+
+    /** The maximum duration that a client can capture before the server manually stops capture. */
+    public static final Duration MAX_CAPTURE_DURATION = Duration.ofHours(1);
+
+    /** {@link AudioSystemService} that should be used to access audio system resources. */
+    private final AudioSystemService mAudioSystemService;
+
+    /** Factory for StreamObserverOutputStreams used during the procedure handling process. */
+    private final AudioCaptureSessionFactory mAudioCaptureSessionFactory;
+
+    @Inject
+    public AudioTestHarnessImpl(
+            AudioSystemService audioSystemService,
+            AudioCaptureSessionFactory audioCaptureSessionFactory) {
+        mAudioSystemService = audioSystemService;
+        mAudioCaptureSessionFactory = audioCaptureSessionFactory;
+    }
+
+    @Override
+    public void capture(
+            AudioTestHarnessService.CaptureRequest request,
+            StreamObserver<AudioTestHarnessService.CaptureChunk> responseObserver) {
+        ServerCallStreamObserver<AudioTestHarnessService.CaptureChunk> serverCallResponseObserver =
+                (ServerCallStreamObserver<AudioTestHarnessService.CaptureChunk>) responseObserver;
+        LOGGER.info("Handling Capture procedure");
+
+        // Allocate the default AudioCapturer from the Audio System Service.
+        AudioCapturer capturer;
+        try {
+            capturer = mAudioSystemService.createDefaultCapturer();
+        } catch (IOException ioe) {
+            LOGGER.log(Level.SEVERE, "Failed to allocate default AudioCapturer", ioe);
+            serverCallResponseObserver.onError(
+                    Status.UNAVAILABLE
+                            .withCause(ioe)
+                            .withDescription("Failed to allocate default AudioCapturer")
+                            .asException());
+            return;
+        }
+
+        // Start a new capture session
+        AudioCaptureSession captureSession =
+                mAudioCaptureSessionFactory.createCaptureSession(
+                        serverCallResponseObserver, capturer);
+
+        // Start capturing and continue until either cancelled by the client or MAX_CAPTURE_DURATION
+        // is hit.
+        serverCallResponseObserver.setOnCancelHandler(captureSession::stop);
+        try {
+            captureSession.start();
+        } catch (IOException ioe) {
+            LOGGER.log(Level.SEVERE, "Internal Error while Capturing", ioe);
+            serverCallResponseObserver.onError(
+                    Status.INTERNAL.withCause(ioe).withDescription(ioe.getMessage()).asException());
+        }
+    }
+}
diff --git a/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/service/CaptureChunkStreamObserverOutputStream.java b/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/service/CaptureChunkStreamObserverOutputStream.java
new file mode 100644
index 0000000..27844e2
--- /dev/null
+++ b/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/service/CaptureChunkStreamObserverOutputStream.java
@@ -0,0 +1,181 @@
+/*
+ * 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.media.audiotestharness.server.service;
+
+import com.android.media.audiotestharness.proto.AudioTestHarnessService;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.protobuf.ByteString;
+
+import io.grpc.stub.ServerCallStreamObserver;
+import io.grpc.stub.StreamObserver;
+
+import java.io.OutputStream;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.logging.Logger;
+
+/**
+ * {@link OutputStream} that streams data written to it to a provided {@link StreamObserver} in the
+ * form of {@link AudioTestHarnessService.CaptureChunk}s.
+ *
+ * <p>This class is thread compatible but not thread safe. That is, with proper synchronization,
+ * this class could be used by multiple threads, however there is no built-in synchronization.
+ * However, the {@link #awaitClose()} methods are provided so that other threads can wait on the
+ * this {@link OutputStream} to be closed before continuing.
+ *
+ * <p>This class should not be extended, however is left non-final for mocking purposes.
+ */
+public class CaptureChunkStreamObserverOutputStream extends OutputStream {
+    private static final Logger LOGGER =
+            Logger.getLogger(CaptureChunkStreamObserverOutputStream.class.getName());
+
+    /**
+     * Used for synchronizing actions during gRPC execution. Thus, a main thread can delegate
+     * streaming actions to this {@link OutputStream} and then when done can take back control and
+     * finish execution of the procedure.
+     */
+    private final CountDownLatch mCountDownLatch;
+
+    /**
+     * {@link StreamObserver} that underlies this {@link OutputStream} and is written to whenever
+     * any of this class's write methods are called.
+     */
+    private final ServerCallStreamObserver<AudioTestHarnessService.CaptureChunk>
+            mCaptureChunkStreamObserver;
+
+    /**
+     * Flag to track whether or not this {@link OutputStream} has been closed. If so, then does not
+     * allow write actions to occur to prevent a stray call to onNext after onCompleted has been
+     * called on the underlying {@link StreamObserver}.
+     *
+     * <p>Atomic since this value could be written to and read from separate threads thus ensuring
+     * that no race condition occurs where the thread writing to the stream thinks that it is open
+     * while the stream is in fact closed.
+     */
+    private AtomicBoolean mClosed = new AtomicBoolean(false);
+
+    private CaptureChunkStreamObserverOutputStream(
+            ServerCallStreamObserver<AudioTestHarnessService.CaptureChunk>
+                    captureChunkStreamObserver,
+            CountDownLatch countDownLatch) {
+        mCaptureChunkStreamObserver = captureChunkStreamObserver;
+        mCountDownLatch = countDownLatch;
+        LOGGER.finest("new CaptureChunkStreamObserverOutputStream");
+    }
+
+    public static CaptureChunkStreamObserverOutputStream create(
+            ServerCallStreamObserver<AudioTestHarnessService.CaptureChunk>
+                    captureChunkStreamObserver) {
+        return create(captureChunkStreamObserver, new CountDownLatch(1));
+    }
+
+    @VisibleForTesting
+    static CaptureChunkStreamObserverOutputStream create(
+            ServerCallStreamObserver<AudioTestHarnessService.CaptureChunk>
+                    captureChunkStreamObserver,
+            CountDownLatch countDownLatch) {
+        return new CaptureChunkStreamObserverOutputStream(
+                Preconditions.checkNotNull(captureChunkStreamObserver),
+                Preconditions.checkNotNull(countDownLatch));
+    }
+
+    @Override
+    public void write(int b) {
+        Preconditions.checkState(
+                !mClosed.get(),
+                "CaptureChunkStreamObserverOutputStream has already been closed and cannot be"
+                        + " written to.");
+
+        byte[] toWrite = new byte[1];
+
+        // Grab only the lowest byte per the docs for the write(int) method.
+        toWrite[0] = (byte) (b & 0xFF);
+
+        write(toWrite);
+    }
+
+    @Override
+    public void write(byte[] b) {
+        Preconditions.checkState(
+                !mClosed.get(),
+                "CaptureChunkStreamObserverOutputStream has already been closed and cannot be"
+                        + " written to.");
+        write(b, 0, b.length);
+    }
+
+    @Override
+    public void write(byte[] b, int off, int len) {
+        Preconditions.checkState(
+                !mClosed.get(),
+                "CaptureChunkStreamObserverOutputStream has already been closed and cannot be"
+                        + " written to.");
+
+        ByteString chunkBytes = ByteString.copyFrom(b, off, len);
+
+        AudioTestHarnessService.CaptureChunk captureChunk =
+                AudioTestHarnessService.CaptureChunk.newBuilder().setData(chunkBytes).build();
+
+        // Skip sending any chunks that are written to the stream after cancellation.
+        //
+        // Since the writing to this Output Stream comes from a separate thread from the original
+        // gRPC handling thread, there is a chance that an extra chunk of data will be written
+        // before the cancellation can propagate to the AudioCapturer that is publishing data. In
+        // these cases, simply ignore the extra chunk of data and log that it was seen.
+        if (mCaptureChunkStreamObserver.isCancelled()) {
+            LOGGER.fine("Extra chunk sent after cancellation will be discarded");
+        } else {
+            mCaptureChunkStreamObserver.onNext(captureChunk);
+        }
+    }
+
+    @Override
+    public void close() {
+        mClosed.set(true);
+        mCountDownLatch.countDown();
+        LOGGER.info("Stream Closed");
+    }
+
+    public boolean isClosed() {
+        return mClosed.get();
+    }
+
+    /**
+     * Causes the current thread to wait until the stream is closed.
+     *
+     * @throws InterruptedException if the waiting thread is interrupted before this stream is
+     *     closed.
+     */
+    public void awaitClose() throws InterruptedException {
+        mCountDownLatch.await();
+    }
+
+    /**
+     * Causes the current thread to wait until the stream is closed or the provided timeout has
+     * elapsed. Returns true if the stream is closed before the end of the timeout, false otherwise.
+     *
+     * @param timeout the maximum wait time
+     * @param timeUnit the {@link TimeUnit} of the timeout arg
+     * @throws InterruptedException if the waiting thread is interrupted before this stream is
+     *     closed.
+     */
+    public boolean awaitClose(long timeout, TimeUnit timeUnit) throws InterruptedException {
+        return mCountDownLatch.await(timeout, timeUnit);
+    }
+}
diff --git a/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/service/StreamObserverOutputStreamFactory.java b/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/service/StreamObserverOutputStreamFactory.java
new file mode 100644
index 0000000..ad6c824
--- /dev/null
+++ b/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/service/StreamObserverOutputStreamFactory.java
@@ -0,0 +1,37 @@
+/*
+ * 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.media.audiotestharness.server.service;
+
+import com.android.media.audiotestharness.proto.AudioTestHarnessService;
+
+import io.grpc.stub.ServerCallStreamObserver;
+
+/**
+ * Factory class for {@link java.io.OutputStream}s that package written data into specialized the
+ * proper format before writing the packaged data to a {@link io.grpc.stub.StreamObserver}
+ */
+public class StreamObserverOutputStreamFactory {
+
+    /**
+     * Creates a new {@link CaptureChunkStreamObserverOutputStream} for the provided {@link
+     * ServerCallStreamObserver}.
+     */
+    public CaptureChunkStreamObserverOutputStream createNewCaptureChunkStreamObserverOutputStream(
+            ServerCallStreamObserver<AudioTestHarnessService.CaptureChunk> streamObserver) {
+        return CaptureChunkStreamObserverOutputStream.create(streamObserver);
+    }
+}
diff --git a/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/utility/PortUtility.java b/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/utility/PortUtility.java
new file mode 100644
index 0000000..9f1df74
--- /dev/null
+++ b/libraries/audio-test-harness/server/src/main/java/com/android/media/audiotestharness/server/utility/PortUtility.java
@@ -0,0 +1,81 @@
+/*
+ * 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.media.audiotestharness.server.utility;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.logging.Logger;
+
+/** Utility Class containing utilities for IP ports. */
+public final class PortUtility {
+    private static final Logger LOGGER = Logger.getLogger(PortUtility.class.getName());
+
+    /** The lower bound of ports to use, currently the start of the dynamic port range */
+    public static final int START_PORT = 49152;
+
+    /** The upper bound of ports to use, currently the end of the dynamic port range */
+    public static final int END_PORT = 65535;
+
+    /**
+     * Maximum number of attempts that the utility should go through before failing to find an open
+     * port.
+     */
+    public static final int MAX_ATTEMPTS = 1000;
+
+    private PortUtility() {}
+
+    /**
+     * Searches for and returns the next open port number on the host system.
+     *
+     * @throws RuntimeException if unable to find an open port with {@link #MAX_ATTEMPTS} tries.
+     */
+    public static int nextAvailablePort() {
+        int port;
+        int count = 0;
+
+        while (count < MAX_ATTEMPTS) {
+            count++;
+            LOGGER.finest(
+                    String.format("Attempting to find open port. Number of attempts: %d", count));
+            port = ThreadLocalRandom.current().nextInt(START_PORT, END_PORT);
+            if (isPortFree(port)) {
+                LOGGER.finest(String.format("Found open port (%d) after %d tries", port, count));
+                return port;
+            }
+        }
+
+        throw new RuntimeException("Unable to find an open port.");
+    }
+
+    /**
+     * Helper method that identifies if a provided port is free.
+     *
+     * @param port the number of the port to check.
+     * @return True if the port is free, false otherwise.
+     */
+    private static boolean isPortFree(int port) {
+
+        // If ServerSocket throws IO Exception, port is not open.
+        try {
+            new ServerSocket(port).close();
+            return true;
+        } catch (IOException ioe) {
+            return false;
+        }
+    }
+}
diff --git a/libraries/audio-test-harness/server/src/test/java/com/android/media/audiotestharness/server/AudioTestHarnessGrpcServerTests.java b/libraries/audio-test-harness/server/src/test/java/com/android/media/audiotestharness/server/AudioTestHarnessGrpcServerTests.java
new file mode 100644
index 0000000..8be66fd
--- /dev/null
+++ b/libraries/audio-test-harness/server/src/test/java/com/android/media/audiotestharness/server/AudioTestHarnessGrpcServerTests.java
@@ -0,0 +1,181 @@
+/*
+ * 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.media.audiotestharness.server;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+import com.android.media.audiotestharness.proto.AudioTestHarnessGrpc;
+import com.android.media.audiotestharness.proto.AudioTestHarnessService;
+import com.android.media.audiotestharness.server.utility.PortUtility;
+
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+
+import io.grpc.ManagedChannel;
+import io.grpc.ManagedChannelBuilder;
+
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.util.Iterator;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/** Tests for the {@link AudioTestHarnessGrpcServer} class. */
+@RunWith(JUnit4.class)
+public class AudioTestHarnessGrpcServerTests {
+
+    /**
+     * Tests for the {@link AudioTestHarnessGrpcServer#create(int, ExecutorService, Injector)},
+     * {@link AudioTestHarnessGrpcServer#createOnPort(int)}, and {@link
+     * AudioTestHarnessGrpcServer#createWithDefault()} methods.
+     */
+    @Test(expected = NullPointerException.class)
+    public void create_throwsNullPointerException_nullExecutor() throws Exception {
+        AudioTestHarnessGrpcServer.create(
+                /* port= */ 80, /* executor= */ null, Guice.createInjector());
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void create_throwsNullPointerException_nullInjector() throws Exception {
+        AudioTestHarnessGrpcServer.create(
+                /* port= */ 80, Executors.newSingleThreadExecutor(), /* injector= */ null);
+    }
+
+    @Test
+    public void createWithDefault_returnsNonNullResult() throws Exception {
+        assertNotNull(AudioTestHarnessGrpcServer.createWithDefault());
+    }
+
+    @Test
+    public void createWithPort_returnsNonNullResult() throws Exception {
+        assertNotNull(AudioTestHarnessGrpcServer.createOnPort(/* port= */ 80));
+    }
+
+    /** Tests for the {@link AudioTestHarnessGrpcServer#open()} method. */
+    @Test
+    public void open_buildsAndStartsGrpcServerAsExpected() throws Exception {
+        int port = PortUtility.nextAvailablePort();
+        AudioTestHarnessGrpcServer server =
+                setUpTestServer(port, Executors.newSingleThreadExecutor());
+
+        assertPortOpen(port);
+
+        server.open();
+
+        assertServerRunningAsExpected(port);
+    }
+
+    // Per (b/175643926) the following two tests are ignored since they do not run within Forrest.
+    // They do run (and pass) locally.
+
+    @Ignore
+    @Test(expected = IOException.class)
+    public void open_throwsIOExceptionForMissingServiceDependency() throws Exception {
+        AudioTestHarnessGrpcServer.create(
+                        8080, Executors.newSingleThreadExecutor(), Guice.createInjector())
+                .open();
+    }
+
+    @Ignore
+    @Test(expected = IOException.class)
+    public void open_throwsIOException_forPortTaken() throws Exception {
+        int port = PortUtility.nextAvailablePort();
+        AudioTestHarnessGrpcServer server =
+                setUpTestServer(port, Executors.newSingleThreadExecutor());
+
+        new ServerSocket(port);
+        server.open();
+    }
+
+    /** Tests for the {@link AudioTestHarnessGrpcServer#close()} method. */
+    @Test
+    public void close_shutsDownGrpcServerAsExpected() throws Exception {
+        int port = PortUtility.nextAvailablePort();
+        ExecutorService executorService = Executors.newSingleThreadExecutor();
+        AudioTestHarnessGrpcServer server = setUpTestServer(port, executorService);
+
+        server.open();
+        assertServerRunningAsExpected(port);
+
+        server.close();
+        assertPortOpen(port);
+    }
+
+    /**
+     * Builds a {@link AudioTestHarnessGrpcServer} on the specified port with the specified {@link
+     * ExecutorService} for testing purposes using the test implementation of the service.
+     *
+     * <p>While not the standard method of testing gRPC services, these tests aim to ensure that a
+     * working, production-ready server is properly set up and torn down by the {@link
+     * AudioTestHarnessGrpcServer} class. Thus, it uses an actual web server and executor to ensure
+     * that requires can be properly sent and received and that the setup and tear down actions
+     * operate as expected.
+     */
+    private static AudioTestHarnessGrpcServer setUpTestServer(
+            int port, ExecutorService executorService) {
+
+        // Build Guice injector to inject test implementation of the Audio Test Harness service.
+        Injector injector =
+                Guice.createInjector(
+                        binder ->
+                                binder.bind(AudioTestHarnessGrpc.AudioTestHarnessImplBase.class)
+                                        .to(AudioTestHarnessTestImpl.class));
+        return AudioTestHarnessGrpcServer.create(port, executorService, injector);
+    }
+
+    /**
+     * Verifies that the test gRPC Server was set up correctly by attempting to communicate with it
+     * over the specified port.
+     */
+    private static void assertServerRunningAsExpected(int port) {
+        // Attempt to communicate with running host over defined port.
+        ManagedChannel managedChannel =
+                ManagedChannelBuilder.forAddress("0.0.0.0", port).usePlaintext().build();
+        AudioTestHarnessGrpc.AudioTestHarnessBlockingStub stub =
+                AudioTestHarnessGrpc.newBlockingStub(managedChannel);
+        Iterator<AudioTestHarnessService.CaptureChunk> chunks =
+                stub.capture(AudioTestHarnessService.CaptureRequest.getDefaultInstance());
+
+        // Verify that the received message matches the expected message sent by the test
+        // implementation.
+        AudioTestHarnessService.CaptureChunk chunk = chunks.next();
+        assertArrayEquals(AudioTestHarnessTestImpl.MESSAGE, chunk.getData().toByteArray());
+        assertFalse(chunks.hasNext());
+
+        managedChannel.shutdown();
+    }
+
+    /**
+     * Verifies that a given port is open by attempting to create a {@link ServerSocket} on the
+     * port.
+     */
+    private static void assertPortOpen(int port) {
+        try {
+            new ServerSocket(port).close();
+        } catch (IOException ioe) {
+            fail("Expected Port to be open, but was actually not.");
+        }
+    }
+}
diff --git a/libraries/audio-test-harness/server/src/test/java/com/android/media/audiotestharness/server/AudioTestHarnessServerModuleTests.java b/libraries/audio-test-harness/server/src/test/java/com/android/media/audiotestharness/server/AudioTestHarnessServerModuleTests.java
new file mode 100644
index 0000000..68d5ae5
--- /dev/null
+++ b/libraries/audio-test-harness/server/src/test/java/com/android/media/audiotestharness/server/AudioTestHarnessServerModuleTests.java
@@ -0,0 +1,79 @@
+/*
+ * 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.media.audiotestharness.server;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import com.android.media.audiotestharness.proto.AudioTestHarnessGrpc;
+import com.android.media.audiotestharness.server.core.AudioSystemService;
+import com.android.media.audiotestharness.server.service.AudioCaptureSessionFactory;
+import com.android.media.audiotestharness.server.service.StreamObserverOutputStreamFactory;
+
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.concurrent.Executor;
+
+@RunWith(JUnit4.class)
+public class AudioTestHarnessServerModuleTests {
+
+    @Rule public final MockitoRule rule = MockitoJUnit.rule();
+
+    @Mock Executor mExecutor;
+
+    private Injector mInjector;
+
+    @Before
+    public void setUp() throws Exception {
+        this.mInjector = Guice.createInjector(AudioTestHarnessServerModule.create(mExecutor));
+    }
+
+    @Test
+    public void getInstance_executor_returnsProperExecutor() throws Exception {
+        assertEquals(mExecutor, mInjector.getInstance(Executor.class));
+    }
+
+    @Test
+    public void getInstance_AudioTestHarnessBaseImpl_returnsInstance() throws Exception {
+        assertNotNull(mInjector.getInstance(AudioTestHarnessGrpc.AudioTestHarnessImplBase.class));
+    }
+
+    @Test
+    public void getInstance_StreamObserverOutputStreamFactory_returnsInstance() throws Exception {
+        assertNotNull(mInjector.getInstance(StreamObserverOutputStreamFactory.class));
+    }
+
+    @Test
+    public void getInstance_AudioSystemService_returnsInstance() throws Exception {
+        assertNotNull(mInjector.getInstance(AudioSystemService.class));
+    }
+
+    @Test
+    public void getInstance_AudioCaptureSessionFactory_returnsInstance() throws Exception {
+        assertNotNull(mInjector.getInstance(AudioCaptureSessionFactory.class));
+    }
+}
diff --git a/libraries/audio-test-harness/server/src/test/java/com/android/media/audiotestharness/server/AudioTestHarnessTestImpl.java b/libraries/audio-test-harness/server/src/test/java/com/android/media/audiotestharness/server/AudioTestHarnessTestImpl.java
new file mode 100644
index 0000000..a11d01d
--- /dev/null
+++ b/libraries/audio-test-harness/server/src/test/java/com/android/media/audiotestharness/server/AudioTestHarnessTestImpl.java
@@ -0,0 +1,49 @@
+/*
+ * 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.media.audiotestharness.server;
+
+import com.android.media.audiotestharness.proto.AudioTestHarnessGrpc;
+import com.android.media.audiotestharness.proto.AudioTestHarnessService;
+
+import com.google.protobuf.ByteString;
+
+import io.grpc.stub.StreamObserver;
+
+/**
+ * Test implementation of the {@link
+ * com.android.media.audiotestharness.proto.AudioTestHarnessGrpc.AudioTestHarnessImplBase} that
+ * contains stubbed methods.
+ */
+public class AudioTestHarnessTestImpl extends AudioTestHarnessGrpc.AudioTestHarnessImplBase {
+
+    public static final byte[] MESSAGE = {0x0, 0x1, 0x2, 0x3};
+
+    /**
+     * Test implementation of the <code>Capture</code> procedure that simply ignores the request and
+     * sends back a predefined response containing the {@link #MESSAGE} bytes.
+     */
+    @Override
+    public void capture(
+            AudioTestHarnessService.CaptureRequest request,
+            StreamObserver<AudioTestHarnessService.CaptureChunk> responseObserver) {
+        responseObserver.onNext(
+                AudioTestHarnessService.CaptureChunk.newBuilder()
+                        .setData(ByteString.copyFrom(MESSAGE))
+                        .build());
+        responseObserver.onCompleted();
+    }
+}
diff --git a/libraries/audio-test-harness/server/src/test/java/com/android/media/audiotestharness/server/javasound/JavaAudioCapturerFactoryTests.java b/libraries/audio-test-harness/server/src/test/java/com/android/media/audiotestharness/server/javasound/JavaAudioCapturerFactoryTests.java
new file mode 100644
index 0000000..58413d8
--- /dev/null
+++ b/libraries/audio-test-harness/server/src/test/java/com/android/media/audiotestharness/server/javasound/JavaAudioCapturerFactoryTests.java
@@ -0,0 +1,96 @@
+/*
+ * 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.media.audiotestharness.server.javasound;
+
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.when;
+
+import com.android.media.audiotestharness.proto.AudioDeviceOuterClass;
+import com.android.media.audiotestharness.proto.AudioFormatOuterClass;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.concurrent.ExecutorService;
+
+import javax.sound.sampled.TargetDataLine;
+
+@RunWith(JUnit4.class)
+public class JavaAudioCapturerFactoryTests {
+
+    @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+    @Mock TargetDataLine mTargetDataLine;
+
+    @Mock ExecutorService mExecutorService;
+
+    JavaAudioCapturerFactory mJavaAudioCapturerFactory;
+
+    @Before
+    public void setUp() throws Exception {
+        mJavaAudioCapturerFactory = new JavaAudioCapturerFactory(mExecutorService);
+        when(mTargetDataLine.isOpen()).thenReturn(true);
+    }
+
+    @Test
+    public void build_returnsNonNullForValidParameters() throws Exception {
+        assertNotNull(
+                mJavaAudioCapturerFactory.build(
+                        AudioDeviceOuterClass.AudioDevice.getDefaultInstance(),
+                        AudioFormatOuterClass.AudioFormat.getDefaultInstance(),
+                        mTargetDataLine));
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void build_throwsNullPointerException_nullAudioDevice() throws Exception {
+        mJavaAudioCapturerFactory.build(
+                /* audioDevice= */ null,
+                AudioFormatOuterClass.AudioFormat.getDefaultInstance(),
+                mTargetDataLine);
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void build_throwsNullPointerException_nullAudioFormat() throws Exception {
+        mJavaAudioCapturerFactory.build(
+                AudioDeviceOuterClass.AudioDevice.getDefaultInstance(),
+                /* audioFormat= */ null,
+                mTargetDataLine);
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void build_throwsNullPointerException_nullTargetDataLine() throws Exception {
+        mJavaAudioCapturerFactory.build(
+                AudioDeviceOuterClass.AudioDevice.getDefaultInstance(),
+                AudioFormatOuterClass.AudioFormat.getDefaultInstance(),
+                /* targetDataLine= */ null);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void build_throwsIllegalArgumentException_closedTargetDataLine() throws Exception {
+        when(mTargetDataLine.isOpen()).thenReturn(false);
+        mJavaAudioCapturerFactory.build(
+                AudioDeviceOuterClass.AudioDevice.getDefaultInstance(),
+                AudioFormatOuterClass.AudioFormat.getDefaultInstance(),
+                mTargetDataLine);
+    }
+}
diff --git a/libraries/audio-test-harness/server/src/test/java/com/android/media/audiotestharness/server/javasound/JavaAudioCapturerTests.java b/libraries/audio-test-harness/server/src/test/java/com/android/media/audiotestharness/server/javasound/JavaAudioCapturerTests.java
new file mode 100644
index 0000000..dbfc51c
--- /dev/null
+++ b/libraries/audio-test-harness/server/src/test/java/com/android/media/audiotestharness/server/javasound/JavaAudioCapturerTests.java
@@ -0,0 +1,255 @@
+/*
+ * 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.media.audiotestharness.server.javasound;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.android.media.audiotestharness.common.Defaults;
+import com.android.media.audiotestharness.proto.AudioDeviceOuterClass;
+import com.android.media.audiotestharness.proto.AudioFormatOuterClass;
+import com.android.media.audiotestharness.server.core.AudioCapturer;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.rules.Timeout;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.util.concurrent.Executor;
+
+import javax.sound.sampled.TargetDataLine;
+
+@RunWith(JUnit4.class)
+public class JavaAudioCapturerTests {
+
+    @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
+    @Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+
+    /**
+     * Timeout to catch breakages where the TargetDataLineWatchingPublisher infinite loops. The
+     * slowest test takes about 50ms to run so a 20x buffer for timeout should be acceptable.
+     */
+    @Rule public Timeout mTimeout = Timeout.millis(1000);
+
+    @Mock Executor mExecutor;
+
+    @Mock TargetDataLine mTargetDataLine;
+
+    private static final AudioDeviceOuterClass.AudioDevice TEST_DEVICE = Defaults.AUDIO_DEVICE;
+    private static final AudioFormatOuterClass.AudioFormat TEST_FORMAT = Defaults.AUDIO_FORMAT;
+
+    @Test(expected = IllegalArgumentException.class)
+    public void create_throwsIllegalArgumentException_closedDataLine() throws Exception {
+        when(mTargetDataLine.isOpen()).thenReturn(false);
+        JavaAudioCapturer.create(TEST_DEVICE, TEST_FORMAT, mTargetDataLine, mExecutor);
+    }
+
+    @Test
+    public void open_startsDataLine() throws Exception {
+        when(mTargetDataLine.isOpen()).thenReturn(true);
+        when(mTargetDataLine.isRunning()).thenReturn(false);
+
+        JavaAudioCapturer.create(TEST_DEVICE, TEST_FORMAT, mTargetDataLine, mExecutor).open();
+
+        verify(mTargetDataLine).start();
+    }
+
+    @Test
+    public void open_schedulesPublisherTask() throws Exception {
+        when(mTargetDataLine.isOpen()).thenReturn(true);
+        when(mTargetDataLine.isRunning()).thenReturn(false);
+
+        JavaAudioCapturer.create(TEST_DEVICE, TEST_FORMAT, mTargetDataLine, mExecutor).open();
+
+        verify(mExecutor).execute(any(JavaAudioCapturer.TargetDataLineWatchingPublisher.class));
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void open_throwsIllegalStateException_alreadyRunningLine() throws Exception {
+        when(mTargetDataLine.isOpen()).thenReturn(true);
+        when(mTargetDataLine.isRunning()).thenReturn(true);
+
+        JavaAudioCapturer.create(TEST_DEVICE, TEST_FORMAT, mTargetDataLine, mExecutor).open();
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void open_throwsIllegalStateException_afterCloseCalled() throws Exception {
+        when(mTargetDataLine.isOpen()).thenReturn(true);
+        when(mTargetDataLine.isRunning()).thenReturn(false);
+        JavaAudioCapturer capturer =
+                JavaAudioCapturer.create(TEST_DEVICE, TEST_FORMAT, mTargetDataLine, mExecutor);
+
+        capturer.open();
+        capturer.close();
+
+        capturer.open();
+    }
+
+    @Test
+    public void attachOutput_attachesOutputSuccessfully_outputStream() throws Exception {
+        byte[] expectedBytes = {0x1, 0x2, 0x3, 0x4};
+
+        when(mTargetDataLine.isOpen()).thenReturn(true);
+        when(mTargetDataLine.isRunning()).thenReturn(false);
+
+        // Simulate a "read" from the data line when running through.
+        when(mTargetDataLine.read(any(), anyInt(), anyInt()))
+                .then(
+                        invocation -> {
+                            byte[] buffer = (byte[]) invocation.getArgument(0);
+
+                            buffer[0] = expectedBytes[0];
+                            buffer[1] = expectedBytes[1];
+                            buffer[2] = expectedBytes[2];
+                            buffer[3] = expectedBytes[3];
+
+                            return 4;
+                        });
+        ArgumentCaptor<Runnable> runnableArgCaptor = ArgumentCaptor.forClass(Runnable.class);
+
+        // Attach a byte array output stream to the runner.
+        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+        AudioCapturer capturer =
+                JavaAudioCapturer.create(TEST_DEVICE, TEST_FORMAT, mTargetDataLine, mExecutor);
+        capturer.attachOutput(byteArrayOutputStream);
+        capturer.open();
+
+        verify(mExecutor).execute(runnableArgCaptor.capture());
+        JavaAudioCapturer.TargetDataLineWatchingPublisher publisher =
+                (JavaAudioCapturer.TargetDataLineWatchingPublisher) runnableArgCaptor.getValue();
+
+        // Ensure the publisher only loops once by adding a stream that stops it.
+        capturer.attachOutput(new RunnableOutputStream(publisher::stop));
+        publisher.run();
+
+        // Verify output is as expected to the array.
+        assertArrayEquals(expectedBytes, byteArrayOutputStream.toByteArray());
+    }
+
+    @Test
+    public void attachOutput_attachesOutputSuccessfully_file() throws Exception {
+        byte[] expectedBytes = {0x1, 0x2, 0x3, 0x4};
+
+        when(mTargetDataLine.isOpen()).thenReturn(true);
+        when(mTargetDataLine.isRunning()).thenReturn(false);
+
+        // Simulate a "read" from the data line when running through.
+        when(mTargetDataLine.read(any(), anyInt(), anyInt()))
+                .then(
+                        invocation -> {
+                            byte[] buffer = (byte[]) invocation.getArgument(0);
+
+                            buffer[0] = expectedBytes[0];
+                            buffer[1] = expectedBytes[1];
+                            buffer[2] = expectedBytes[2];
+                            buffer[3] = expectedBytes[3];
+
+                            return 4;
+                        });
+        ArgumentCaptor<Runnable> runnableArgCaptor = ArgumentCaptor.forClass(Runnable.class);
+
+        File testFile = mTemporaryFolder.newFile();
+        AudioCapturer capturer =
+                JavaAudioCapturer.create(TEST_DEVICE, TEST_FORMAT, mTargetDataLine, mExecutor);
+        capturer.attachOutput(testFile);
+        capturer.open();
+
+        verify(mExecutor).execute(runnableArgCaptor.capture());
+        JavaAudioCapturer.TargetDataLineWatchingPublisher publisher =
+                (JavaAudioCapturer.TargetDataLineWatchingPublisher) runnableArgCaptor.getValue();
+
+        // Ensure the publisher only loops once by adding a stream that stops it.
+        capturer.attachOutput(new RunnableOutputStream(publisher::stop));
+        publisher.run();
+
+        // Verify output is as expected to the array.
+        assertArrayEquals(expectedBytes, Files.readAllBytes(testFile.toPath()));
+    }
+
+    @Test
+    public void close_stopsPublisherAsExpected() throws Exception {
+        when(mTargetDataLine.isOpen()).thenReturn(true);
+        when(mTargetDataLine.isRunning()).thenReturn(false);
+        ArgumentCaptor<Runnable> runnableArgCaptor = ArgumentCaptor.forClass(Runnable.class);
+
+        JavaAudioCapturer capturer =
+                JavaAudioCapturer.create(TEST_DEVICE, TEST_FORMAT, mTargetDataLine, mExecutor);
+        capturer.open();
+
+        verify(mExecutor).execute(runnableArgCaptor.capture());
+        JavaAudioCapturer.TargetDataLineWatchingPublisher publisher =
+                (JavaAudioCapturer.TargetDataLineWatchingPublisher) runnableArgCaptor.getValue();
+
+        assertTrue(publisher.isRunning());
+        capturer.close();
+        assertFalse(publisher.isRunning());
+    }
+
+    @Test
+    public void close_closesDataLineAsExpected() throws Exception {
+        when(mTargetDataLine.isOpen()).thenReturn(true);
+        when(mTargetDataLine.isRunning()).thenReturn(false);
+        JavaAudioCapturer capturer =
+                JavaAudioCapturer.create(TEST_DEVICE, TEST_FORMAT, mTargetDataLine, mExecutor);
+        capturer.open();
+
+        capturer.close();
+
+        verify(mTargetDataLine).stop();
+        verify(mTargetDataLine).close();
+    }
+
+    /** Custom {@link OutputStream} that triggers a contained Runnable when written to. */
+    private static class RunnableOutputStream extends OutputStream {
+        private final Runnable mRunnable;
+
+        private RunnableOutputStream(Runnable runnable) {
+            mRunnable = runnable;
+        }
+
+        @Override
+        public void write(int b) throws IOException {
+            mRunnable.run();
+        }
+
+        @Override
+        public void write(byte[] b, int off, int len) throws IOException {
+            mRunnable.run();
+        }
+
+        @Override
+        public void write(byte[] b) {
+            mRunnable.run();
+        }
+    }
+}
diff --git a/libraries/audio-test-harness/server/src/test/java/com/android/media/audiotestharness/server/javasound/JavaAudioSystemServiceTests.java b/libraries/audio-test-harness/server/src/test/java/com/android/media/audiotestharness/server/javasound/JavaAudioSystemServiceTests.java
new file mode 100644
index 0000000..4d9fc5c
--- /dev/null
+++ b/libraries/audio-test-harness/server/src/test/java/com/android/media/audiotestharness/server/javasound/JavaAudioSystemServiceTests.java
@@ -0,0 +1,218 @@
+/*
+ * 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.media.audiotestharness.server.javasound;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.android.media.audiotestharness.common.Defaults;
+import com.android.media.audiotestharness.proto.AudioDeviceOuterClass.AudioDevice;
+import com.android.media.audiotestharness.proto.AudioFormatOuterClass.AudioFormat;
+
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.io.IOException;
+
+import javax.sound.sampled.LineUnavailableException;
+import javax.sound.sampled.Mixer;
+import javax.sound.sampled.TargetDataLine;
+
+/** Tests for the {@link JavaAudioSystemService} class. */
+@RunWith(JUnit4.class)
+public class JavaAudioSystemServiceTests {
+
+    @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+    @Mock JavaAudioSystem mJavaAudioSystem;
+    @Mock JavaAudioCapturerFactory mAudioCapturerFactory;
+    @Mock TargetDataLine mTargetDataLine;
+
+    private JavaAudioSystemService mJavaAudioSystemService;
+
+    @Before
+    public void setUp() throws Exception {
+        mJavaAudioSystemService =
+                new JavaAudioSystemService(mJavaAudioSystem, mAudioCapturerFactory);
+    }
+
+    @Test
+    public void getDevices_returnsEmptyList_noConfiguredDevices() {
+        when(mJavaAudioSystem.getMixerInfo()).thenReturn(new Mixer.Info[0]);
+        assertEquals(0, mJavaAudioSystemService.getDevices().size());
+    }
+
+    @Test
+    public void getDevices_returnsProperDevices_twoConfiguredDevices() throws Exception {
+        configureJavaSoundSystemMockWithMixers(
+                new TestMixer(
+                        /* name= */ "Mixer One",
+                        /* targetLineCount= */ 2,
+                        /* targetDataLine= */ null),
+                new TestMixer(
+                        /* name= */ "Mixer Two",
+                        /* targetLineCount= */ 0,
+                        /* targetDataLine= */ null));
+
+        ImmutableSet<AudioDevice> expectedDevices =
+                ImmutableSet.of(
+                        AudioDevice.newBuilder()
+                                .setName("Mixer One")
+                                .addCapabilities(AudioDevice.Capability.CAPTURE)
+                                .build(),
+                        AudioDevice.newBuilder().setName("Mixer Two").build());
+        ImmutableSet<AudioDevice> actualDevices = mJavaAudioSystemService.getDevices();
+        assertEquals(expectedDevices, actualDevices);
+    }
+
+    @Test
+    public void createCapturerFor_buildsAudioCapturerWithFactoryCorrectly() throws Exception {
+        configureJavaSoundSystemMockWithMixers(
+                new TestMixer(
+                        /* name= */ "Mixer",
+                        /* targetLineCount= */ 1,
+                        /* targetDataLine= */ mTargetDataLine));
+
+        mJavaAudioSystemService.createCapturerFor(
+                AudioDevice.getDefaultInstance(), AudioFormat.getDefaultInstance());
+
+        verify(mAudioCapturerFactory)
+                .build(
+                        AudioDevice.getDefaultInstance(),
+                        AudioFormat.getDefaultInstance(),
+                        mTargetDataLine);
+    }
+
+    @Test
+    public void createCapturerFor_selectsProperMixerFromMultiple_forName() throws Exception {
+        configureJavaSoundSystemMockWithMixers(
+                new TestMixer(
+                        /* name= */ "Right Mixer",
+                        /* targetLineCount= */ 1,
+                        /* targetDataLine= */ mTargetDataLine),
+                new TestMixer(
+                        /* name= */ "Wrong Mixer 1",
+                        /* targetLineCount= */ 0,
+                        /* targetDataLine= */ null),
+                new TestMixer(
+                        /* name= */ "Wrong Mixer 2",
+                        /* targetLineCount= */ 2,
+                        /* targetDataLine= */ null));
+        AudioDevice audioDevice = AudioDevice.newBuilder().setName("Right").build();
+
+        mJavaAudioSystemService.createCapturerFor(audioDevice, AudioFormat.getDefaultInstance());
+
+        verify(mAudioCapturerFactory)
+                .build(audioDevice, AudioFormat.getDefaultInstance(), mTargetDataLine);
+    }
+
+    @Test
+    public void createCapturerFor_selectsProperMixerFromMultiple_Capability() throws Exception {
+        configureJavaSoundSystemMockWithMixers(
+                new TestMixer(
+                        /* name= */ "Right Mixer",
+                        /* targetLineCount= */ 1,
+                        /* targetDataLine= */ mTargetDataLine),
+                new TestMixer(
+                        /* name= */ "Wrong Mixer 1",
+                        /* targetLineCount= */ 0,
+                        /* targetDataLine= */ null),
+                new TestMixer(
+                        /* name= */ "Wrong Mixer 2",
+                        /* targetLineCount= */ 0,
+                        /* targetDataLine= */ null));
+        AudioDevice audioDevice =
+                AudioDevice.newBuilder().addCapabilities(AudioDevice.Capability.CAPTURE).build();
+
+        mJavaAudioSystemService.createCapturerFor(audioDevice, AudioFormat.getDefaultInstance());
+
+        verify(mAudioCapturerFactory)
+                .build(audioDevice, AudioFormat.getDefaultInstance(), mTargetDataLine);
+    }
+
+    @Test
+    public void createCapturerFor_opensTargetDataLineCorrectly() throws Exception {
+        configureJavaSoundSystemMockWithMixers(
+                new TestMixer(
+                        /* name= */ "Mixer",
+                        /* targetLineCount= */ 1,
+                        /* targetDataLine= */ mTargetDataLine));
+
+        ArgumentCaptor<javax.sound.sampled.AudioFormat> formatArgumentCaptor =
+                ArgumentCaptor.forClass(javax.sound.sampled.AudioFormat.class);
+
+        mJavaAudioSystemService.createCapturerFor(
+                AudioDevice.newBuilder().setName("Mixer").build(), Defaults.AUDIO_FORMAT);
+
+        verify(mTargetDataLine).open(formatArgumentCaptor.capture());
+        javax.sound.sampled.AudioFormat format = formatArgumentCaptor.getValue();
+        assertTrue(JavaSoundUtility.audioFormatFrom(Defaults.AUDIO_FORMAT).matches(format));
+    }
+
+    @Test(expected = IOException.class)
+    public void createCapturerFor_throwsIOException_failureToOpenTargetDataLine() throws Exception {
+        configureJavaSoundSystemMockWithMixers(
+                new TestMixer(
+                        /* name= */ "Mixer",
+                        /* targetLineCount= */ 1,
+                        /* targetDataLine= */ mTargetDataLine));
+        doThrow(LineUnavailableException.class).when(mTargetDataLine).open(any());
+
+        mJavaAudioSystemService.createCapturerFor(Defaults.AUDIO_DEVICE, Defaults.AUDIO_FORMAT);
+    }
+
+    @Test(expected = IOException.class)
+    public void createCapturerFor_throwsIOException_failureToGetTargetDataLine() throws Exception {
+        configureJavaSoundSystemMockWithMixers(
+                new TestMixer(
+                        /* name= */ "Mixer", /* targetLineCount= */ 1, /* targetDataLine= */ null));
+
+        mJavaAudioSystemService.createCapturerFor(Defaults.AUDIO_DEVICE, Defaults.AUDIO_FORMAT);
+    }
+
+    @Test(expected = IOException.class)
+    public void createCapturerFor_throwsIOException_noMixerFound() throws Exception {
+        when(mJavaAudioSystem.getMixerInfo()).thenReturn(new Mixer.Info[0]);
+
+        mJavaAudioSystemService.createCapturerFor(Defaults.AUDIO_DEVICE, Defaults.AUDIO_FORMAT);
+    }
+
+    public void configureJavaSoundSystemMockWithMixers(TestMixer... mixers) {
+        Mixer.Info[] mixerInfos = new Mixer.Info[mixers.length];
+
+        for (int i = 0; i < mixers.length; i++) {
+            mixerInfos[i] = mixers[i].getMixerInfo();
+            when(mJavaAudioSystem.getMixer(eq(mixerInfos[i]))).thenReturn(mixers[i]);
+        }
+
+        when(mJavaAudioSystem.getMixerInfo()).thenReturn(mixerInfos);
+    }
+}
diff --git a/libraries/audio-test-harness/server/src/test/java/com/android/media/audiotestharness/server/javasound/TestMixer.java b/libraries/audio-test-harness/server/src/test/java/com/android/media/audiotestharness/server/javasound/TestMixer.java
new file mode 100644
index 0000000..5b4e5c2
--- /dev/null
+++ b/libraries/audio-test-harness/server/src/test/java/com/android/media/audiotestharness/server/javasound/TestMixer.java
@@ -0,0 +1,170 @@
+/*
+ * 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.media.audiotestharness.server.javasound;
+
+import javax.sound.sampled.Control;
+import javax.sound.sampled.Line;
+import javax.sound.sampled.LineListener;
+import javax.sound.sampled.LineUnavailableException;
+import javax.sound.sampled.Mixer;
+import javax.sound.sampled.Mixer.Info;
+import javax.sound.sampled.TargetDataLine;
+
+/**
+ * Test implementation of the {@link Mixer} interface that scaffolds out enough functionality to be
+ * able to test the {@link JavaAudioSystemService}.
+ */
+public class TestMixer implements Mixer {
+
+    private final String mName;
+    private final int mTargetLineCount;
+    private final TargetDataLine mTargetDataLine;
+    private final TestInfo mTestInfo;
+
+    /**
+     * Constructor.
+     *
+     * @param name the name that should be provided in the {@link Mixer.Info} returned by the {@link
+     *     #getMixerInfo()} method.
+     * @param targetLineCount the number of target lines this Mixer should expose, in general, this
+     *     means that the returned arrays for any target line method are of this size.
+     */
+    public TestMixer(String name, int targetLineCount, TargetDataLine targetDataLine) {
+        mName = name;
+        mTargetLineCount = targetLineCount;
+        mTargetDataLine = targetDataLine;
+        mTestInfo = new TestInfo(mName, null, null, null);
+    }
+
+    @Override
+    public Info getMixerInfo() {
+        return mTestInfo;
+    }
+
+    @Override
+    public Line.Info[] getSourceLineInfo() {
+        return new Line.Info[0];
+    }
+
+    @Override
+    public Line.Info[] getTargetLineInfo() {
+        return new Line.Info[mTargetLineCount];
+    }
+
+    @Override
+    public Line.Info[] getSourceLineInfo(Line.Info info) {
+        return new Line.Info[0];
+    }
+
+    @Override
+    public Line.Info[] getTargetLineInfo(Line.Info info) {
+        return new Line.Info[mTargetLineCount];
+    }
+
+    @Override
+    public boolean isLineSupported(Line.Info info) {
+        return false;
+    }
+
+    @Override
+    public Line getLine(Line.Info info) throws LineUnavailableException {
+        if (mTargetDataLine == null) {
+            throw new LineUnavailableException("Unavailable");
+        } else {
+            return mTargetDataLine;
+        }
+    }
+
+    @Override
+    public int getMaxLines(Line.Info info) {
+        return 0;
+    }
+
+    @Override
+    public Line[] getSourceLines() {
+        return new Line[0];
+    }
+
+    @Override
+    public Line[] getTargetLines() {
+        return new Line[mTargetLineCount];
+    }
+
+    @Override
+    public void synchronize(Line[] lines, boolean maintainSync) {}
+
+    @Override
+    public void unsynchronize(Line[] lines) {}
+
+    @Override
+    public boolean isSynchronizationSupported(Line[] lines, boolean maintainSync) {
+        return false;
+    }
+
+    @Override
+    public Line.Info getLineInfo() {
+        return null;
+    }
+
+    @Override
+    public void open() throws LineUnavailableException {}
+
+    @Override
+    public void close() {}
+
+    @Override
+    public boolean isOpen() {
+        return false;
+    }
+
+    @Override
+    public Control[] getControls() {
+        return new Control[0];
+    }
+
+    @Override
+    public boolean isControlSupported(Control.Type control) {
+        return false;
+    }
+
+    @Override
+    public Control getControl(Control.Type control) {
+        return null;
+    }
+
+    @Override
+    public void addLineListener(LineListener listener) {}
+
+    @Override
+    public void removeLineListener(LineListener listener) {}
+
+    /** Test implementation of the Mixer.Info class for visibility here. */
+    public static class TestInfo extends Info {
+
+        /**
+         * Constructs a mixer's info object, passing it the given textual information.
+         *
+         * @param name the name of the mixer
+         * @param vendor the company who manufactures or creates the hardware or software mixer
+         * @param description descriptive text about the mixer
+         * @param version version information for the mixer
+         */
+        protected TestInfo(String name, String vendor, String description, String version) {
+            super(name, vendor, description, version);
+        }
+    }
+}
diff --git a/libraries/audio-test-harness/server/src/test/java/com/android/media/audiotestharness/server/service/AudioCaptureSessionFactoryImplTests.java b/libraries/audio-test-harness/server/src/test/java/com/android/media/audiotestharness/server/service/AudioCaptureSessionFactoryImplTests.java
new file mode 100644
index 0000000..63a7f0a
--- /dev/null
+++ b/libraries/audio-test-harness/server/src/test/java/com/android/media/audiotestharness/server/service/AudioCaptureSessionFactoryImplTests.java
@@ -0,0 +1,86 @@
+/*
+ * 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.media.audiotestharness.server.service;
+
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+import com.android.media.audiotestharness.server.core.AudioCapturer;
+
+import io.grpc.stub.ServerCallStreamObserver;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+/** Tests for the {@link AudioCaptureSessionFactoryImpl} class. */
+@RunWith(JUnit4.class)
+public class AudioCaptureSessionFactoryImplTests {
+
+    @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+    @Mock StreamObserverOutputStreamFactory mStreamObserverOutputStreamFactory;
+
+    @Mock ServerCallStreamObserver mStreamObserver;
+
+    @Mock AudioCapturer mAudioCapturer;
+
+    CaptureChunkStreamObserverOutputStream mCaptureChunkStreamObserverOutputStream;
+
+    AudioCaptureSessionFactoryImpl mAudioCaptureSessionFactory;
+
+    @Before
+    public void setUp() throws Exception {
+        mCaptureChunkStreamObserverOutputStream =
+                CaptureChunkStreamObserverOutputStream.create(mStreamObserver);
+        when(mStreamObserverOutputStreamFactory.createNewCaptureChunkStreamObserverOutputStream(
+                        any()))
+                .thenReturn(mCaptureChunkStreamObserverOutputStream);
+        this.mAudioCaptureSessionFactory =
+                new AudioCaptureSessionFactoryImpl(mStreamObserverOutputStreamFactory);
+    }
+
+    @Test
+    public void createCaptureSession_returnsNonNullCaptureSession() {
+        assertNotNull(
+                mAudioCaptureSessionFactory.createCaptureSession(mStreamObserver, mAudioCapturer));
+    }
+
+    @Test
+    public void createCaptureSession_usesUniqueIdentifiers() throws Exception {
+        AudioCaptureSession audioCaptureSessionOne =
+                mAudioCaptureSessionFactory.createCaptureSession(mStreamObserver, mAudioCapturer);
+        AudioCaptureSession audioCaptureSessionTwo =
+                mAudioCaptureSessionFactory.createCaptureSession(mStreamObserver, mAudioCapturer);
+        AudioCaptureSession audioCaptureSessionThree =
+                mAudioCaptureSessionFactory.createCaptureSession(mStreamObserver, mAudioCapturer);
+
+        assertNotEquals(
+                audioCaptureSessionOne.getSessionId(), audioCaptureSessionTwo.getSessionId());
+        assertNotEquals(
+                audioCaptureSessionOne.getSessionId(), audioCaptureSessionThree.getSessionId());
+        assertNotEquals(
+                audioCaptureSessionTwo.getSessionId(), audioCaptureSessionThree.getSessionId());
+    }
+}
diff --git a/libraries/audio-test-harness/server/src/test/java/com/android/media/audiotestharness/server/service/AudioCaptureSessionTests.java b/libraries/audio-test-harness/server/src/test/java/com/android/media/audiotestharness/server/service/AudioCaptureSessionTests.java
new file mode 100644
index 0000000..4a1ee58
--- /dev/null
+++ b/libraries/audio-test-harness/server/src/test/java/com/android/media/audiotestharness/server/service/AudioCaptureSessionTests.java
@@ -0,0 +1,193 @@
+/*
+ * 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.media.audiotestharness.server.service;
+
+import static org.hamcrest.core.IsNull.notNullValue;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.android.media.audiotestharness.server.core.AudioCapturer;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+/** Tests for the {@link AudioCaptureSession} class. */
+@RunWith(JUnit4.class)
+public class AudioCaptureSessionTests {
+
+    @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+    @Rule public ExpectedException mExceptionRule = ExpectedException.none();
+
+    @Mock AudioCapturer mAudioCapturer;
+
+    @Mock CaptureChunkStreamObserverOutputStream mCaptureChunkStreamObserverOutputStream;
+
+    /**
+     * Tests for the {@link AudioCaptureSession#create(int, AudioCapturer,
+     * CaptureChunkStreamObserverOutputStream)} method.
+     */
+    @Test(expected = NullPointerException.class)
+    public void create_throwsNullPointerException_nullAudioCapturer() throws Exception {
+        AudioCaptureSession.create(
+                /* id= */ 1, /* audioCapturer= */ null, mCaptureChunkStreamObserverOutputStream);
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void create_throwsNullPointerException_nullCaptureChunkStreamObserverOutputStream()
+            throws Exception {
+        AudioCaptureSession.create(
+                /* id= */ 1, mAudioCapturer, /* captureChunkStreamObserverOutputStream= */ null);
+    }
+
+    @Test
+    public void create_attachesOutputStreamToAudioCapturer() throws Exception {
+        AudioCaptureSession.create(1, mAudioCapturer, mCaptureChunkStreamObserverOutputStream);
+        verify(mAudioCapturer).attachOutput(mCaptureChunkStreamObserverOutputStream);
+    }
+
+    @Test
+    public void create_returnsNonNullSession() throws Exception {
+        assertNotNull(
+                AudioCaptureSession.create(
+                        1, mAudioCapturer, mCaptureChunkStreamObserverOutputStream));
+    }
+
+    /** Tests for the {@link AudioCaptureSession#start()} methods. */
+    @Test
+    public void start_opensAudioCapturer() throws Exception {
+        AudioCaptureSession audioCaptureSession =
+                AudioCaptureSession.create(
+                        1, mAudioCapturer, mCaptureChunkStreamObserverOutputStream);
+
+        audioCaptureSession.start();
+
+        verify(mAudioCapturer).open();
+    }
+
+    @Test
+    public void start_throwsIOException_whenCapturerFails() throws Exception {
+        AudioCaptureSession audioCaptureSession =
+                AudioCaptureSession.create(
+                        1, mAudioCapturer, mCaptureChunkStreamObserverOutputStream);
+        doThrow(IOException.class).when(mAudioCapturer).open();
+        mExceptionRule.expectMessage("Failed to open AudioCapturer for Audio Capture Session");
+        mExceptionRule.expectCause(notNullValue(Throwable.class));
+
+        audioCaptureSession.start();
+    }
+
+    /** Tests for the {@link AudioCaptureSession#stop()} method. */
+    @Test
+    public void stop_closesAudioCapturer() throws Exception {
+        AudioCaptureSession audioCaptureSession =
+                AudioCaptureSession.create(
+                        1, mAudioCapturer, mCaptureChunkStreamObserverOutputStream);
+
+        audioCaptureSession.stop();
+
+        verify(mAudioCapturer).close();
+    }
+
+    @Test
+    public void stop_closesCaptureChunkOutputStream() throws Exception {
+        AudioCaptureSession audioCaptureSession =
+                AudioCaptureSession.create(
+                        1, mAudioCapturer, mCaptureChunkStreamObserverOutputStream);
+
+        audioCaptureSession.stop();
+
+        verify(mCaptureChunkStreamObserverOutputStream).close();
+    }
+
+    @Test
+    public void stop_suppressesErrorsWhenAudioCapturerIsClosed() throws Exception {
+        AudioCaptureSession audioCaptureSession =
+                AudioCaptureSession.create(
+                        1, mAudioCapturer, mCaptureChunkStreamObserverOutputStream);
+        doThrow(IOException.class).when(mAudioCapturer).close();
+
+        audioCaptureSession.stop();
+
+        // No need to assert since test should pass as long as no exception is thrown.
+    }
+
+    /** Tests for the {@link AudioCaptureSession#awaitStop(long, TimeUnit)} method. */
+    @Test
+    public void awaitStop_waitsOnCaptureChunkStreamObserverToClose() throws Exception {
+        AudioCaptureSession audioCaptureSession =
+                AudioCaptureSession.create(
+                        1, mAudioCapturer, mCaptureChunkStreamObserverOutputStream);
+
+        audioCaptureSession.awaitStop(1234, TimeUnit.SECONDS);
+
+        verify(mCaptureChunkStreamObserverOutputStream).awaitClose(1234, TimeUnit.SECONDS);
+    }
+
+    @Test
+    public void awaitStop_closesAudioCapturerWhenTimeoutReached() throws Exception {
+        AudioCaptureSession audioCaptureSession =
+                AudioCaptureSession.create(
+                        1, mAudioCapturer, mCaptureChunkStreamObserverOutputStream);
+        when(mCaptureChunkStreamObserverOutputStream.awaitClose(anyLong(), any()))
+                .thenReturn(false);
+
+        audioCaptureSession.awaitStop(1234, TimeUnit.SECONDS);
+
+        verify(mAudioCapturer).close();
+    }
+
+    @Test
+    public void awaitStop_closesCaptureChunkOutputStreamWhenTimeoutReached() throws Exception {
+        AudioCaptureSession audioCaptureSession =
+                AudioCaptureSession.create(
+                        1, mAudioCapturer, mCaptureChunkStreamObserverOutputStream);
+        when(mCaptureChunkStreamObserverOutputStream.awaitClose(anyLong(), any()))
+                .thenReturn(false);
+
+        audioCaptureSession.awaitStop(1234, TimeUnit.SECONDS);
+
+        verify(mCaptureChunkStreamObserverOutputStream).close();
+    }
+
+    @Test
+    public void awaitStop_throwsIOException_whenInterrupted() throws Exception {
+        AudioCaptureSession audioCaptureSession =
+                AudioCaptureSession.create(
+                        1, mAudioCapturer, mCaptureChunkStreamObserverOutputStream);
+        when(mCaptureChunkStreamObserverOutputStream.awaitClose(anyLong(), any()))
+                .thenThrow(InterruptedException.class);
+        mExceptionRule.expectMessage(
+                "Interrupted while waiting for capture to finish for Audio Capture Session");
+        mExceptionRule.expectCause(notNullValue(InterruptedException.class));
+
+        audioCaptureSession.awaitStop(1234, TimeUnit.SECONDS);
+    }
+}
diff --git a/libraries/audio-test-harness/server/src/test/java/com/android/media/audiotestharness/server/service/AudioTestHarnessImplTests.java b/libraries/audio-test-harness/server/src/test/java/com/android/media/audiotestharness/server/service/AudioTestHarnessImplTests.java
new file mode 100644
index 0000000..e64f2f3
--- /dev/null
+++ b/libraries/audio-test-harness/server/src/test/java/com/android/media/audiotestharness/server/service/AudioTestHarnessImplTests.java
@@ -0,0 +1,240 @@
+/*
+ * 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.media.audiotestharness.server.service;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.android.media.audiotestharness.proto.AudioTestHarnessGrpc;
+import com.android.media.audiotestharness.proto.AudioTestHarnessService;
+import com.android.media.audiotestharness.server.core.AudioCapturer;
+import com.android.media.audiotestharness.server.core.AudioSystemService;
+
+import com.google.protobuf.ByteString;
+
+import io.grpc.Context;
+import io.grpc.ManagedChannel;
+import io.grpc.Status;
+import io.grpc.StatusRuntimeException;
+import io.grpc.inprocess.InProcessChannelBuilder;
+import io.grpc.inprocess.InProcessServerBuilder;
+import io.grpc.stub.ServerCallStreamObserver;
+import io.grpc.stub.StreamObserver;
+import io.grpc.testing.GrpcCleanupRule;
+
+import org.hamcrest.CustomMatcher;
+import org.hamcrest.Matcher;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicReference;
+
+/** Tests for the {@link AudioTestHarnessImpl} class. */
+@RunWith(JUnit4.class)
+public class AudioTestHarnessImplTests {
+
+    public static final byte[] TEST_PAYLOAD = {0x1, 0x2, 0x3, 0x4};
+
+    @Rule public GrpcCleanupRule mGrpcCleanupRule = new GrpcCleanupRule();
+
+    @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+    @Rule public ExpectedException mExceptionRule = ExpectedException.none();
+
+    @Mock AudioSystemService mAudioSystemService;
+
+    @Mock AudioCapturer mAudioCapturer;
+
+    @Mock AudioCaptureSession mAudioCaptureSession;
+
+    @Mock AudioCaptureSessionFactory mAudioCaptureSessionFactory;
+
+    /** Stubs used to communicate with the service-under-test. */
+    private AudioTestHarnessGrpc.AudioTestHarnessBlockingStub mBlockingStub;
+
+    private AudioTestHarnessGrpc.AudioTestHarnessStub mStub;
+
+    @Before
+    public void setUp() throws Exception {
+        String serverName = InProcessServerBuilder.generateName();
+
+        // Create and Start In-Process Server
+        mGrpcCleanupRule.register(
+                InProcessServerBuilder.forName(serverName)
+                        .directExecutor()
+                        .addService(
+                                new AudioTestHarnessImpl(
+                                        mAudioSystemService, mAudioCaptureSessionFactory))
+                        .build()
+                        .start());
+
+        // Create and Start Stubs for interacting with the Service
+        ManagedChannel channel =
+                mGrpcCleanupRule.register(
+                        InProcessChannelBuilder.forName(serverName).directExecutor().build());
+        mBlockingStub = AudioTestHarnessGrpc.newBlockingStub(channel);
+        mStub = AudioTestHarnessGrpc.newStub(channel);
+
+        // Ensure the mocks output is valid.
+        when(mAudioSystemService.createDefaultCapturer()).thenReturn(mAudioCapturer);
+        when(mAudioCaptureSessionFactory.createCaptureSession(any(), any()))
+                .then(
+                        (inv) -> {
+
+                            // Ensure that the stream observer is closed properly so it can be
+                            // cleaned up.
+                            ServerCallStreamObserver<AudioTestHarnessService.CaptureChunk>
+                                    streamObserver = inv.getArgument(0);
+                            streamObserver.onCompleted();
+
+                            return mAudioCaptureSession;
+                        });
+    }
+
+    @Test
+    public void capture_properlyAllocatesDefaultCapturer() throws Exception {
+        mBlockingStub.capture(AudioTestHarnessService.CaptureRequest.getDefaultInstance());
+        verify(mAudioSystemService).createDefaultCapturer();
+    }
+
+    @Test
+    public void capture_properlyCreatesCaptureSession() throws Exception {
+        mBlockingStub.capture(AudioTestHarnessService.CaptureRequest.getDefaultInstance());
+        verify(mAudioCaptureSessionFactory).createCaptureSession(any(), eq(mAudioCapturer));
+    }
+
+    @Test
+    public void capture_callsStopOnSessionWhenCanceled() throws Exception {
+        AtomicReference<StreamObserver<AudioTestHarnessService.CaptureChunk>>
+                streamObserverReference = new AtomicReference<>();
+        reset(mAudioCaptureSessionFactory);
+        when(mAudioCaptureSessionFactory.createCaptureSession(any(), any()))
+                .thenAnswer(
+                        (invocation -> {
+                            // Grab a reference to the stream observer, then return the mock.
+                            streamObserverReference.set(invocation.getArgument(0));
+                            return mAudioCaptureSession;
+                        }));
+
+        doAnswer(
+                        (invocation) -> {
+                            if (streamObserverReference.get() != null) {
+                                streamObserverReference
+                                        .get()
+                                        .onNext(
+                                                AudioTestHarnessService.CaptureChunk.newBuilder()
+                                                        .setData(ByteString.copyFrom(TEST_PAYLOAD))
+                                                        .build());
+                            }
+                            return null;
+                        })
+                .when(mAudioCaptureSession)
+                .start();
+
+        Context.CancellableContext cancellableContext = Context.current().withCancellation();
+        cancellableContext.run(
+                () ->
+                        mStub.capture(
+                                AudioTestHarnessService.CaptureRequest.getDefaultInstance(),
+                                new StreamObserver<AudioTestHarnessService.CaptureChunk>() {
+                                    @Override
+                                    public void onNext(AudioTestHarnessService.CaptureChunk value) {
+                                        cancellableContext.cancel(Status.CANCELLED.asException());
+                                    }
+
+                                    @Override
+                                    public void onError(Throwable t) {}
+
+                                    @Override
+                                    public void onCompleted() {}
+                                }));
+
+        verify(mAudioCaptureSession).stop();
+    }
+
+    @Test
+    public void capture_properlyStartsCaptureSession() throws Exception {
+        mBlockingStub.capture(AudioTestHarnessService.CaptureRequest.getDefaultInstance());
+        verify(mAudioCaptureSession).start();
+    }
+
+    @Test
+    public void capture_throwsProperStatusException_failureToOpenCapturer() throws Exception {
+        when(mAudioSystemService.createDefaultCapturer())
+                .thenThrow(new IOException("Some exception occurred."));
+
+        mExceptionRule.expect(
+                generateCustomMatcherForExpected(
+                        /* expectedDescription= */ "Failed to allocate default AudioCapturer",
+                        Status.UNAVAILABLE));
+        mBlockingStub
+                .capture(AudioTestHarnessService.CaptureRequest.getDefaultInstance())
+                .forEachRemaining(chunk -> {});
+    }
+
+    @Test
+    public void capture_throwsProperStatusException_failureToStartCapturer() throws Exception {
+        reset(mAudioCaptureSessionFactory);
+        when(mAudioCaptureSessionFactory.createCaptureSession(any(), any()))
+                .thenReturn(mAudioCaptureSession);
+        doThrow(new IOException("Capturer Start Failure!")).when(mAudioCaptureSession).start();
+
+        mExceptionRule.expect(
+                generateCustomMatcherForExpected(
+                        /* expectedDescription= */ "Capturer Start Failure!", Status.INTERNAL));
+
+        mBlockingStub
+                .capture(AudioTestHarnessService.CaptureRequest.getDefaultInstance())
+                .forEachRemaining(chunk -> {});
+    }
+
+    /**
+     * Generates a {@link org.hamcrest.Matcher} that matches a given {@link StatusRuntimeException}
+     * if the description and status code parameters are an exact match.
+     */
+    public Matcher<StatusRuntimeException> generateCustomMatcherForExpected(
+            String expectedDescription, Status expectedStatus) {
+        return new CustomMatcher<StatusRuntimeException>(
+                String.format(
+                        "StatusRuntimeException with Message (%s) and Status (%s)",
+                        expectedDescription, expectedStatus)) {
+            @Override
+            public boolean matches(Object item) {
+                if (item instanceof StatusRuntimeException) {
+                    StatusRuntimeException exception = (StatusRuntimeException) item;
+                    return exception.getStatus().getCode().equals(expectedStatus.getCode())
+                            && exception.getStatus().getDescription() != null
+                            && exception.getStatus().getDescription().equals(expectedDescription);
+                }
+                return false;
+            }
+        };
+    }
+}
diff --git a/libraries/audio-test-harness/server/src/test/java/com/android/media/audiotestharness/server/service/CaptureChunkStreamObserverOutputStreamTests.java b/libraries/audio-test-harness/server/src/test/java/com/android/media/audiotestharness/server/service/CaptureChunkStreamObserverOutputStreamTests.java
new file mode 100644
index 0000000..288b6fc
--- /dev/null
+++ b/libraries/audio-test-harness/server/src/test/java/com/android/media/audiotestharness/server/service/CaptureChunkStreamObserverOutputStreamTests.java
@@ -0,0 +1,372 @@
+/*
+ * 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.media.audiotestharness.server.service;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.android.media.audiotestharness.proto.AudioTestHarnessService;
+
+import com.google.common.collect.ImmutableList;
+
+import io.grpc.stub.ServerCallStreamObserver;
+import io.grpc.stub.StreamObserver;
+
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.TimeUnit;
+
+/** Tests for the {@link CaptureChunkStreamObserverOutputStream}. */
+@RunWith(JUnitParamsRunner.class)
+public class CaptureChunkStreamObserverOutputStreamTests {
+
+    @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+    @Mock CountDownLatch mCountDownLatch;
+
+    @Test(expected = NullPointerException.class)
+    public void create_throwsNullPointerException_nullStreamObserver() throws Exception {
+        CaptureChunkStreamObserverOutputStream.create(/* captureChunkStreamObserver= */ null);
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void createInternal_throwsNullPointerException_nullStreamObserver() throws Exception {
+        CaptureChunkStreamObserverOutputStream.create(
+                /* captureChunkStreamObserver= */ null, new CountDownLatch(0));
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void createInternal_throwsNullPointerException_nullCountDownLatch() throws Exception {
+        CaptureChunkStreamObserverOutputStream.create(
+                createStreamObserverThatWritesTo(new ArrayList<>()), /* countDownLatch= */ null);
+    }
+
+    @Test
+    @Parameters(method = "getSingleByteParams")
+    public void write_writesAsExpected_singleByte(
+            String testName, int byteToWrite, int expectedByte) throws Exception {
+        ArrayList<byte[]> writtenBytes = new ArrayList<>();
+        OutputStream stream =
+                CaptureChunkStreamObserverOutputStream.create(
+                        createStreamObserverThatWritesTo(writtenBytes), mCountDownLatch);
+
+        stream.write(byteToWrite);
+
+        byte[] expectedByteArray = new byte[1];
+        expectedByteArray[0] = (byte) expectedByte;
+
+        assertListsContainSameByteArrays(
+                testName, writtenBytes, ImmutableList.of(expectedByteArray));
+    }
+
+    public Object[] getSingleByteParams() throws Exception {
+        return new Object[][] {
+            {"Test Case One", 125, 0x7D},
+            {"Test Case Two", 254658, 0xC2},
+            {"Test Case Three", -44587, 0xD5}
+        };
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void write_throwsIllegalStateException_singleByte_afterClose() throws Exception {
+        OutputStream stream =
+                CaptureChunkStreamObserverOutputStream.create(
+                        createStreamObserverThatWritesTo(new ArrayList<>()), mCountDownLatch);
+        stream.close();
+
+        stream.write(/* b= */ 42);
+    }
+
+    @Test
+    @Parameters(method = "getByteArrayParams")
+    public void write_writesAsExpected_multipleBytes(
+            String testName, List<byte[]> byteArraysToWrite) throws Exception {
+        ArrayList<byte[]> writtenBytes = new ArrayList<>();
+        OutputStream stream =
+                CaptureChunkStreamObserverOutputStream.create(
+                        createStreamObserverThatWritesTo(writtenBytes), mCountDownLatch);
+
+        // Write all of the arrays.
+        for (byte[] bytes : byteArraysToWrite) {
+            stream.write(bytes);
+        }
+
+        assertListsContainSameByteArrays(testName, byteArraysToWrite, writtenBytes);
+    }
+
+    public Object[] getByteArrayParams() throws Exception {
+        byte[][] testBytes = generateRandomByteArrayData(/* numArrays= */ 4);
+        return new Object[][] {
+            {"Test Case One", ImmutableList.of(testBytes[0])},
+            {
+                "Test Case Two",
+                ImmutableList.of(testBytes[0], testBytes[1], testBytes[2], testBytes[3])
+            },
+            {"Test Case Three", ImmutableList.of(testBytes[3], testBytes[1])},
+        };
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void write_throwsIllegalStateException_multipleBytes_afterClose() throws Exception {
+        OutputStream stream =
+                CaptureChunkStreamObserverOutputStream.create(
+                        createStreamObserverThatWritesTo(new ArrayList<>()), mCountDownLatch);
+        stream.close();
+
+        stream.write(new byte[4]);
+    }
+
+    @Test
+    @Parameters(method = "getByteArrayParamsWithOffsetsAndLengths")
+    public void write_writesAsExpected_multipleBytesOffsetAndLength(
+            String testName,
+            List<byte[]> byteArraysToWrite,
+            List<Integer> offsets,
+            List<Integer> lengths,
+            List<byte[]> expectedByteArrays)
+            throws Exception {
+        ArrayList<byte[]> writtenBytes = new ArrayList<>();
+        OutputStream stream =
+                CaptureChunkStreamObserverOutputStream.create(
+                        createStreamObserverThatWritesTo(writtenBytes), mCountDownLatch);
+
+        // Write all of the arrays with proper offsets and lengths.
+        for (int i = 0; i < byteArraysToWrite.size(); i++) {
+            stream.write(byteArraysToWrite.get(i), offsets.get(i), lengths.get(i));
+        }
+
+        assertListsContainSameByteArrays(testName, expectedByteArrays, writtenBytes);
+    }
+
+    public Object[] getByteArrayParamsWithOffsetsAndLengths() throws Exception {
+        byte[][] testBytes = generateRandomByteArrayData(/* numArrays= */ 4);
+        return new Object[][] {
+            {
+                "Test Case One",
+                /* byteArraysToWrite= */ ImmutableList.of(testBytes[0]),
+                /* offsets= */ ImmutableList.of(0),
+                /* lengths= */ ImmutableList.of(8),
+                /* expectedByteArrays= */ ImmutableList.of(testBytes[0])
+            },
+            {
+                "Test Case Two",
+                /* byteArraysToWrite= */ ImmutableList.of(
+                        testBytes[0], testBytes[1], testBytes[2], testBytes[3]),
+                /* offsets= */ ImmutableList.of(0, 0, 0, 0),
+                /* lengths= */ ImmutableList.of(8, 16, 24, 32),
+                /* expectedByteArrays= */ ImmutableList.of(
+                        testBytes[0], testBytes[1], testBytes[2], testBytes[3])
+            },
+            {
+                "Test Case Three",
+                /* byteArraysToWrite= */ ImmutableList.of(testBytes[0]),
+                /* offsets= */ ImmutableList.of(2),
+                /* lengths= */ ImmutableList.of(5),
+                /* expectedByteArrays= */ ImmutableList.of(Arrays.copyOfRange(testBytes[0], 2, 7))
+            },
+            {
+                "Test Case Four",
+                /* byteArraysToWrite= */ ImmutableList.of(
+                        testBytes[0], testBytes[1], testBytes[2], testBytes[3]),
+                /* offsets= */ ImmutableList.of(2, 10, 14, 25),
+                /* lengths= */ ImmutableList.of(4, 5, 9, 2),
+                /* expectedByteArrays= */ ImmutableList.of(
+                        Arrays.copyOfRange(testBytes[0], 2, 6),
+                        Arrays.copyOfRange(testBytes[1], 10, 15),
+                        Arrays.copyOfRange(testBytes[2], 14, 23),
+                        Arrays.copyOfRange(testBytes[3], 25, 27))
+            },
+            {
+                "Test Case Five",
+                /* byteArraysToWrite= */ ImmutableList.of(testBytes[3], testBytes[1]),
+                /* offsets= */ ImmutableList.of(17, 2),
+                /* lengths= */ ImmutableList.of(15, 10),
+                /* expectedByteArrays= */ ImmutableList.of(
+                        Arrays.copyOfRange(testBytes[3], 17, 32),
+                        Arrays.copyOfRange(testBytes[1], 2, 12))
+            },
+        };
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void write_throwsIllegalStateException_multipleBytesOffsetAndLength_afterClose()
+            throws Exception {
+        OutputStream stream =
+                CaptureChunkStreamObserverOutputStream.create(
+                        createStreamObserverThatWritesTo(new ArrayList<>()), mCountDownLatch);
+        stream.close();
+
+        stream.write(new byte[4], 2, 1);
+    }
+
+    @Test
+    public void close_countDownsCountDownLatch() throws Exception {
+        OutputStream stream =
+                CaptureChunkStreamObserverOutputStream.create(
+                        createStreamObserverThatWritesTo(new ArrayList<>()), mCountDownLatch);
+
+        stream.close();
+
+        verify(mCountDownLatch).countDown();
+    }
+
+    @Test
+    public void awaitClose_callsCountDownLatchAwait() throws Exception {
+        CaptureChunkStreamObserverOutputStream stream =
+                CaptureChunkStreamObserverOutputStream.create(
+                        createStreamObserverThatWritesTo(new ArrayList<>()), mCountDownLatch);
+
+        stream.awaitClose();
+
+        verify(mCountDownLatch).await();
+    }
+
+    @Test
+    public void awaitClose_callsCountDownLatchAwait_withParameters() throws Exception {
+        CaptureChunkStreamObserverOutputStream stream =
+                CaptureChunkStreamObserverOutputStream.create(
+                        createStreamObserverThatWritesTo(new ArrayList<>()), mCountDownLatch);
+
+        stream.awaitClose(/* timeout= */ 1, TimeUnit.HOURS);
+
+        verify(mCountDownLatch).await(/* timeout= */ 1, TimeUnit.HOURS);
+    }
+
+    @Test
+    public void awaitClose_returnsProperValue_withParameters() throws Exception {
+        when(mCountDownLatch.await(anyLong(), any())).thenReturn(true);
+
+        CaptureChunkStreamObserverOutputStream stream =
+                CaptureChunkStreamObserverOutputStream.create(
+                        createStreamObserverThatWritesTo(new ArrayList<>()), mCountDownLatch);
+
+        assertTrue(stream.awaitClose(/* timeout= */ 1, TimeUnit.HOURS));
+    }
+
+    /**
+     * Asserts that the two provided {@link List}s contain the same byte arrays in the same order.
+     *
+     * <p>Appends the 'message' parameter to the end of failure messages.
+     */
+    private void assertListsContainSameByteArrays(
+            String message, List<byte[]> expected, List<byte[]> actual) {
+        assertEquals(
+                String.format("Failure, Written Data Size does not match Expected: %s", message),
+                expected.size(),
+                actual.size());
+        for (int i = 0; i < expected.size(); i++) {
+            byte[] expectedBytes = expected.get(i);
+            byte[] actualBytes = actual.get(i);
+
+            assertArrayEquals(
+                    String.format(
+                            "Failure, Payload at Index %d does not match Expected: %s", i, message),
+                    expectedBytes,
+                    actualBytes);
+        }
+    }
+
+    /**
+     * Helper method that generates an array containing numArray byte arrays with random bytes.
+     *
+     * <p>The generated arrays are incrementally larger in multiples of 8. So for example, asking
+     * for three arrays results in an array containing 3 separate byte arrays each with lengths 8,
+     * 16, and 24 bytes respectively.
+     */
+    private static byte[][] generateRandomByteArrayData(int numArrays) {
+        Random random = ThreadLocalRandom.current();
+
+        byte[][] result = new byte[numArrays][];
+
+        for (int i = 0; i < numArrays; i++) {
+            result[i] = new byte[8 * (i + 1)];
+            random.nextBytes(result[i]);
+        }
+
+        return result;
+    }
+
+    /**
+     * Creates a new {@link StreamObserver} that writes the results of the write actions to the
+     * provided list for later analysis and verification.
+     *
+     * @param writtenBytes a List of byte[]s representing each individual call to write.
+     */
+    private static ServerCallStreamObserver<AudioTestHarnessService.CaptureChunk>
+            createStreamObserverThatWritesTo(final List<byte[]> writtenBytes) {
+        return new ServerCallStreamObserver<AudioTestHarnessService.CaptureChunk>() {
+            @Override
+            public boolean isReady() {
+                return false;
+            }
+
+            @Override
+            public void setOnReadyHandler(Runnable onReadyHandler) {}
+
+            @Override
+            public void disableAutoInboundFlowControl() {}
+
+            @Override
+            public void request(int count) {}
+
+            @Override
+            public void setMessageCompression(boolean enable) {}
+
+            @Override
+            public boolean isCancelled() {
+                return false;
+            }
+
+            @Override
+            public void setOnCancelHandler(Runnable onCancelHandler) {}
+
+            @Override
+            public void setCompression(String compression) {}
+
+            @Override
+            public void onNext(AudioTestHarnessService.CaptureChunk value) {
+                writtenBytes.add(value.getData().toByteArray());
+            }
+
+            @Override
+            public void onError(Throwable t) {}
+
+            @Override
+            public void onCompleted() {}
+        };
+    }
+}
diff --git a/libraries/audio-test-harness/server/src/test/java/com/android/media/audiotestharness/server/service/StreamObserverOutputStreamFactoryTests.java b/libraries/audio-test-harness/server/src/test/java/com/android/media/audiotestharness/server/service/StreamObserverOutputStreamFactoryTests.java
new file mode 100644
index 0000000..4a117cd
--- /dev/null
+++ b/libraries/audio-test-harness/server/src/test/java/com/android/media/audiotestharness/server/service/StreamObserverOutputStreamFactoryTests.java
@@ -0,0 +1,77 @@
+/*
+ * 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.media.audiotestharness.server.service;
+
+import static org.junit.Assert.assertNotNull;
+
+import com.android.media.audiotestharness.proto.AudioTestHarnessService;
+
+import io.grpc.stub.ServerCallStreamObserver;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class StreamObserverOutputStreamFactoryTests {
+
+    @Test
+    public void createNewCaptureChunkStreamObserverOutputStream_returnsNonNullInstance()
+            throws Exception {
+        StreamObserverOutputStreamFactory factory = new StreamObserverOutputStreamFactory();
+        assertNotNull(
+                factory.createNewCaptureChunkStreamObserverOutputStream(
+                        new ServerCallStreamObserver<AudioTestHarnessService.CaptureChunk>() {
+                            @Override
+                            public boolean isReady() {
+                                return false;
+                            }
+
+                            @Override
+                            public void setOnReadyHandler(Runnable onReadyHandler) {}
+
+                            @Override
+                            public void disableAutoInboundFlowControl() {}
+
+                            @Override
+                            public void request(int count) {}
+
+                            @Override
+                            public void setMessageCompression(boolean enable) {}
+
+                            @Override
+                            public boolean isCancelled() {
+                                return false;
+                            }
+
+                            @Override
+                            public void setOnCancelHandler(Runnable onCancelHandler) {}
+
+                            @Override
+                            public void setCompression(String compression) {}
+
+                            @Override
+                            public void onNext(AudioTestHarnessService.CaptureChunk value) {}
+
+                            @Override
+                            public void onError(Throwable t) {}
+
+                            @Override
+                            public void onCompleted() {}
+                        }));
+    }
+}
diff --git a/libraries/audio-test-harness/server/src/test/java/com/android/media/audiotestharness/server/utility/PortUtilityTests.java b/libraries/audio-test-harness/server/src/test/java/com/android/media/audiotestharness/server/utility/PortUtilityTests.java
new file mode 100644
index 0000000..f548bb0
--- /dev/null
+++ b/libraries/audio-test-harness/server/src/test/java/com/android/media/audiotestharness/server/utility/PortUtilityTests.java
@@ -0,0 +1,43 @@
+/*
+ * 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.media.audiotestharness.server.utility;
+
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.net.ServerSocket;
+
+@RunWith(JUnit4.class)
+public class PortUtilityTests {
+
+    @Test
+    public void nextAvailablePort_returnsAvailablePortWithinRange_fuzz() throws Exception {
+        // Fuzz test the utility ten times.
+        for (int i = 0; i < 10; i++) {
+            int port = PortUtility.nextAvailablePort();
+
+            assertTrue(port >= PortUtility.START_PORT);
+            assertTrue(port < PortUtility.END_PORT);
+
+            // If no exception is thrown, the port was indeed open and can be used.
+            new ServerSocket(port).close();
+        }
+    }
+}
diff --git a/libraries/audio-test-harness/tradefed/Android.bp b/libraries/audio-test-harness/tradefed/Android.bp
new file mode 100644
index 0000000..ba343b6
--- /dev/null
+++ b/libraries/audio-test-harness/tradefed/Android.bp
@@ -0,0 +1,51 @@
+/*
+ * 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.
+ */
+
+// Contains the Trade Federation extensions for the Audio Test Harness system.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library_host {
+    name: "audiotestharness-testlib",
+
+    srcs: [
+        "src/main/java/com/android/media/audiotestharness/tradefed/*.java",
+    ],
+
+    libs: [
+        "tradefed",
+    ],
+}
+
+java_test_host {
+    name: "audiotestharness-testlib-tests",
+    test_suites: ["general-tests"],
+
+    srcs: [
+        "src/main/java/com/android/media/audiotestharness/tradefed/*.java",
+    ],
+
+    static_libs: [
+        "audiotestharness-testlib",
+        "tradefed",
+    ],
+
+    test_options: {
+        unit_test: true,
+    },
+}
diff --git a/libraries/audio-test-harness/tradefed/src/main/java/com/android/media/audiotestharness/tradefed/AudioTestHarnessHermeticServerManagingMetricCollector.java b/libraries/audio-test-harness/tradefed/src/main/java/com/android/media/audiotestharness/tradefed/AudioTestHarnessHermeticServerManagingMetricCollector.java
new file mode 100644
index 0000000..20a4577
--- /dev/null
+++ b/libraries/audio-test-harness/tradefed/src/main/java/com/android/media/audiotestharness/tradefed/AudioTestHarnessHermeticServerManagingMetricCollector.java
@@ -0,0 +1,42 @@
+/*
+ * 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.media.audiotestharness.tradefed;
+
+import com.android.tradefed.device.metric.BaseDeviceMetricCollector;
+import com.android.tradefed.device.metric.DeviceMetricData;
+import com.android.tradefed.metrics.proto.MetricMeasurement;
+
+import java.util.Map;
+
+/**
+ * {@link BaseDeviceMetricCollector} that manages the spin-up and tear-down of hermetic instances of
+ * the Audio Test Harness Server.
+ */
+public class AudioTestHarnessHermeticServerManagingMetricCollector
+        extends BaseDeviceMetricCollector {
+
+    @Override
+    public void onTestRunStart(DeviceMetricData runData) {
+        super.onTestRunStart(runData);
+    }
+
+    @Override
+    public void onTestRunEnd(
+            DeviceMetricData runData, Map<String, MetricMeasurement.Metric> currentRunMetrics) {
+        super.onTestRunEnd(runData, currentRunMetrics);
+    }
+}
diff --git a/libraries/audio-test-harness/tradefed/src/test/java/com/android/media/audiotestharness/tradefed/AudioTestHarnessHermeticServerManagingMetricCollectorTests.java b/libraries/audio-test-harness/tradefed/src/test/java/com/android/media/audiotestharness/tradefed/AudioTestHarnessHermeticServerManagingMetricCollectorTests.java
new file mode 100644
index 0000000..59656f4
--- /dev/null
+++ b/libraries/audio-test-harness/tradefed/src/test/java/com/android/media/audiotestharness/tradefed/AudioTestHarnessHermeticServerManagingMetricCollectorTests.java
@@ -0,0 +1,31 @@
+/*
+ * 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.media.audiotestharness.tradefed;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class AudioTestHarnessHermeticServerManagingMetricCollectorTests {
+
+    @Test
+    public void constructor_successfullyCreatesNewMetricCollector() throws Exception {
+        new AudioTestHarnessHermeticServerManagingMetricCollector();
+        // If no exceptions are thrown, then test passes.
+    }
+}
diff --git a/libraries/automotive-helpers/OWNERS b/libraries/automotive-helpers/OWNERS
new file mode 100644
index 0000000..34ed130
--- /dev/null
+++ b/libraries/automotive-helpers/OWNERS
@@ -0,0 +1,4 @@
+smara@google.com
+schinchalkar@google.com
+aceyansf@google.com
+tongfei@google.com
\ No newline at end of file
diff --git a/libraries/automotive-helpers/app-grid-helper/Android.bp b/libraries/automotive-helpers/app-grid-helper/Android.bp
new file mode 100644
index 0000000..ef0be56
--- /dev/null
+++ b/libraries/automotive-helpers/app-grid-helper/Android.bp
@@ -0,0 +1,34 @@
+//
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+    name: "automotive-app-grid-helper",
+    libs: [
+        "ub-uiautomator",
+        "app-helpers-auto-interfaces",
+    ],
+    static_libs: [
+        "androidx.test.runner",
+        "android-support-annotations",
+        "automotive-standard-app-helper",
+    ],
+    srcs: ["src/**/*.java"],
+    sdk_version: "test_current",
+}
diff --git a/libraries/automotive-helpers/app-grid-helper/src/android/platform/helpers/AppGridHelperImpl.java b/libraries/automotive-helpers/app-grid-helper/src/android/platform/helpers/AppGridHelperImpl.java
new file mode 100644
index 0000000..5e3f555
--- /dev/null
+++ b/libraries/automotive-helpers/app-grid-helper/src/android/platform/helpers/AppGridHelperImpl.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2021 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.helpers;
+
+import android.os.SystemClock;
+import android.app.Instrumentation;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiObject2;
+
+public class AppGridHelperImpl extends AbstractAutoStandardAppHelper implements IAutoAppGridHelper {
+
+    private static final String PAGE_UP_BUTTON_ID = "page_up";
+    private static final String PAGE_DOWN_BUTTON_ID = "page_down";
+
+    private static final int UI_RESPONSE_WAIT_MS = 5000;
+
+    private final BySelector PAGE_UP_BUTTON =
+            By.res(getApplicationConfig(AutoConfigConstants.APP_GRID_PACKAGE), PAGE_UP_BUTTON_ID);
+    private final BySelector PAGE_DOWN_BUTTON =
+            By.res(getApplicationConfig(AutoConfigConstants.APP_GRID_PACKAGE), PAGE_DOWN_BUTTON_ID);
+
+    public AppGridHelperImpl(Instrumentation instr) {
+        super(instr);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getPackage() {
+        return getApplicationConfig(AutoConfigConstants.APP_GRID_PACKAGE);
+    }
+
+    /**
+     * Setup expectation: None.
+     *
+     * <p>Open app grid by pressing app grid facet ony if app grid is not open.
+     */
+    @Override
+    public void open() {
+        if (!isAppInForeground()) {
+            executeShellCommand(getApplicationConfig(AutoConfigConstants.OPEN_APP_GRID_COMMAND));
+            SystemClock.sleep(UI_RESPONSE_WAIT_MS);
+        }
+    }
+
+    /**
+     * Setup expectations: None
+     *
+     * <p>Check if app grid is in foreground.
+     */
+    @Override
+    public boolean isAppInForeground() {
+        return (findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.APP_GRID,
+                                AutoConfigConstants.APP_GRID_VIEW,
+                                AutoConfigConstants.APP_GRID_VIEW_ID))
+                != null);
+    }
+
+    /**
+     * Setup expectation: None.
+     *
+     * <p>Exit app grid by pressing home facet only if app grid is open.
+     */
+    @Override
+    public void exit() {
+        if (isAppInForeground()) {
+            mDevice.pressHome();
+        }
+    }
+
+    @Override
+    public void openApp(String appName) {
+        UiObject2 app = scrollAndFindUiObject(By.text(appName));
+        if (app != null) {
+            clickAndWaitForIdleScreen(app);
+        } else {
+            throw new IllegalStateException(String.format("App %s cannot be found", appName));
+        }
+    }
+
+    /** {@inherticDoc} */
+    @Override
+    public boolean isTop() {
+        if (isAppInForeground()) {
+            UiObject2 pageUp = mDevice.findObject(PAGE_UP_BUTTON);
+            if (pageUp != null) {
+                return !pageUp.isEnabled();
+            } else {
+                // Number of apps fits in one page, at top by default
+                return true;
+            }
+        } else {
+            throw new IllegalStateException("App grid is not open.");
+        }
+    }
+
+    /** {@inherticDoc} */
+    @Override
+    public boolean isBottom() {
+        if (isAppInForeground()) {
+            UiObject2 pageDown = mDevice.findObject(PAGE_DOWN_BUTTON);
+            if (pageDown != null) {
+                return !pageDown.isEnabled();
+            } else {
+                // Number of apps fits in one page, at bottom by default
+                return true;
+            }
+        } else {
+            throw new IllegalStateException("App grid is not open.");
+        }
+    }
+}
diff --git a/libraries/automotive-helpers/dial-app-helper/Android.bp b/libraries/automotive-helpers/dial-app-helper/Android.bp
new file mode 100644
index 0000000..c298786
--- /dev/null
+++ b/libraries/automotive-helpers/dial-app-helper/Android.bp
@@ -0,0 +1,34 @@
+//
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+    name: "automotive-dial-app-helper",
+    libs: [
+        "ub-uiautomator",
+        "app-helpers-auto-interfaces",
+    ],
+    static_libs: [
+        "androidx.test.runner",
+        "android-support-annotations",
+        "automotive-standard-app-helper",
+    ],
+    srcs: ["src/**/*.java"],
+    sdk_version: "test_current",
+}
diff --git a/libraries/automotive-helpers/dial-app-helper/src/android/platform/helpers/ContactDetailsHelperImpl.java b/libraries/automotive-helpers/dial-app-helper/src/android/platform/helpers/ContactDetailsHelperImpl.java
new file mode 100644
index 0000000..8e0f535
--- /dev/null
+++ b/libraries/automotive-helpers/dial-app-helper/src/android/platform/helpers/ContactDetailsHelperImpl.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2021 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.helpers;
+
+import android.app.Instrumentation;
+import android.platform.helpers.exceptions.UnknownUiException;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiObject2;
+
+import android.os.SystemClock;
+
+public class ContactDetailsHelperImpl extends AbstractAutoStandardAppHelper
+        implements IAutoDialContactDetailsHelper {
+    private static final String LOG_TAG = DialHelperImpl.class.getSimpleName();
+
+    public ContactDetailsHelperImpl(Instrumentation instr) {
+        super(instr);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getPackage() {
+        return getApplicationConfig(AutoConfigConstants.DIAL_PACKAGE);
+    }
+
+    /** {@inheritDoc} */
+    public void open() {
+        pressHome();
+        waitForIdle();
+        String phone_launch_command =
+                "am start -n " + getApplicationConfig(AutoConfigConstants.PHONE_ACTIVITY);
+        executeShellCommand(phone_launch_command);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getLauncherName() {
+        return "Phone";
+    }
+
+    /** {@inheritDoc} */
+    public void addRemoveFavoriteContact() {
+        UiObject2 favoriteButton =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.PHONE,
+                                AutoConfigConstants.CONTACTS_VIEW,
+                                AutoConfigConstants.ADD_CONTACT_TO_FAVORITE));
+        if (favoriteButton != null) {
+            clickAndWaitForIdleScreen(favoriteButton);
+        } else {
+            throw new UnknownUiException("Unable to find add favorite button");
+        }
+    }
+
+    /** {@inheritDoc} */
+    public void makeCallFromDetailsPageByType(ContactType contactType) {
+        UiObject2 number = null;
+        switch (contactType) {
+            case HOME:
+                number =
+                        findUiObject(
+                                getResourceFromConfig(
+                                        AutoConfigConstants.PHONE,
+                                        AutoConfigConstants.CONTACTS_VIEW,
+                                        AutoConfigConstants.CONTACT_TYPE_HOME));
+                break;
+            case WORK:
+                number =
+                        findUiObject(
+                                getResourceFromConfig(
+                                        AutoConfigConstants.PHONE,
+                                        AutoConfigConstants.CONTACTS_VIEW,
+                                        AutoConfigConstants.CONTACT_TYPE_WORK));
+                break;
+            case MOBILE:
+                number =
+                        findUiObject(
+                                getResourceFromConfig(
+                                        AutoConfigConstants.PHONE,
+                                        AutoConfigConstants.CONTACTS_VIEW,
+                                        AutoConfigConstants.CONTACT_TYPE_MOBILE));
+                break;
+            default:
+                number = findUiObject(By.text("undefined"));
+        }
+        if (number != null) {
+            clickAndWaitForIdleScreen(number.getParent());
+        } else {
+            throw new UnknownUiException("Unable to find number in contact details");
+        }
+    }
+
+    /** {@inheritDoc} */
+    public void closeDetailsPage() {
+        // count is used to avoid infinite loop in case someone invokes
+        // after exiting settings application
+        int count = 5;
+        while (count > 0
+                && findUiObject(
+                                By.clickable(true)
+                                        .hasDescendant(
+                                                getResourceFromConfig(
+                                                        AutoConfigConstants.PHONE,
+                                                        AutoConfigConstants.DIAL_PAD_VIEW,
+                                                        AutoConfigConstants.DIAL_PAD_MENU)))
+                        == null) {
+            pressBack();
+            SystemClock.sleep(3000); // to avoid stale object error
+            count--;
+        }
+    }
+}
diff --git a/libraries/automotive-helpers/dial-app-helper/src/android/platform/helpers/DialHelperImpl.java b/libraries/automotive-helpers/dial-app-helper/src/android/platform/helpers/DialHelperImpl.java
new file mode 100644
index 0000000..02eda5d
--- /dev/null
+++ b/libraries/automotive-helpers/dial-app-helper/src/android/platform/helpers/DialHelperImpl.java
@@ -0,0 +1,595 @@
+/*
+ * Copyright (C) 2021 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.helpers;
+
+import android.app.Instrumentation;
+import android.content.Context;
+import android.platform.helpers.exceptions.UnknownUiException;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothManager;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiObject2;
+
+import java.util.regex.Pattern;
+import android.os.SystemClock;
+
+public class DialHelperImpl extends AbstractAutoStandardAppHelper implements IAutoDialHelper {
+    private static final String LOG_TAG = DialHelperImpl.class.getSimpleName();
+
+    private BluetoothManager mBluetoothManager;
+    private BluetoothAdapter mBluetoothAdapter;
+
+    public DialHelperImpl(Instrumentation instr) {
+        super(instr);
+        mBluetoothManager =
+                (BluetoothManager)
+                        mInstrumentation.getContext().getSystemService(Context.BLUETOOTH_SERVICE);
+        mBluetoothAdapter = mBluetoothManager.getAdapter();
+    }
+
+    /** {@inheritDoc} */
+    public void open() {
+        pressHome();
+        waitForIdle();
+        String phone_launch_command =
+                "am start -n " + getApplicationConfig(AutoConfigConstants.PHONE_ACTIVITY);
+        executeShellCommand(phone_launch_command);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getPackage() {
+        return getApplicationConfig(AutoConfigConstants.DIAL_PACKAGE);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getLauncherName() {
+        return "Phone";
+    }
+
+    /** {@inheritDoc} */
+    public void makeCall() {
+        BySelector dialButtonSelector =
+                getResourceFromConfig(
+                        AutoConfigConstants.PHONE,
+                        AutoConfigConstants.DIAL_PAD_VIEW,
+                        AutoConfigConstants.MAKE_CALL);
+        UiObject2 dialButton = findUiObject(dialButtonSelector);
+        if (dialButton != null) {
+            clickAndWaitForGone(dialButton, dialButtonSelector);
+        } else {
+            throw new UnknownUiException("Unable to find call button.");
+        }
+        waitForWindowUpdate(getApplicationConfig(AutoConfigConstants.DIAL_PACKAGE));
+        SystemClock.sleep(3000); // Wait for the call to go through
+    }
+
+    /** {@inheritDoc} */
+    public void endCall() {
+        BySelector endButtonSelector =
+                getResourceFromConfig(
+                        AutoConfigConstants.PHONE,
+                        AutoConfigConstants.IN_CALL_VIEW,
+                        AutoConfigConstants.END_CALL);
+        UiObject2 endButton = findUiObject(endButtonSelector);
+        if (endButton != null) {
+            clickAndWaitForGone(endButton, endButtonSelector);
+        } else {
+            throw new UnknownUiException("Unable to find end call button.");
+        }
+        waitForWindowUpdate(getApplicationConfig(AutoConfigConstants.DIAL_PACKAGE));
+    }
+
+    /** {@inheritDoc} */
+    public void dialANumber(String phoneNumber) {
+        enterNumber(phoneNumber);
+        waitForIdle();
+    }
+
+    /** {@inheritDoc} */
+    public void openCallHistory() {
+        BySelector callHistorySelector =
+                By.clickable(true)
+                        .hasDescendant(
+                                getResourceFromConfig(
+                                        AutoConfigConstants.PHONE,
+                                        AutoConfigConstants.CALL_HISTORY_VIEW,
+                                        AutoConfigConstants.CALL_HISTORY_MENU));
+        UiObject2 historyMenuButton = findUiObject(callHistorySelector);
+        if (historyMenuButton != null) {
+            clickAndWaitForIdleScreen(historyMenuButton);
+        } else {
+            throw new UnknownUiException("Unable to find call history menu.");
+        }
+    }
+
+    /** {@inheritDoc} */
+    public void callContact(String contactName) {
+        openContacts();
+        dialFromList(contactName);
+        SystemClock.sleep(3000); // Wait for the call to go through
+    }
+
+    /** {@inheritDoc} */
+    public String getRecentCallHistory() {
+        UiObject2 recentCallHistory = getCallHistory();
+        if (recentCallHistory != null) {
+            return recentCallHistory.getText();
+        } else {
+            throw new UnknownUiException("Unable to find history");
+        }
+    }
+
+    /** {@inheritDoc} */
+    public void callMostRecentHistory() {
+        UiObject2 recentCallHistory = getCallHistory();
+        clickAndWaitForIdleScreen(recentCallHistory);
+    }
+
+    /** {@inheritDoc} */
+    public void deleteDialedNumber() {
+        String phoneNumber = getDialInNumber();
+        UiObject2 deleteButton =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.PHONE,
+                                AutoConfigConstants.DIAL_PAD_VIEW,
+                                AutoConfigConstants.DELETE_NUMBER));
+        for (int index = 0; index < phoneNumber.length(); ++index) {
+            if (deleteButton != null) {
+                clickAndWaitForIdleScreen(deleteButton);
+            } else {
+                throw new UnknownUiException("Unable to find delete button");
+            }
+        }
+    }
+
+    /** {@inheritDoc} */
+    public String getDialInNumber() {
+        UiObject2 dialInNumber =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.PHONE,
+                                AutoConfigConstants.DIAL_PAD_VIEW,
+                                AutoConfigConstants.DIALED_NUMBER));
+        String phoneNumber = dialInNumber.getText();
+        return phoneNumber;
+    }
+
+    /** {@inheritDoc} */
+    public String getDialedNumber() {
+        UiObject2 dialedNumber =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.PHONE,
+                                AutoConfigConstants.IN_CALL_VIEW,
+                                AutoConfigConstants.DIALED_CONTACT_TITLE));
+        String phoneNumber = dialedNumber.getText();
+        return phoneNumber;
+    }
+
+    /** {@inheritDoc} */
+    public String getDialedContactName() {
+        UiObject2 dialedContactName =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.PHONE,
+                                AutoConfigConstants.IN_CALL_VIEW,
+                                AutoConfigConstants.DIALED_CONTACT_TITLE));
+        String callerName = dialedContactName.getText();
+        return callerName;
+    }
+
+    /** {@inheritDoc} */
+    public void inCallDialPad(String phoneNumber) {
+        UiObject2 dialPad =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.PHONE,
+                                AutoConfigConstants.IN_CALL_VIEW,
+                                AutoConfigConstants.SWITCH_TO_DIAL_PAD));
+        if (dialPad != null) {
+            clickAndWaitForIdleScreen(dialPad);
+        } else {
+            throw new UnknownUiException("Unable to find in-call dial pad");
+        }
+        enterNumber(phoneNumber);
+        waitForIdle();
+    }
+
+    /** {@inheritDoc} */
+    public void muteCall() {
+        UiObject2 muteButton =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.PHONE,
+                                AutoConfigConstants.IN_CALL_VIEW,
+                                AutoConfigConstants.MUTE_CALL));
+        if (muteButton != null) {
+            clickAndWaitForIdleScreen(muteButton);
+        } else {
+            throw new UnknownUiException("Unable to find mute call button.");
+        }
+    }
+
+    /** {@inheritDoc} */
+    public void unmuteCall() {
+        UiObject2 muteButton =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.PHONE,
+                                AutoConfigConstants.IN_CALL_VIEW,
+                                AutoConfigConstants.MUTE_CALL));
+        if (muteButton != null) {
+            clickAndWaitForIdleScreen(muteButton);
+        } else {
+            throw new UnknownUiException("Unable to find unmute call button.");
+        }
+    }
+
+    /** {@inheritDoc} */
+    public void dialFromList(String contact) {
+        UiObject2 contactToCall = findUiObject(By.text(contact));
+        if (contactToCall != null) {
+            clickAndWaitForIdleScreen(contactToCall);
+        } else {
+            scrollThroughCallList(contact);
+        }
+    }
+
+    /** {@inheritDoc} */
+    public void changeAudioSource(AudioSource source) {
+        UiObject2 voiceChannelButton =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.PHONE,
+                                AutoConfigConstants.IN_CALL_VIEW,
+                                AutoConfigConstants.CHANGE_VOICE_CHANNEL));
+        clickAndWaitForIdleScreen(voiceChannelButton);
+        UiObject2 channelButton;
+        if (source == AudioSource.PHONE) {
+            channelButton =
+                    findUiObject(
+                            getResourceFromConfig(
+                                    AutoConfigConstants.PHONE,
+                                    AutoConfigConstants.IN_CALL_VIEW,
+                                    AutoConfigConstants.VOICE_CHANNEL_PHONE));
+        } else {
+            channelButton =
+                    findUiObject(
+                            getResourceFromConfig(
+                                    AutoConfigConstants.PHONE,
+                                    AutoConfigConstants.IN_CALL_VIEW,
+                                    AutoConfigConstants.VOICE_CHANNEL_CAR));
+        }
+        clickAndWaitForIdleScreen(channelButton);
+        SystemClock.sleep(3000);
+    }
+
+    /** {@inheritDoc} */
+    public String getContactName() {
+        UiObject2 contactName =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.PHONE,
+                                AutoConfigConstants.IN_CALL_VIEW,
+                                AutoConfigConstants.DIALED_CONTACT_TITLE));
+        if (contactName != null) {
+            return contactName.getText();
+        } else {
+            throw new UnknownUiException("Unable to find contact name.");
+        }
+    }
+
+    /** {@inheritDoc} */
+    public String getContactType() {
+        // Contact number displayed on screen contains type
+        // e.g. Mobile xxx-xxx-xxxx , Work xxx-xxx-xxxx
+        UiObject2 contactDetail =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.PHONE,
+                                AutoConfigConstants.IN_CALL_VIEW,
+                                AutoConfigConstants.DIALED_CONTACT_NUMBER));
+        if (contactDetail != null) {
+            return contactDetail.getText();
+        } else {
+            throw new UnknownUiException("Unable to find contact details.");
+        }
+    }
+
+    /** {@inheritDoc} */
+    public void searchContactsByName(String contact) {
+        openSearchContact();
+        UiObject2 searchBox =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.PHONE,
+                                AutoConfigConstants.CONTACTS_VIEW,
+                                AutoConfigConstants.CONTACT_SEARCH_BAR));
+        if (searchBox != null) {
+            searchBox.setText(contact);
+        } else {
+            throw new UnknownUiException("Unable to find the search box.");
+        }
+    }
+
+    /** {@inheritDoc} */
+    public void searchContactsByNumber(String number) {
+        openSearchContact();
+        UiObject2 searchBox =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.PHONE,
+                                AutoConfigConstants.CONTACTS_VIEW,
+                                AutoConfigConstants.CONTACT_SEARCH_BAR));
+        if (searchBox != null) {
+            searchBox.setText(number);
+        } else {
+            throw new UnknownUiException("Unable to find the search box.");
+        }
+    }
+
+    /** {@inheritDoc} */
+    public String getFirstSearchResult() {
+        UiObject2 searchResult =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.PHONE,
+                                AutoConfigConstants.CONTACTS_VIEW,
+                                AutoConfigConstants.SEARCH_RESULT));
+        String result;
+        if (searchResult != null) {
+            result = searchResult.getText();
+        } else {
+            throw new UnknownUiException("Unable to find the search result");
+        }
+        pressBack();
+        return result;
+    }
+
+    /** {@inheritDoc} */
+    public void sortContactListBy(OrderType orderType) {
+        openContacts();
+        openSettings();
+        UiObject2 contactOrderButton =
+                findUiObject(
+                        By.clickable(true)
+                                .hasDescendant(
+                                        getResourceFromConfig(
+                                                AutoConfigConstants.PHONE,
+                                                AutoConfigConstants.CONTACTS_VIEW,
+                                                AutoConfigConstants.CONTACT_ORDER)));
+        clickAndWaitForIdleScreen(contactOrderButton);
+        UiObject2 orderButton;
+        if (orderType == OrderType.FIRST_NAME) {
+            orderButton =
+                    findUiObject(
+                            getResourceFromConfig(
+                                    AutoConfigConstants.PHONE,
+                                    AutoConfigConstants.CONTACTS_VIEW,
+                                    AutoConfigConstants.SORT_BY_FIRST_NAME));
+        } else {
+            orderButton =
+                    findUiObject(
+                            getResourceFromConfig(
+                                    AutoConfigConstants.PHONE,
+                                    AutoConfigConstants.CONTACTS_VIEW,
+                                    AutoConfigConstants.SORT_BY_LAST_NAME));
+        }
+        if (orderButton != null) {
+            clickAndWaitForIdleScreen(orderButton);
+        } else {
+            throw new UnknownUiException("Unable to find dialer settings button");
+        }
+        UiObject2 contactsMenu =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.PHONE,
+                                AutoConfigConstants.CONTACTS_VIEW,
+                                AutoConfigConstants.CONTACTS_MENU));
+        while (contactsMenu == null) {
+            pressBack();
+            contactsMenu =
+                    findUiObject(
+                            getResourceFromConfig(
+                                    AutoConfigConstants.PHONE,
+                                    AutoConfigConstants.CONTACTS_VIEW,
+                                    AutoConfigConstants.CONTACTS_MENU));
+        }
+    }
+
+    /** {@inheritDoc} */
+    public String getFirstContactFromContactList() {
+        openContacts();
+        scrollToTop();
+        UiObject2 firstContact =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.PHONE,
+                                AutoConfigConstants.CONTACTS_VIEW,
+                                AutoConfigConstants.CONTACT_NAME));
+        if (firstContact != null) {
+            return firstContact.getText();
+        } else {
+            throw new UnknownUiException("Unable to find first contact from contact list");
+        }
+    }
+
+    /** {@inheritDoc} */
+    public boolean isContactInFavorites(String contact) {
+        openFavorites();
+        UiObject2 obj = findUiObject(By.text(contact));
+        return obj != null;
+    }
+
+    /** {@inheritDoc} */
+    public void openDetailsPage(String contactName) {
+        openContacts();
+        UiObject2 contact =
+                scrollAndFindUiObject(
+                        By.text(Pattern.compile(contactName, Pattern.CASE_INSENSITIVE)));
+        if (contact != null) {
+            UiObject2 contactDetailButton;
+            UiObject2 showDetailsButton =
+                    findUiObject(
+                            getResourceFromConfig(
+                                    AutoConfigConstants.PHONE,
+                                    AutoConfigConstants.CONTACTS_VIEW,
+                                    AutoConfigConstants.CONTACT_DETAIL));
+            if (showDetailsButton == null) {
+                contactDetailButton = contact;
+            } else {
+                int lastIndex = contact.getParent().getChildren().size() - 1;
+                contactDetailButton = contact.getParent().getChildren().get(lastIndex);
+            }
+            clickAndWaitForIdleScreen(contactDetailButton);
+        } else {
+            throw new UnknownUiException(
+                    String.format("Unable to find contact name %s.", contactName));
+        }
+    }
+
+    /** This method is used to get the first history in the Recents tab. */
+    private UiObject2 getCallHistory() {
+        UiObject2 callHistory =
+                findUiObject(
+                                getResourceFromConfig(
+                                        AutoConfigConstants.PHONE,
+                                        AutoConfigConstants.CALL_HISTORY_VIEW,
+                                        AutoConfigConstants.CALL_HISTORY_INFO))
+                        .getParent();
+        UiObject2 recentCallHistory = callHistory.getChildren().get(2);
+        return recentCallHistory;
+    }
+
+    /** This method is used to open the contacts menu */
+    public void openContacts() {
+        BySelector contactMenuSelector =
+                By.clickable(true)
+                        .hasDescendant(
+                                getResourceFromConfig(
+                                        AutoConfigConstants.PHONE,
+                                        AutoConfigConstants.CONTACTS_VIEW,
+                                        AutoConfigConstants.CONTACTS_MENU));
+        UiObject2 contactMenuButton = findUiObject(contactMenuSelector);
+        if (contactMenuButton != null) {
+            clickAndWaitForIdleScreen(contactMenuButton);
+        } else {
+            throw new UnknownUiException("Unable to find Contacts menu.");
+        }
+    }
+
+    /** This method opens the contact search window. */
+    private void openSearchContact() {
+        UiObject2 searchContact =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.PHONE,
+                                AutoConfigConstants.CONTACTS_VIEW,
+                                AutoConfigConstants.SEARCH_CONTACT));
+        if (searchContact != null) {
+            clickAndWaitForIdleScreen(searchContact);
+        } else {
+            throw new UnknownUiException("Unable to find the search contact button.");
+        }
+    }
+
+    /** This method opens the settings for contact. */
+    private void openSettings() {
+        UiObject2 settingButton =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.PHONE,
+                                AutoConfigConstants.CONTACTS_VIEW,
+                                AutoConfigConstants.CONTACT_SETTINGS));
+        if (settingButton != null) {
+            clickAndWaitForIdleScreen(settingButton);
+        } else {
+            throw new UnknownUiException("Unable to find dialer settings button");
+        }
+    }
+
+    /** This method opens the Favorites tab. */
+    private void openFavorites() {
+        BySelector favoritesMenuSelector =
+                By.clickable(true)
+                        .hasDescendant(
+                                getResourceFromConfig(
+                                        AutoConfigConstants.PHONE,
+                                        AutoConfigConstants.FAVORITES_VIEW,
+                                        AutoConfigConstants.FAVORITES_MENU));
+        UiObject2 favoritesMenuButton = findUiObject(favoritesMenuSelector);
+        if (favoritesMenuButton != null) {
+            clickAndWaitForIdleScreen(favoritesMenuButton);
+        } else {
+            throw new UnknownUiException("Unable to find Favorites menu.");
+        }
+    }
+
+    public boolean isPhonePaired() {
+        return mBluetoothAdapter.getBondedDevices().size() != 0;
+    }
+
+    /**
+     * This method is used to scroll through the list (Favorites, Call History, Contact)
+     *
+     * <p>in search of the contact number or name and click if found
+     *
+     * @param contact contact number or name to be dialed
+     */
+    private void scrollThroughCallList(String contact) {
+        UiObject2 contactObject =
+                scrollAndFindUiObject(By.text(Pattern.compile(contact, Pattern.CASE_INSENSITIVE)));
+        if (contactObject != null) {
+            clickAndWaitForIdleScreen(contactObject);
+        }
+    }
+
+    /**
+     * This method is used to enter phonenumber from the on-screen numberpad
+     *
+     * @param phoneNumber number to be dialed
+     */
+    private void enterNumber(String phoneNumber) {
+        if (phoneNumber == null) {
+            throw new UnknownUiException("No phone number provided");
+        }
+        pressHome();
+        waitForIdle();
+        executeShellCommand(getApplicationConfig(AutoConfigConstants.OPEN_DIAL_PAD_COMMAND));
+        UiObject2 dial_pad =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.PHONE,
+                                AutoConfigConstants.DIAL_PAD_VIEW,
+                                AutoConfigConstants.DIAL_PAD_FRAGMENT));
+        if (dial_pad == null) {
+            throw new UnknownUiException("Unable to find dial pad");
+        }
+        char[] array = phoneNumber.toCharArray();
+        for (char ch : array) {
+            UiObject2 numberButton = findUiObject(By.text(Character.toString(ch)));
+            if (numberButton == null) {
+                throw new UnknownUiException("Unable to find number" + phoneNumber);
+            }
+            clickAndWaitForIdleScreen(numberButton);
+        }
+    }
+}
diff --git a/libraries/automotive-helpers/hardkeys-app-helper/Android.bp b/libraries/automotive-helpers/hardkeys-app-helper/Android.bp
new file mode 100644
index 0000000..2e44189
--- /dev/null
+++ b/libraries/automotive-helpers/hardkeys-app-helper/Android.bp
@@ -0,0 +1,34 @@
+//
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+    name: "automotive-vehiclehardkeys-app-helper",
+    libs: [
+        "ub-uiautomator",
+        "app-helpers-auto-interfaces",
+        "android.car",
+    ],
+    static_libs: [
+        "androidx.test.runner",
+        "automotive-standard-app-helper",
+    ],
+    srcs: ["src/**/*.java"],
+    sdk_version: "test_current",
+}
diff --git a/libraries/automotive-helpers/hardkeys-app-helper/src/android/platform/helpers/VehicleHardKeysHelperImpl.java b/libraries/automotive-helpers/hardkeys-app-helper/src/android/platform/helpers/VehicleHardKeysHelperImpl.java
new file mode 100644
index 0000000..380b330
--- /dev/null
+++ b/libraries/automotive-helpers/hardkeys-app-helper/src/android/platform/helpers/VehicleHardKeysHelperImpl.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2021 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.helpers;
+
+import static android.car.drivingstate.CarDrivingStateEvent.DRIVING_STATE_IDLING;
+import static android.car.drivingstate.CarDrivingStateEvent.DRIVING_STATE_MOVING;
+import static android.car.drivingstate.CarDrivingStateEvent.DRIVING_STATE_PARKED;
+
+import android.app.Instrumentation;
+import android.app.UiAutomation;
+import android.car.Car;
+import android.car.drivingstate.CarDrivingStateEvent;
+import android.car.drivingstate.CarDrivingStateManager;
+import android.content.Context;
+import androidx.test.InstrumentationRegistry;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class VehicleHardKeysHelperImpl extends AbstractAutoStandardAppHelper
+        implements IAutoVehicleHardKeysHelper {
+
+    private static final String ENABLE_DRIVING_MODE =
+            "cmd car_service " + "inject-vhal-event 0x11400400 8";
+    private static final String ENABLE_PARKING_MODE =
+            "cmd car_service " + "inject-vhal-event 0x11400400 4";
+    private static final String SET_SPEED =
+            "cmd car_service " + "inject-vhal-event 0x11600207 %s -t 2000";
+
+    private Car mCar;
+    private Context mContext;
+    private CarDrivingStateManager mCarDrivingStateManager;
+    private final UiAutomation mUiAutomation =
+            InstrumentationRegistry.getInstrumentation().getUiAutomation();
+
+    public VehicleHardKeysHelperImpl(Instrumentation instr) {
+        super(instr);
+        mContext = InstrumentationRegistry.getContext();
+        mUiAutomation.adoptShellPermissionIdentity("android.car.permission.CAR_DRIVING_STATE");
+        mCar = Car.createCar(mContext);
+        mCarDrivingStateManager =
+                (CarDrivingStateManager) mCar.getCarManager(Car.CAR_DRIVING_STATE_SERVICE);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getPackage() {
+        return "";
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void open() {
+        // Noting to open
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getLauncherName() {
+        throw new RuntimeException("Not Supported");
+    }
+
+    /** {@inheritDoc} */
+    public void pressRecieveCallKey() { // NOTYPO
+        executeShellCommand("input keyevent KEYCODE_CALL");
+    }
+
+    /** {@inheritDoc} */
+    public void pressEndCallKey() {
+        String cmd = executeShellCommand("input keyevent KEYCODE_ENDCALL");
+    }
+
+    /** {@inheritDoc} */
+    public void pressMediaNextTrackKey() {
+        String cmd = executeShellCommand("input keyevent KEYCODE_MEDIA_NEXT");
+    }
+
+    /** {@inheritDoc} */
+    public void pressMediaPreviousTrackKey() {
+        String cmd = executeShellCommand("input keyevent KEYCODE_MEDIA_PREVIOUS");
+    }
+
+    /** {@inheritDoc} */
+    public void tuneVolumeUpKey() {
+        String cmd = executeShellCommand("input keyevent KEYCODE_VOLUME_UP");
+    }
+
+    /** {@inheritDoc} */
+    public void tuneVolumeDownKey() {
+        String cmd = executeShellCommand("input keyevent KEYCODE_VOLUME_DOWN");
+    }
+
+    /** {@inheritDoc} */
+    public void pressBrightnessUpKey() {
+        String cmd = executeShellCommand("input keyevent KEYCODE_BRIGHTNESS_UP");
+    }
+
+    /** {@inheritDoc} */
+    public void pressBrightnessDownKey() {
+        String cmd = executeShellCommand("input keyevent KEYCODE_BRIGHTNESS_DOWN");
+    }
+
+    /** {@inheritDoc} */
+    public void pressAssistantKey() {
+        throw new RuntimeException("Not Supported");
+    }
+
+    /** {@inheritDoc} */
+    public void tuneMuteKey() {
+        String cmd = executeShellCommand("input keyevent KEYCODE_VOLUME_MUTE");
+    }
+
+    /** {@inheritDoc} */
+    public void pressScreenOffKey() {
+        throw new RuntimeException("Not Supported");
+    }
+
+    /** {@inheritDoc} */
+    public void tuneKnobKey() {
+        throw new RuntimeException("Not Supported");
+    }
+
+    /** {@inheritDoc} */
+    public void pressKnobButtonKey() {
+        throw new RuntimeException("Not Supported");
+    }
+
+    /** {@inheritDoc} */
+    public void tuneVolumeKnobKey() {
+        throw new RuntimeException("Not Supported");
+    }
+
+    /** {@inheritDoc} */
+    public void pressVolumeKnobButtonKey() {
+        throw new RuntimeException("Not Supported");
+    }
+
+    /** {@inheritDoc} */
+    public int getCurrentVolumeLevel(VolumeType type) {
+        String volumeType = "";
+        switch (type) {
+            case Media:
+                volumeType = "MUSIC";
+                break;
+            case Ring:
+                volumeType = "RING";
+                break;
+            case Notification:
+                volumeType = "NOTIFICATION";
+                break;
+            case Navigation:
+                volumeType = "NAVIGATION";
+                break;
+        }
+        String cmd = "dumpsys car_service";
+        String res = executeShellCommand(cmd);
+        Pattern p =
+                Pattern.compile(
+                        "(?s).+CarVolumeGroup.+?"
+                                + volumeType
+                                + ".+?Gain values.+?"
+                                + "([0-9]+)\\s+([0-9]+)\\s+([0-9]+)\\s+([0-9]+).+");
+        Matcher m = p.matcher(res);
+        if (m.matches()) {
+            /*
+                Four volumes can be obtained from the match:
+                1: min, 2: max, 3: default, 4: current
+            */
+            return Integer.parseInt(m.group(4));
+        } else {
+            throw new RuntimeException("Cannot find current volume");
+        }
+    }
+
+    /** {@inheritDoc} */
+    public DrivingState getDrivingState() {
+        CarDrivingStateEvent event = mCarDrivingStateManager.getCurrentCarDrivingState();
+        switch (event.eventValue) {
+            case DRIVING_STATE_IDLING:
+                return DrivingState.IDLING;
+            case DRIVING_STATE_MOVING:
+                return DrivingState.MOVING;
+            case DRIVING_STATE_PARKED:
+                return DrivingState.PARKED;
+        }
+        return DrivingState.UNKNOWN;
+    }
+
+    /** {@inheritDoc} */
+    public void setDrivingState(DrivingState state) {
+        if (state == DrivingState.UNKNOWN) {
+            throw new RuntimeException("State not supported");
+        } else if (state == DrivingState.PARKED) {
+            executeShellCommand(ENABLE_PARKING_MODE);
+        } else {
+            executeShellCommand(ENABLE_DRIVING_MODE);
+        }
+    }
+
+    /** {@inheritDoc} */
+    public void setSpeed(int speed) {
+        if (getDrivingState() == DrivingState.PARKED) {
+            throw new RuntimeException("Car is in parking mode.");
+        } else {
+            executeShellCommand(String.format(SET_SPEED, speed));
+        }
+    }
+}
diff --git a/libraries/automotive-helpers/home-helper/Android.bp b/libraries/automotive-helpers/home-helper/Android.bp
new file mode 100644
index 0000000..f01eb15
--- /dev/null
+++ b/libraries/automotive-helpers/home-helper/Android.bp
@@ -0,0 +1,34 @@
+//
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+    name: "automotive-home-helper",
+    libs: [
+        "ub-uiautomator",
+        "app-helpers-auto-interfaces",
+    ],
+    static_libs: [
+        "androidx.test.runner",
+        "android-support-annotations",
+        "automotive-standard-app-helper",
+    ],
+    srcs: ["src/**/*.java"],
+    sdk_version: "test_current",
+}
diff --git a/libraries/automotive-helpers/home-helper/src/android/platform/helpers/HomeHelperImpl.java b/libraries/automotive-helpers/home-helper/src/android/platform/helpers/HomeHelperImpl.java
new file mode 100644
index 0000000..5ed6713
--- /dev/null
+++ b/libraries/automotive-helpers/home-helper/src/android/platform/helpers/HomeHelperImpl.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2021 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.helpers;
+
+import android.app.Instrumentation;
+
+public class HomeHelperImpl extends AbstractAutoStandardAppHelper implements IAutoHomeHelper {
+
+    private static final int UI_RESPONSE_WAIT_WC = 5000;
+
+    public HomeHelperImpl(Instrumentation instr) {
+        super(instr);
+    }
+
+    public boolean hasMapWidget() {
+        return (findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.HOME,
+                                AutoConfigConstants.HOME_VIEW,
+                                AutoConfigConstants.MAP_CARD))
+                != null);
+    }
+
+    public boolean hasAssistantWidget() {
+        return (findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.HOME,
+                                AutoConfigConstants.HOME_VIEW,
+                                AutoConfigConstants.TOP_CARD))
+                != null);
+    }
+
+    public boolean hasMediaWidget() {
+        return (findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.HOME,
+                                AutoConfigConstants.HOME_VIEW,
+                                AutoConfigConstants.BOTTOM_CARD))
+                != null);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void open() {
+        pressHome();
+        waitForIdle();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getPackage() {
+        return getApplicationConfig(AutoConfigConstants.HOME_PACKAGE);
+    }
+}
diff --git a/libraries/automotive-helpers/lock-screen-helper/Android.bp b/libraries/automotive-helpers/lock-screen-helper/Android.bp
new file mode 100644
index 0000000..b894107
--- /dev/null
+++ b/libraries/automotive-helpers/lock-screen-helper/Android.bp
@@ -0,0 +1,35 @@
+//
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+    name: "automotive-lock-screen-helper",
+    libs: [
+        "ub-uiautomator",
+        "app-helpers-auto-interfaces",
+    ],
+    static_libs: [
+        "androidx.test.runner",
+        "android-support-annotations",
+        "automotive-settings-app-helper",
+        "automotive-standard-app-helper",
+    ],
+    srcs: ["src/**/*.java"],
+    sdk_version: "test_current",
+}
diff --git a/libraries/automotive-helpers/lock-screen-helper/src/android/platform/helpers/LockScreenHelperImpl.java b/libraries/automotive-helpers/lock-screen-helper/src/android/platform/helpers/LockScreenHelperImpl.java
new file mode 100644
index 0000000..881d1b3
--- /dev/null
+++ b/libraries/automotive-helpers/lock-screen-helper/src/android/platform/helpers/LockScreenHelperImpl.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2021 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.helpers;
+
+import android.app.Instrumentation;
+import android.os.SystemClock;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+
+public class LockScreenHelperImpl extends AbstractAutoStandardAppHelper
+        implements IAutoLockScreenHelper {
+    private static final int DEFAULT_WAIT_TIME = 5000;
+    private static final int THREE_SECOND_WAIT_TIME = 3000;
+
+    private static final String UNLOCK_BY = "input text %s";
+
+    private HelperAccessor<IAutoSecuritySettingsHelper> mSecuritySettingsHelper;
+    private HelperAccessor<IAutoSettingHelper> mSettingHelper;
+
+    public LockScreenHelperImpl(Instrumentation instr) {
+        super(instr);
+        mSecuritySettingsHelper = new HelperAccessor<>(IAutoSecuritySettingsHelper.class);
+        mSettingHelper = new HelperAccessor<>(IAutoSettingHelper.class);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getPackage() {
+        return getApplicationConfig(AutoConfigConstants.LOCK_SCREEN_PACKAGE);
+    }
+
+    @Override
+    public void lockScreenBy(LockType lockType, String credential) {
+        if (lockType == LockType.PIN) {
+            mSecuritySettingsHelper.get().setLockByPin(credential);
+        } else {
+            mSecuritySettingsHelper.get().setLockByPassword(credential);
+        }
+        pressPowerButton();
+    }
+
+    @Override
+    public void unlockScreenBy(LockType lockType, String credential) {
+        pressPowerButton();
+        if (lockType == LockType.PIN) {
+            unlockByPin(credential);
+        } else {
+            unlockByPassword(credential);
+        }
+    }
+
+    private void unlockByPassword(String password) {
+        executeShellCommand(String.format(UNLOCK_BY, password));
+        SystemClock.sleep(THREE_SECOND_WAIT_TIME);
+        pressEnter();
+        AutoUtility.exitSuw();
+        mSettingHelper.get().openSetting(AutoConfigConstants.SECURITY_SETTINGS);
+    }
+
+    private void unlockByPin(String pin) {
+        selectPinOnPinPad(pin);
+        UiObject2 enter_button =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.LOCK_SCREEN,
+                                AutoConfigConstants.LOCK_SCREEN_VIEW,
+                                AutoConfigConstants.ENTER_KEY));
+        clickAndWaitForIdleScreen(enter_button);
+        UiObject2 pinPad =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.LOCK_SCREEN,
+                                AutoConfigConstants.LOCK_SCREEN_VIEW,
+                                AutoConfigConstants.PIN_PAD));
+        SystemClock.sleep(THREE_SECOND_WAIT_TIME);
+    }
+
+    private void selectPinOnPinPad(String pin) {
+        int length = pin.length();
+        for (int i = 0; i < length; i++) {
+            char c = pin.charAt(i);
+            String resourceId = "key" + c;
+            BySelector number_selector =
+                    By.res(
+                            getApplicationConfig(AutoConfigConstants.LOCK_SCREEN_PACKAGE),
+                            resourceId);
+            UiObject2 number = mDevice.wait(Until.findObject(number_selector), DEFAULT_WAIT_TIME);
+            number.click();
+        }
+    }
+}
diff --git a/libraries/automotive-helpers/media-center-app-helper/Android.bp b/libraries/automotive-helpers/media-center-app-helper/Android.bp
new file mode 100644
index 0000000..14375ff
--- /dev/null
+++ b/libraries/automotive-helpers/media-center-app-helper/Android.bp
@@ -0,0 +1,35 @@
+//
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+    name: "automotive-media-center-app-helper",
+    libs: [
+        "ub-uiautomator",
+        "app-helpers-auto-interfaces",
+        "automotive-utility-helper",
+        "android.car",
+    ],
+    static_libs: [
+        "androidx.test.runner",
+        "automotive-standard-app-helper",
+    ],
+    srcs: ["src/**/*.java"],
+    sdk_version: "test_current",
+}
diff --git a/libraries/automotive-helpers/media-center-app-helper/src/android/platform/helpers/MediaCenterHelperImpl.java b/libraries/automotive-helpers/media-center-app-helper/src/android/platform/helpers/MediaCenterHelperImpl.java
new file mode 100644
index 0000000..9b5530d
--- /dev/null
+++ b/libraries/automotive-helpers/media-center-app-helper/src/android/platform/helpers/MediaCenterHelperImpl.java
@@ -0,0 +1,386 @@
+/*
+ * Copyright (C) 2021 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.helpers;
+
+import android.app.Instrumentation;
+import android.app.UiAutomation;
+import android.content.Context;
+import android.media.session.MediaController;
+import android.media.session.MediaSessionManager;
+import android.media.session.PlaybackState;
+import android.os.SystemClock;
+import android.platform.helpers.exceptions.UnknownUiException;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.UiObject;
+import android.support.test.uiautomator.UiObjectNotFoundException;
+import android.support.test.uiautomator.UiScrollable;
+import android.support.test.uiautomator.UiSelector;
+
+import java.util.List;
+
+public class MediaCenterHelperImpl extends AbstractAutoStandardAppHelper
+        implements IAutoMediaHelper {
+    // Wait Time
+    private static final int UI_RESPONSE_WAIT_MS = 5000;
+    private static final int SHORT_RESPONSE_WAIT_MS = 1000;
+
+    private static final String MEDIA_LAUNCH_COMMAND =
+            "am start -a android.car.intent.action.MEDIA_TEMPLATE -e "
+                    + "android.car.intent.extra.MEDIA_COMPONENT ";
+
+    private MediaSessionManager mMediaSessionManager;
+    private UiAutomation mUiAutomation;
+
+    public MediaCenterHelperImpl(Instrumentation instr) {
+        super(instr);
+        mUiAutomation = instr.getUiAutomation();
+        mUiAutomation.adoptShellPermissionIdentity("android.permission.MEDIA_CONTENT_CONTROL");
+        mMediaSessionManager =
+                (MediaSessionManager)
+                        instr.getContext().getSystemService(Context.MEDIA_SESSION_SERVICE);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getPackage() {
+        return getApplicationConfig(AutoConfigConstants.MEDIA_CENTER_PACKAGE);
+    }
+
+    /** {@inheritDoc} */
+    public void open() {
+        openMediaApp(getApplicationConfig(AutoConfigConstants.MEDIA_ACTIVITY));
+    }
+
+    private void openMediaApp(String packagename) {
+        pressHome();
+        waitForIdle();
+        executeShellCommand(MEDIA_LAUNCH_COMMAND + packagename);
+    }
+
+    /** {@inheritDoc} */
+    public void playMedia() {
+        if (!isPlaying()) {
+            UiObject2 playButton =
+                    findUiObject(
+                            getResourceFromConfig(
+                                    AutoConfigConstants.MEDIA_CENTER,
+                                    AutoConfigConstants.MEDIA_CENTER_SCREEN,
+                                    AutoConfigConstants.PLAY_PAUSE_BUTTON));
+            if (playButton == null) {
+                throw new UnknownUiException("Unable to find play/pause button");
+            }
+            clickAndWaitForIdleScreen(playButton);
+            SystemClock.sleep(UI_RESPONSE_WAIT_MS);
+        }
+    }
+
+    /** {@inheritDoc} */
+    public void playPauseMediaFromHomeScreen() {
+        UiObject2 playButton =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.MEDIA_CENTER,
+                                AutoConfigConstants.MEDIA_CENTER_ON_HOME_SCREEN,
+                                AutoConfigConstants.PLAY_PAUSE_BUTTON));
+        if (playButton == null) {
+            throw new UnknownUiException("Unable to find play button from home screen");
+        }
+        clickAndWaitForIdleScreen(playButton);
+        SystemClock.sleep(UI_RESPONSE_WAIT_MS);
+    }
+
+    /** {@inheritDoc} */
+    public void pauseMedia() {
+        if (isPlaying()) {
+            UiObject2 pauseButton =
+                    findUiObject(
+                            getResourceFromConfig(
+                                    AutoConfigConstants.MEDIA_CENTER,
+                                    AutoConfigConstants.MEDIA_CENTER_SCREEN,
+                                    AutoConfigConstants.PLAY_PAUSE_BUTTON));
+            if (pauseButton == null) {
+                throw new UnknownUiException("Unable to find pause button");
+            }
+            clickAndWaitForIdleScreen(pauseButton);
+            SystemClock.sleep(UI_RESPONSE_WAIT_MS);
+        }
+    }
+
+    /** {@inheritDoc} */
+    public void clickNextTrack() {
+        UiObject2 nextTrackButton =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.MEDIA_CENTER,
+                                AutoConfigConstants.MEDIA_CENTER_SCREEN,
+                                AutoConfigConstants.NEXT_BUTTON));
+        if (nextTrackButton == null) {
+            throw new UnknownUiException("Unable to find next track button");
+        }
+        clickAndWaitForIdleScreen(nextTrackButton);
+        SystemClock.sleep(UI_RESPONSE_WAIT_MS);
+    }
+
+    /** {@inheritDoc} */
+    public void clickNextTrackFromHomeScreen() {
+        UiObject2 nextTrackButton =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.MEDIA_CENTER,
+                                AutoConfigConstants.MEDIA_CENTER_ON_HOME_SCREEN,
+                                AutoConfigConstants.NEXT_BUTTON));
+        if (nextTrackButton == null) {
+            throw new UnknownUiException("Unable to find next track button from home screen");
+        }
+        clickAndWaitForIdleScreen(nextTrackButton);
+        SystemClock.sleep(UI_RESPONSE_WAIT_MS);
+    }
+
+    /** {@inheritDoc} */
+    public void clickPreviousTrack() {
+        UiObject2 previousTrackButton =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.MEDIA_CENTER,
+                                AutoConfigConstants.MEDIA_CENTER_SCREEN,
+                                AutoConfigConstants.PREVIOUS_BUTTON));
+        if (previousTrackButton == null) {
+            throw new UnknownUiException("Unable to find previous track button");
+        }
+        clickAndWaitForIdleScreen(previousTrackButton);
+        SystemClock.sleep(UI_RESPONSE_WAIT_MS);
+    }
+
+    /** {@inheritDoc} */
+    public void clickPreviousTrackFromHomeScreen() {
+        UiObject2 previousTrackButton =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.MEDIA_CENTER,
+                                AutoConfigConstants.MEDIA_CENTER_ON_HOME_SCREEN,
+                                AutoConfigConstants.PREVIOUS_BUTTON));
+        if (previousTrackButton == null) {
+            throw new UnknownUiException("Unable to find previous track button");
+        }
+        clickAndWaitForIdleScreen(previousTrackButton);
+        SystemClock.sleep(UI_RESPONSE_WAIT_MS);
+    }
+
+    /** {@inheritDoc} */
+    public void clickShuffleAll() {
+        UiObject2 shufflePlaylistButton =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.MEDIA_CENTER,
+                                AutoConfigConstants.MEDIA_CENTER_SCREEN,
+                                AutoConfigConstants.SHUFFLE_BUTTON));
+        if (shufflePlaylistButton == null) {
+            throw new UnknownUiException("Unable to find shuffle playlist button");
+        }
+        clickAndWaitForIdleScreen(shufflePlaylistButton);
+    }
+
+    /** Click the nth instance among the visible menu items */
+    public void clickMenuItem(int instance) {
+        if (!mDevice.hasObject(By.scrollable(true))) {
+            // Menu is not open
+            return;
+        }
+        UiScrollable menuList = new UiScrollable(new UiSelector().scrollable(true));
+        menuList.setAsVerticalList();
+        try {
+            UiObject menuListItem =
+                    menuList.getChildByInstance(new UiSelector().clickable(true), instance);
+            menuListItem.clickAndWaitForNewWindow(UI_RESPONSE_WAIT_MS);
+            waitForIdle();
+        } catch (UiObjectNotFoundException e) {
+            throw new UnknownUiException("Unable to find menu item", e);
+        }
+    }
+
+    /** {@inheritDoc} */
+    public void openMenuWith(String... menuOptions) {
+        for (String menu : menuOptions) {
+            UiObject2 menuButton = findUiObject(By.text(menu));
+            if (menuButton != null) {
+                clickAndWaitForIdleScreen(menuButton);
+                waitForGone(By.text(menu));
+            } else {
+                try {
+                    UiObject menuItem_object = selectByName(menu);
+                    menuItem_object.clickAndWaitForNewWindow();
+                } catch (UiObjectNotFoundException exception) {
+                    throw new UnknownUiException("Unable to find the menu item");
+                }
+            }
+            SystemClock.sleep(SHORT_RESPONSE_WAIT_MS);
+        }
+    }
+
+    /** {@inheritDoc} */
+    public void openNowPlayingWith(String trackName) {
+        UiObject2 nowPlayingButton =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.MEDIA_CENTER,
+                                AutoConfigConstants.MEDIA_CENTER_SCREEN,
+                                AutoConfigConstants.PLAY_QUEUE_BUTTON));
+        if (nowPlayingButton == null) {
+            throw new UnknownUiException("Unable to find Now playing button");
+        }
+        clickAndWaitForIdleScreen(nowPlayingButton);
+        waitForWindowUpdate(getApplicationConfig(AutoConfigConstants.MEDIA_CENTER_PACKAGE));
+        UiObject2 playTrackName = findUiObject(By.text(trackName));
+        if (playTrackName != null) {
+            clickAndWaitForIdleScreen(playTrackName);
+        } else {
+            try {
+                UiObject menuItem_object = selectByName(trackName);
+                menuItem_object.clickAndWaitForNewWindow();
+            } catch (UiObjectNotFoundException exception) {
+                throw new UnknownUiException("Unable to find the trackname from Now playing list");
+            }
+        }
+    }
+
+    /** {@inheritDoc} */
+    public String getMediaTrackName() {
+        String track;
+        UiObject2 mediaControl =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.MEDIA_CENTER,
+                                AutoConfigConstants.MEDIA_CENTER_SCREEN,
+                                AutoConfigConstants.MINIMIZED_MEDIA_CONTROLS));
+        if (mediaControl != null) {
+            track = getMediaTrackNameFromMinimizedControl();
+        } else {
+            track = getMediaTrackNameFromPlayback();
+        }
+        return track;
+    }
+
+    /** {@inheritDoc} */
+    public String getMediaTrackNameFromHomeScreen() {
+        String trackName;
+        UiObject2 trackNameText =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.MEDIA_CENTER,
+                                AutoConfigConstants.MEDIA_CENTER_ON_HOME_SCREEN,
+                                AutoConfigConstants.TRACK_NAME));
+        if (trackNameText == null) {
+            throw new UnknownUiException("Unable to find track name from Home Screen");
+        }
+        trackName = trackNameText.getText();
+        return trackName;
+    }
+
+    private String getMediaTrackNameFromMinimizedControl() {
+        String trackName;
+        UiObject2 trackNameText =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.MEDIA_CENTER,
+                                AutoConfigConstants.MEDIA_CENTER_SCREEN,
+                                AutoConfigConstants.TRACK_NAME_MINIMIZED_CONTROL));
+        if (trackNameText == null) {
+            throw new UnknownUiException("Unable to find track name from minimized control");
+        }
+        trackName = trackNameText.getText();
+        return trackName;
+    }
+
+    private String getMediaTrackNameFromPlayback() {
+        String trackName;
+        waitForIdle();
+        UiObject2 trackNameText =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.MEDIA_CENTER,
+                                AutoConfigConstants.MEDIA_CENTER_SCREEN,
+                                AutoConfigConstants.TRACK_NAME));
+        if (trackNameText == null) {
+            throw new UnknownUiException("Unable to find track name from now playing");
+        }
+        trackName = trackNameText.getText();
+        return trackName;
+    }
+
+    /** {@inheritDoc} */
+    public void goBackToMediaHomePage() {
+        minimizeNowPlaying();
+        UiObject2 back_btn =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.MEDIA_CENTER,
+                                AutoConfigConstants.MEDIA_CENTER_SCREEN,
+                                AutoConfigConstants.BACK_BUTTON));
+        while (back_btn != null) {
+            clickAndWaitForIdleScreen(back_btn);
+            back_btn =
+                    findUiObject(
+                            getResourceFromConfig(
+                                    AutoConfigConstants.MEDIA_CENTER,
+                                    AutoConfigConstants.MEDIA_CENTER_SCREEN,
+                                    AutoConfigConstants.BACK_BUTTON));
+        }
+    }
+
+    /** Minimize the Now Playing window. */
+    private void minimizeNowPlaying() {
+        UiObject2 trackNameText =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.MEDIA_CENTER,
+                                AutoConfigConstants.MEDIA_CENTER_SCREEN,
+                                AutoConfigConstants.TRACK_NAME));
+        if (trackNameText != null) {
+            trackNameText.swipe(Direction.DOWN, 1.0f, 500);
+        }
+    }
+
+    /**
+     * Scrolls through the list in search of the provided menu
+     *
+     * @param menu : menu to search
+     * @return UiObject found for the menu searched
+     */
+    private UiObject selectByName(String menu) throws UiObjectNotFoundException {
+        UiObject menuListItem = null;
+        UiScrollable menuList = new UiScrollable(new UiSelector().scrollable(true));
+        menuList.setAsVerticalList();
+        menuListItem =
+                menuList.getChildByText(
+                        new UiSelector().className(android.widget.TextView.class.getName()), menu);
+        mDevice.waitForIdle();
+        return menuListItem;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isPlaying() {
+        List<MediaController> controllers = mMediaSessionManager.getActiveSessions(null);
+        if (controllers.size() == 0) {
+            throw new RuntimeException("Unable to find Media Controller");
+        }
+        PlaybackState state = controllers.get(0).getPlaybackState();
+        return state.getState() == PlaybackState.STATE_PLAYING;
+    }
+}
diff --git a/libraries/automotive-helpers/notifications-app-helper/Android.bp b/libraries/automotive-helpers/notifications-app-helper/Android.bp
new file mode 100644
index 0000000..bfe97f8
--- /dev/null
+++ b/libraries/automotive-helpers/notifications-app-helper/Android.bp
@@ -0,0 +1,33 @@
+//
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+    name: "automotive-notifications-app-helper",
+    libs: [
+        "ub-uiautomator",
+        "app-helpers-auto-interfaces",
+    ],
+    static_libs: [
+        "truth-prebuilt",
+        "automotive-standard-app-helper",
+    ],
+    srcs: ["src/**/*.java"],
+    sdk_version: "test_current",
+}
diff --git a/libraries/automotive-helpers/notifications-app-helper/src/android/platform/helpers/AutoNotificationHelperImpl.java b/libraries/automotive-helpers/notifications-app-helper/src/android/platform/helpers/AutoNotificationHelperImpl.java
new file mode 100644
index 0000000..2ac3b96
--- /dev/null
+++ b/libraries/automotive-helpers/notifications-app-helper/src/android/platform/helpers/AutoNotificationHelperImpl.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2021 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.helpers;
+
+import android.app.Instrumentation;
+import android.os.SystemClock;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiObject2;
+
+/** Helper for Notifications on Automotive device */
+public class AutoNotificationHelperImpl extends AbstractAutoStandardAppHelper
+        implements IAutoNotificationHelper {
+
+    private static final int UI_RESPONSE_WAIT_MS = 1000;
+
+    public AutoNotificationHelperImpl(Instrumentation instr) {
+        super(instr);
+    }
+
+    /**
+     * Setup expectation: None.
+     *
+     * <p>Open notification, do nothing if notification is already open.
+     */
+    @Override
+    public void open() {
+        if (!isAppInForeground()) {
+            executeShellCommand(
+                    getApplicationConfig(AutoConfigConstants.OPEN_NOTIFICATIONS_COMMAND));
+            SystemClock.sleep(UI_RESPONSE_WAIT_MS);
+        }
+    }
+
+    /**
+     * Setup expectations: None
+     *
+     * <p>Check if notification app is in foreground by checking if the notification list exists.
+     */
+    @Override
+    public boolean isAppInForeground() {
+        return (findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.NOTIFICATIONS,
+                                AutoConfigConstants.EXPANDED_NOTIFICATIONS_SCREEN,
+                                AutoConfigConstants.NOTIFICATION_VIEW))
+                != null);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void tapClearAllBtn() {
+        scrollThroughNotifications();
+        if (clearAllBtnExist()) {
+            UiObject2 clear_all_btn =
+                    findUiObject(
+                            getResourceFromConfig(
+                                    AutoConfigConstants.NOTIFICATIONS,
+                                    AutoConfigConstants.EXPANDED_NOTIFICATIONS_SCREEN,
+                                    AutoConfigConstants.CLEAR_ALL_BUTTON));
+            clickAndWaitForIdleScreen(clear_all_btn);
+        } else {
+            throw new RuntimeException("Cannot find Clear All button");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean checkNotificationExists(String title) {
+        executeShellCommand(getApplicationConfig(AutoConfigConstants.OPEN_NOTIFICATIONS_COMMAND));
+        UiObject2 notification_list =
+                findUiObject(
+                        getResourceFromConfig(
+                                        AutoConfigConstants.NOTIFICATIONS,
+                                        AutoConfigConstants.EXPANDED_NOTIFICATIONS_SCREEN,
+                                        AutoConfigConstants.NOTIFICATION_LIST)
+                                .clazz(
+                                        getApplicationConfig(
+                                                AutoConfigConstants.RECYCLER_VIEW_CLASS)));
+        UiObject2 postedNotification = findUiObject(By.text(title));
+        // This scrolls the notification list until the notification is found
+        // or reaches to the bottom when "Clear All" button is presented.
+        while (postedNotification == null && isAppInForeground()) {
+            if (clearAllBtnExist()) {
+                break;
+            }
+            notification_list.scroll(Direction.DOWN, 20, 300);
+            postedNotification = findUiObject(By.text(title));
+        }
+        return postedNotification != null;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void removeNotification(String title) {
+        waitForGone(By.text(title));
+        executeShellCommand(getApplicationConfig(AutoConfigConstants.OPEN_NOTIFICATIONS_COMMAND));
+        UiObject2 postedNotification = findUiObject(By.text(title));
+        postedNotification.swipe(Direction.LEFT, 1.0f);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void openNotification() {
+        UiObject2 statusBar =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.NOTIFICATIONS,
+                                AutoConfigConstants.EXPANDED_NOTIFICATIONS_SCREEN,
+                                AutoConfigConstants.STATUS_BAR));
+        statusBar.swipe(Direction.DOWN, 1.0f, 500);
+    }
+
+    private void scrollThroughNotifications() {
+        executeShellCommand(getApplicationConfig(AutoConfigConstants.OPEN_NOTIFICATIONS_COMMAND));
+        UiObject2 notification_list =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.NOTIFICATIONS,
+                                AutoConfigConstants.EXPANDED_NOTIFICATIONS_SCREEN,
+                                AutoConfigConstants.NOTIFICATION_LIST));
+        int max_swipe = 5;
+        while (max_swipe > 0 && isAppInForeground()) {
+            if (clearAllBtnExist()) {
+                break;
+            } else {
+                max_swipe--;
+            }
+            notification_list.scroll(Direction.DOWN, 20, 300);
+        }
+    }
+
+    private boolean clearAllBtnExist() {
+        UiObject2 clear_all_btn =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.NOTIFICATIONS,
+                                AutoConfigConstants.EXPANDED_NOTIFICATIONS_SCREEN,
+                                AutoConfigConstants.CLEAR_ALL_BUTTON));
+        return clear_all_btn != null;
+    }
+}
diff --git a/libraries/automotive-helpers/notifications-app-helper/src/android/platform/helpers/AutoNotificationMockingHelperImpl.java b/libraries/automotive-helpers/notifications-app-helper/src/android/platform/helpers/AutoNotificationMockingHelperImpl.java
new file mode 100644
index 0000000..c356200
--- /dev/null
+++ b/libraries/automotive-helpers/notifications-app-helper/src/android/platform/helpers/AutoNotificationMockingHelperImpl.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2021 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.helpers;
+
+import static junit.framework.Assert.assertTrue;
+
+import android.app.Instrumentation;
+import android.app.Notification;
+import android.app.Notification.MessagingStyle.Message;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Person;
+import android.content.Context;
+import android.os.SystemClock;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiObject2;
+
+import java.util.List;
+import java.util.ArrayList;
+import java.util.stream.Collectors;
+
+public class AutoNotificationMockingHelperImpl extends AbstractAutoStandardAppHelper
+        implements IAutoNotificationMockingHelper {
+
+    private static final int UI_RESPONSE_WAIT_MS = 5000;
+
+    private static final String NOTIFICATION_CHANNEL_ID = "auto_test_channel_id";
+    private static final String NOTIFICATION_CHANNEL_NAME = "Test Channel";
+    private static final String NOTIFICATION_TITLE_TEXT = "AUTO TEST NOTIFICATION";
+    private static final String NOTIFICATION_CONTENT_TEXT = "Test notification content";
+    private static final String NOTIFICATION_CONTENT_TEXT_FORMAT = "Test notification %d";
+
+    private static final List<BySelector> NOTIFICATION_REQUIRED_FIELDS = new ArrayList<>();
+
+    private static final int NOTIFICATION_DEPTH = 6;
+
+    private NotificationManager mNotificationManager;
+
+    public AutoNotificationMockingHelperImpl(Instrumentation instr) {
+        super(instr);
+        mNotificationManager = instr.getContext().getSystemService(NotificationManager.class);
+        NotificationChannel channel =
+                new NotificationChannel(
+                        NOTIFICATION_CHANNEL_ID,
+                        NOTIFICATION_CHANNEL_NAME,
+                        NotificationManager.IMPORTANCE_HIGH);
+        mNotificationManager.createNotificationChannel(channel);
+        NOTIFICATION_REQUIRED_FIELDS.add(
+                getResourceFromConfig(
+                        AutoConfigConstants.NOTIFICATIONS,
+                        AutoConfigConstants.EXPANDED_NOTIFICATIONS_SCREEN,
+                        AutoConfigConstants.APP_ICON));
+        NOTIFICATION_REQUIRED_FIELDS.add(
+                getResourceFromConfig(
+                        AutoConfigConstants.NOTIFICATIONS,
+                        AutoConfigConstants.EXPANDED_NOTIFICATIONS_SCREEN,
+                        AutoConfigConstants.APP_NAME));
+        NOTIFICATION_REQUIRED_FIELDS.add(
+                getResourceFromConfig(
+                        AutoConfigConstants.NOTIFICATIONS,
+                        AutoConfigConstants.EXPANDED_NOTIFICATIONS_SCREEN,
+                        AutoConfigConstants.NOTIFICATION_TITLE));
+        NOTIFICATION_REQUIRED_FIELDS.add(
+                getResourceFromConfig(
+                        AutoConfigConstants.NOTIFICATIONS,
+                        AutoConfigConstants.EXPANDED_NOTIFICATIONS_SCREEN,
+                        AutoConfigConstants.NOTIFICATION_BODY));
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void postNotifications(int count) {
+        postNotifications(count, null);
+        SystemClock.sleep(UI_RESPONSE_WAIT_MS);
+        assertTrue(
+                "Notification does not have all required fields",
+                checkNotificationRequiredFieldsExist(NOTIFICATION_TITLE_TEXT));
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void postNotifications(int count, String pkg) {
+        postNotifications(count, pkg, false /* interrupting */);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void postNotifications(int count, String pkg, boolean interrupting) {
+        int initialCount = mNotificationManager.getActiveNotifications().length;
+        Notification.Builder builder = getBuilder(pkg);
+        if (interrupting) {
+            Person person = new Person.Builder().setName("Marvelous user").build();
+            builder.setStyle(
+                    new Notification.MessagingStyle(person)
+                            .addMessage(
+                                    new Message(
+                                            "Hello",
+                                            SystemClock.currentThreadTimeMillis(),
+                                            person)));
+        }
+
+        for (int i = initialCount; i < initialCount + count; i++) {
+            builder.setContentText(String.format(NOTIFICATION_CONTENT_TEXT_FORMAT, i));
+            mNotificationManager.notify(i, builder.build());
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void clearAllNotification() {
+        mNotificationManager.cancelAll();
+    }
+
+    private boolean checkNotificationRequiredFieldsExist(String title) {
+        if (!checkNotificationExists(title)) {
+            throw new RuntimeException(
+                    String.format("Unable to find notification with title %s", title));
+        }
+        for (BySelector selector : NOTIFICATION_REQUIRED_FIELDS) {
+            UiObject2 obj = findUiObject(selector);
+            if (obj == null) {
+                throw new RuntimeException("Unable to find required notification field");
+            }
+        }
+        return true;
+    }
+
+    private boolean checkNotificationExists(String title) {
+        executeShellCommand(getApplicationConfig(AutoConfigConstants.OPEN_NOTIFICATIONS_COMMAND));
+        UiObject2 postedNotification = findUiObject(By.text(title));
+        return postedNotification != null;
+    }
+
+    private Notification.Builder getBuilder(String pkg) {
+        Context context = mInstrumentation.getContext();
+        Notification.Builder builder =
+                new Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
+                        .setContentTitle(NOTIFICATION_TITLE_TEXT)
+                        .setContentText(NOTIFICATION_CONTENT_TEXT)
+                        .setSmallIcon(android.R.drawable.stat_notify_chat);
+        if (pkg != null) {
+            builder.setContentIntent(
+                    PendingIntent.getActivity(
+                            context,
+                            0,
+                            context.getPackageManager().getLaunchIntentForPackage(pkg),
+                            android.content.Intent.FLAG_ACTIVITY_NEW_TASK));
+        }
+        return builder;
+    }
+
+    private List<UiObject2> getNotificationStack() {
+        List<UiObject2> objects =
+                findUiObjects(
+                        getResourceFromConfig(
+                                AutoConfigConstants.NOTIFICATIONS,
+                                AutoConfigConstants.EXPANDED_NOTIFICATIONS_SCREEN,
+                                AutoConfigConstants.CARD_VIEW),
+                        NOTIFICATION_DEPTH);
+        return objects.stream().map(o -> o.getParent().getParent()).collect(Collectors.toList());
+    }
+}
diff --git a/libraries/automotive-helpers/settings-app-helper/Android.bp b/libraries/automotive-helpers/settings-app-helper/Android.bp
new file mode 100644
index 0000000..790664e
--- /dev/null
+++ b/libraries/automotive-helpers/settings-app-helper/Android.bp
@@ -0,0 +1,35 @@
+//
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+    name: "automotive-settings-app-helper",
+    libs: [
+        "ub-uiautomator",
+        "app-helpers-auto-interfaces",
+        "automotive-utility-helper",
+        "android.car",
+    ],
+    static_libs: [
+        "androidx.test.runner",
+        "automotive-standard-app-helper",
+    ],
+    srcs: ["src/**/*.java"],
+    sdk_version: "test_current",
+}
diff --git a/libraries/automotive-helpers/settings-app-helper/src/android/platform/helpers/SettingAccountsHelperImpl.java b/libraries/automotive-helpers/settings-app-helper/src/android/platform/helpers/SettingAccountsHelperImpl.java
new file mode 100644
index 0000000..2f93807
--- /dev/null
+++ b/libraries/automotive-helpers/settings-app-helper/src/android/platform/helpers/SettingAccountsHelperImpl.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2021 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.helpers;
+
+import android.app.Instrumentation;
+import android.os.SystemClock;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiObject2;
+
+/** Implementation of {@link IAutoAccountsHelper} to support tests of account settings */
+public class SettingAccountsHelperImpl extends AbstractAutoStandardAppHelper
+        implements IAutoAccountsHelper {
+
+    // Wait Time
+    private static final int UI_RESPONSE_WAIT_MS = 5000;
+
+    public SettingAccountsHelperImpl(Instrumentation instr) {
+        super(instr);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getPackage() {
+        return getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void addAccount(String email, String password) {
+        if (!doesEmailExist(email)) {
+            goToSignInPage();
+            inputAccount(email);
+            inputPassowrd(password);
+            UiObject2 doneButtonObject =
+                    scrollAndFindUiObject(
+                            getResourceFromConfig(
+                                    AutoConfigConstants.SETTINGS,
+                                    AutoConfigConstants.PROFILE_ACCOUNT_SETTINGS,
+                                    AutoConfigConstants.DONE_BUTTON));
+            if (doneButtonObject == null) {
+                throw new RuntimeException("Unable to find Done button.");
+            }
+            clickAndWaitForIdleScreen(doneButtonObject);
+        }
+    }
+
+    private void goToSignInPage() {
+        UiObject2 addAccountObject =
+                scrollAndFindUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.PROFILE_ACCOUNT_SETTINGS,
+                                AutoConfigConstants.ADD_ACCOUNT));
+        if (addAccountObject == null) {
+            throw new RuntimeException("Unable to find Add Account button.");
+        }
+        clickAndWaitForIdleScreen(addAccountObject);
+        UiObject2 signInOnCarScreen =
+                scrollAndFindUiObject(
+                        By.clickable(true)
+                                .hasDescendant(
+                                        getResourceFromConfig(
+                                                AutoConfigConstants.SETTINGS,
+                                                AutoConfigConstants.PROFILE_ACCOUNT_SETTINGS,
+                                                AutoConfigConstants.SIGN_IN_ON_CAR_SCREEN)));
+        if (signInOnCarScreen == null) {
+            throw new RuntimeException("Unable to find Sign In on Car Screen button.");
+        }
+        clickAndWaitForIdleScreen(signInOnCarScreen);
+        scrollAndFindUiObject(
+                getResourceFromConfig(
+                        AutoConfigConstants.SETTINGS,
+                        AutoConfigConstants.PROFILE_ACCOUNT_SETTINGS,
+                        AutoConfigConstants.GOOGLE_SIGN_IN_SCREEN));
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void removeAccount(String email) {
+        if (doesEmailExist(email)) {
+            BySelector accountSelector = By.text(email);
+            UiObject2 accountObject =
+                    scrollAndFindUiObject(accountSelector, getScrollScreenIndex())
+                            .getParent()
+                            .getParent();
+            clickAndWaitForIdleScreen(accountObject);
+            UiObject2 removeButtonObject =
+                    scrollAndFindUiObject(
+                            getResourceFromConfig(
+                                    AutoConfigConstants.SETTINGS,
+                                    AutoConfigConstants.PROFILE_ACCOUNT_SETTINGS,
+                                    AutoConfigConstants.REMOVE_BUTTON));
+            if (removeButtonObject == null) {
+                throw new RuntimeException("Unable to find Remove button.");
+            }
+            clickAndWaitForIdleScreen(removeButtonObject);
+            UiObject2 confirmObject =
+                    scrollAndFindUiObject(
+                            getResourceFromConfig(
+                                    AutoConfigConstants.SETTINGS,
+                                    AutoConfigConstants.PROFILE_ACCOUNT_SETTINGS,
+                                    AutoConfigConstants.REMOVE_ACCOUNT_BUTTON));
+            if (removeButtonObject == null) {
+                throw new RuntimeException("Unable to find Remove Account button.");
+            }
+            clickAndWaitForIdleScreen(confirmObject);
+            waitForGone(accountSelector);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean doesEmailExist(String email) {
+        UiObject2 accountObject = scrollAndFindUiObject(By.text(email), getScrollScreenIndex());
+        return accountObject != null;
+    }
+
+    private int getScrollScreenIndex() {
+        int scrollScreenIndex = 0;
+        if (hasSplitScreenSettingsUI()) {
+            scrollScreenIndex = 1;
+        }
+        return scrollScreenIndex;
+    }
+
+    private void inputAccount(String account) {
+        inputText(account, false);
+    }
+
+    private void inputPassowrd(String password) {
+        inputText(password, true);
+    }
+
+    private void inputText(String text, boolean isPassword) {
+        BySelector selector =
+                getResourceFromConfig(
+                        AutoConfigConstants.SETTINGS,
+                        AutoConfigConstants.PROFILE_ACCOUNT_SETTINGS,
+                        AutoConfigConstants.ENTER_EMAIL);
+        if (isPassword) {
+            selector =
+                    getResourceFromConfig(
+                            AutoConfigConstants.SETTINGS,
+                            AutoConfigConstants.PROFILE_ACCOUNT_SETTINGS,
+                            AutoConfigConstants.ENTER_PASSWORD);
+        }
+        UiObject2 input = scrollAndFindUiObject(selector);
+        if (input == null) {
+            throw new RuntimeException(
+                    String.format("%s input is not present", selector.toString()));
+        }
+        input.setText(text);
+        scrollAndFindUiObject(By.text(text).focused(false));
+        UiObject2 nextButtonObject =
+                scrollAndFindUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.PROFILE_ACCOUNT_SETTINGS,
+                                AutoConfigConstants.NEXT_BUTTON));
+        if (nextButtonObject == null) {
+            throw new RuntimeException("Unable to find Next button.");
+        }
+        clickAndWaitForGone(nextButtonObject, By.text(text));
+        SystemClock.sleep(UI_RESPONSE_WAIT_MS); // to avoid stale object error
+    }
+}
diff --git a/libraries/automotive-helpers/settings-app-helper/src/android/platform/helpers/SettingHelperImpl.java b/libraries/automotive-helpers/settings-app-helper/src/android/platform/helpers/SettingHelperImpl.java
new file mode 100644
index 0000000..c37c453
--- /dev/null
+++ b/libraries/automotive-helpers/settings-app-helper/src/android/platform/helpers/SettingHelperImpl.java
@@ -0,0 +1,476 @@
+/*
+ * Copyright (C) 2021 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.helpers;
+
+import android.app.Instrumentation;
+import android.app.UiModeManager;
+import android.bluetooth.BluetoothAdapter;
+import android.content.Context;
+import android.net.wifi.WifiManager;
+import android.os.SystemClock;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.UiObjectNotFoundException;
+import android.support.test.uiautomator.UiScrollable;
+import android.support.test.uiautomator.UiSelector;
+import androidx.test.InstrumentationRegistry;
+
+import java.util.List;
+import java.util.regex.Pattern;
+
+public class SettingHelperImpl extends AbstractAutoStandardAppHelper implements IAutoSettingHelper {
+
+    private static final String LOG_TAG = SettingHelperImpl.class.getSimpleName();
+
+    // Wait Time
+    private static final int UI_RESPONSE_WAIT_MS = 5000;
+
+    private UiModeManager mUiModeManager;
+    private Context mContext;
+
+    public SettingHelperImpl(Instrumentation instr) {
+        super(instr);
+        mUiModeManager =
+                InstrumentationRegistry.getInstrumentation()
+                        .getContext()
+                        .getSystemService(UiModeManager.class);
+        mContext = InstrumentationRegistry.getContext();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void open() {
+        openFullSettings();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getPackage() {
+        return getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void stopSettingsApplication() {
+        String cmd =
+                String.format(
+                        "am force-stop %s",
+                        getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE));
+        executeShellCommand(cmd);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getLauncherName() {
+        return "Settings";
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void openSetting(String setting) {
+        openFullSettings();
+        openMenuWith(getSettingPath(setting));
+        verifyAvailableOptions(setting);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public UiObject2 findSettingMenu(String setting) {
+        UiObject2 menuObject = null;
+        String[] menuOptions = getSettingPath(setting);
+        int currentIndex = 0;
+        int lastIndex = menuOptions.length - 1;
+        for (String menu : menuOptions) {
+            int scrollableScreenIndex = 0;
+            if (hasSplitScreenSettingsUI() && currentIndex > 0) {
+                scrollableScreenIndex = 1;
+            }
+            menuObject = getMenu(menu, scrollableScreenIndex);
+            if (currentIndex == lastIndex) {
+                return menuObject;
+            }
+            clickAndWaitForIdleScreen(menuObject);
+            waitForIdle();
+            currentIndex++;
+        }
+        return menuObject;
+    }
+
+    @Override
+    public void findSettingMenuAndClick(String setting) {
+        UiObject2 settingMenu = findSettingMenu(setting);
+        if (settingMenu != null) {
+            clickAndWaitForIdleScreen(settingMenu);
+        } else {
+            throw new RuntimeException("Unable to find setting menu");
+        }
+    }
+
+    @Override
+    public String getPageTitleText() {
+        UiObject2 pageToolbarTitle = getPageTitle();
+        return pageToolbarTitle.getText();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void openFullSettings() {
+        executeShellCommand(getApplicationConfig(AutoConfigConstants.OPEN_SETTINGS_COMMAND));
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void openQuickSettings() {
+        pressHome();
+        executeShellCommand(getApplicationConfig(AutoConfigConstants.OPEN_QUICK_SETTINGS_COMMAND));
+        UiObject2 settingObject =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.QUICK_SETTINGS,
+                                AutoConfigConstants.OPEN_MORE_SETTINGS));
+        if (settingObject == null) {
+            throw new RuntimeException("Failed to open quick settings.");
+        }
+    }
+
+    private void verifyAvailableOptions(String setting) {
+        String[] expectedOptions = getSettingOptions(setting);
+        if (expectedOptions == null) {
+            return;
+        }
+        for (String option : expectedOptions) {
+            if (mDevice.hasObject(By.clickable(false).textContains(option))) {
+                continue;
+            }
+            Pattern menuPattern = Pattern.compile(option, Pattern.CASE_INSENSITIVE);
+            BySelector selector = By.text(menuPattern);
+            int scrollScreenIndex = 0;
+            if (hasSplitScreenSettingsUI()) {
+                scrollScreenIndex = 1;
+            }
+            if (scrollAndFindUiObject(selector, scrollScreenIndex) == null) {
+                throw new RuntimeException("Cannot find settings option: " + option);
+            }
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void turnOnOffWifi(boolean onOff) {
+        boolean isOn = isWifiOn();
+        if (isOn != onOff) {
+            UiObject2 enableOption =
+                    findUiObject(
+                            getResourceFromConfig(
+                                    AutoConfigConstants.SETTINGS,
+                                    AutoConfigConstants.NETWORK_AND_INTERNET_SETTINGS,
+                                    AutoConfigConstants.TOGGLE_WIFI));
+            clickAndWaitForWindowUpdate(
+                    getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), enableOption);
+        } else {
+            throw new RuntimeException("Wi-Fi enabled state is already " + (onOff ? "on" : "off"));
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isWifiOn() {
+        WifiManager wifi = (WifiManager) this.mContext.getSystemService(Context.WIFI_SERVICE);
+        return wifi.isWifiEnabled();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void turnOnOffHotspot(boolean onOff) {
+        boolean isOn = isHotspotOn();
+        if (isOn != onOff) {
+            UiObject2 enableOption =
+                    findUiObject(
+                            getResourceFromConfig(
+                                    AutoConfigConstants.SETTINGS,
+                                    AutoConfigConstants.NETWORK_AND_INTERNET_SETTINGS,
+                                    AutoConfigConstants.TOGGLE_HOTSPOT));
+            clickAndWaitForWindowUpdate(
+                    getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), enableOption);
+        } else {
+            throw new RuntimeException(
+                    "Hotspot enabled state is already " + (onOff ? "on" : "off"));
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isHotspotOn() {
+        UiObject2 enableOption =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.NETWORK_AND_INTERNET_SETTINGS,
+                                AutoConfigConstants.TOGGLE_HOTSPOT));
+        return enableOption.isChecked();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void turnOnOffBluetooth(boolean onOff) {
+        boolean isOn = isBluetoothOn();
+        if (isOn != onOff) {
+            UiObject2 enableOption =
+                    findUiObject(
+                            getResourceFromConfig(
+                                    AutoConfigConstants.SETTINGS,
+                                    AutoConfigConstants.BLUETOOTH_SETTINGS,
+                                    AutoConfigConstants.TOGGLE_BLUETOOTH));
+            clickAndWaitForWindowUpdate(
+                    getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), enableOption);
+        } else {
+            throw new RuntimeException(
+                    "Bluetooth enabled state is already " + (onOff ? "on" : "off"));
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isBluetoothOn() {
+        BluetoothAdapter ba = BluetoothAdapter.getDefaultAdapter();
+        return ba.isEnabled();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void searchAndSelect(String item) {
+        searchAndSelect(item, 0);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void searchAndSelect(String item, int selectedIndex) {
+        UiObject2 search_button =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.FULL_SETTINGS,
+                                AutoConfigConstants.SEARCH));
+        clickAndWaitForIdleScreen(search_button);
+        UiObject2 search_box =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.FULL_SETTINGS,
+                                AutoConfigConstants.SEARCH_BOX));
+        search_box.setText(item);
+        SystemClock.sleep(UI_RESPONSE_WAIT_MS);
+        // close the keyboard to reveal all search results.
+        mDevice.pressBack();
+
+        UiObject2 searchResults =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.FULL_SETTINGS,
+                                AutoConfigConstants.SEARCH_RESULTS));
+        int numberOfResults = searchResults.getChildren().size();
+        if (numberOfResults == 0) {
+            throw new RuntimeException("No results found");
+        }
+        clickAndWaitForIdleScreen(searchResults.getChildren().get(selectedIndex));
+        SystemClock.sleep(UI_RESPONSE_WAIT_MS);
+
+        UiObject2 object = findUiObject(By.textContains(item));
+        if (object == null) {
+            throw new RuntimeException("Opened page does not contain searched item");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isValidPageTitle(String item) {
+        UiObject2 pageTitle = getPageTitle();
+        return pageTitle.getText().contains(item);
+    }
+
+    private UiObject2 getPageTitle() {
+        BySelector[] selectors =
+                new BySelector[] {
+                    getResourceFromConfig(
+                            AutoConfigConstants.SETTINGS,
+                            AutoConfigConstants.FULL_SETTINGS,
+                            AutoConfigConstants.PAGE_TITLE),
+                    getResourceFromConfig(
+                            AutoConfigConstants.SETTINGS,
+                            AutoConfigConstants.APPS_SETTINGS,
+                            AutoConfigConstants.PERMISSIONS_PAGE_TITLE)
+                };
+        for (BySelector selector : selectors) {
+            List<UiObject2> pageTitles = findUiObjects(selector);
+            if (pageTitles != null && pageTitles.size() > 0) {
+                return pageTitles.get(pageTitles.size() - 1);
+            }
+        }
+        throw new RuntimeException("Unable to find page title");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void goBackToSettingsScreen() {
+        // count is used to avoid infinite loop in case someone invokes
+        // after exiting settings application
+        int count = 5;
+        while (count > 0
+                && isAppInForeground()
+                && findUiObject(
+                                By.text(
+                                        getApplicationConfig(
+                                                AutoConfigConstants.SETTINGS_TITLE_TEXT)))
+                        == null) {
+            pressBack();
+            SystemClock.sleep(UI_RESPONSE_WAIT_MS); // to avoid stale object error
+            count--;
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void openMenuWith(String... menuOptions) {
+        for (String menu : menuOptions) {
+            Pattern menuPattern = Pattern.compile(menu, Pattern.CASE_INSENSITIVE);
+            UiObject2 menuButton = scrollAndFindUiObject(By.text(menuPattern));
+            if (menuButton == null) {
+                throw new RuntimeException("Unable to find menu item");
+            }
+            clickAndWaitForIdleScreen(menuButton);
+            waitForIdle();
+        }
+    }
+
+    /**
+     * Checks whether a setting menu is enabled or not. When not enabled, the menu item cannot be
+     * clicked.
+     */
+    @Override
+    public boolean isSettingMenuEnabled(String menu) {
+        boolean isSettingMenuEnabled = false;
+        String[] menuOptions = getSettingPath(menu);
+        int currentIndex = 0;
+        int lastIndex = menuOptions.length - 1;
+        for (String menuOption : menuOptions) {
+            int scrollableScreenIndex = 0;
+            if (hasSplitScreenSettingsUI() && currentIndex > 0) {
+                scrollableScreenIndex = 1;
+            }
+            UiObject2 menuObject = getMenu(menuOption, scrollableScreenIndex);
+            if (currentIndex == lastIndex) {
+                return menuObject.isEnabled();
+            }
+            if (!menuObject.isEnabled()) {
+                return isSettingMenuEnabled;
+            }
+            clickAndWaitForIdleScreen(menuObject);
+            waitForIdle();
+            currentIndex++;
+        }
+        return isSettingMenuEnabled;
+    }
+
+    private UiObject2 getMenu(String menu, int index) {
+        Pattern menuPattern = Pattern.compile(menu, Pattern.CASE_INSENSITIVE);
+        UiObject2 menuButton = scrollAndFindUiObject(By.text(menuPattern), index);
+        if (menuButton == null) {
+            throw new RuntimeException("Unable to find menu item");
+        }
+        return menuButton;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int getValue(String setting) {
+        String cmd = String.format("settings get system %s", setting);
+        String value = executeShellCommand(cmd);
+        return Integer.parseInt(value.replaceAll("\\s", ""));
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setValue(String setting, int value) {
+        String cmd = String.format("settings put system %s %d", setting, value);
+        executeShellCommand(cmd);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void changeSeekbarLevel(int index, ChangeType changeType) {
+        try {
+            String seekBar =
+                    String.format(
+                            "%s:id/%s",
+                            getResourcePackage(
+                                    AutoConfigConstants.SETTINGS,
+                                    AutoConfigConstants.DISPLAY_SETTINGS,
+                                    AutoConfigConstants.BRIGHTNESS_LEVEL),
+                            getResourceValue(
+                                    AutoConfigConstants.SETTINGS,
+                                    AutoConfigConstants.DISPLAY_SETTINGS,
+                                    AutoConfigConstants.BRIGHTNESS_LEVEL));
+            UiScrollable seekbar =
+                    new UiScrollable(new UiSelector().resourceId(seekBar).instance(index));
+            if (changeType == ChangeType.INCREASE) {
+                seekbar.scrollForward(1);
+            } else {
+                seekbar.scrollBackward(1);
+            }
+            waitForWindowUpdate(getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE));
+        } catch (UiObjectNotFoundException exception) {
+            throw new RuntimeException("Unable to find seekbar");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setDayNightMode(DayNightMode mode) {
+        if (mode == DayNightMode.DAY_MODE
+                        && getDayNightModeStatus().getValue() == mUiModeManager.MODE_NIGHT_YES
+                || mode == DayNightMode.NIGHT_MODE
+                        && getDayNightModeStatus().getValue() != mUiModeManager.MODE_NIGHT_YES) {
+            clickAndWaitForWindowUpdate(
+                    getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE),
+                    getNightModeButton());
+        }
+    }
+
+    private UiObject2 getNightModeButton() {
+        UiObject2 nightModeButton =
+                scrollAndFindUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.QUICK_SETTINGS,
+                                AutoConfigConstants.NIGHT_MODE));
+        if (nightModeButton == null) {
+            throw new RuntimeException("Unable to find night mode button");
+        }
+        return nightModeButton.getParent();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public DayNightMode getDayNightModeStatus() {
+        return mUiModeManager.getNightMode() == mUiModeManager.MODE_NIGHT_YES
+                ? DayNightMode.NIGHT_MODE
+                : DayNightMode.DAY_MODE;
+    }
+}
diff --git a/libraries/automotive-helpers/settings-app-helper/src/android/platform/helpers/SettingProfileHelperImpl.java b/libraries/automotive-helpers/settings-app-helper/src/android/platform/helpers/SettingProfileHelperImpl.java
new file mode 100644
index 0000000..db68beb
--- /dev/null
+++ b/libraries/automotive-helpers/settings-app-helper/src/android/platform/helpers/SettingProfileHelperImpl.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2021 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.helpers;
+
+import android.app.Instrumentation;
+import android.os.SystemClock;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiObject2;
+import android.util.Log;
+
+/**
+ * Implementation of {@link IAutoProfileHelper} to support tests of account settings
+ */
+public class SettingProfileHelperImpl extends AbstractAutoStandardAppHelper
+    implements IAutoProfileHelper {
+
+    // Packages
+    private static final String APP_NAME = AutoConfigConstants.SETTINGS;
+    private static final String APP_CONFIG = AutoConfigConstants.PROFILE_ACCOUNT_SETTINGS;
+
+    // Wait Time
+    private static final int UI_RESPONSE_WAIT_MS = 10000;
+
+    //constants
+    private static final String TAG = "SettingProfileHelperImpl";
+
+    public SettingProfileHelperImpl(Instrumentation instr) {
+        super(instr);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getPackage() {
+        return getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    // Add a new user
+    @Override
+    public void addProfile() {
+        clickbutton(AutoConfigConstants.ADD_PROFILE);
+        clickbutton(AutoConfigConstants.OK);
+        SystemClock.sleep(UI_RESPONSE_WAIT_MS);
+    }
+
+    // delete an existing user
+    @Override
+    public void deleteProfile(String user) {
+        if (isProfilePresent(user)) {
+            clickbutton(user);
+            clickbutton(AutoConfigConstants.DELETE);
+            clickbutton(AutoConfigConstants.DELETE);
+            SystemClock.sleep(UI_RESPONSE_WAIT_MS);
+        }
+    }
+
+    // delete self profile
+    @Override
+    public void deleteCurrentProfile() {
+        clickbutton(AutoConfigConstants.DELETE_SELF);
+        clickbutton(AutoConfigConstants.DELETE);
+        SystemClock.sleep(UI_RESPONSE_WAIT_MS);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    // check if a user is present in the list of existing users
+    @Override
+    public boolean isProfilePresent(String user) {
+        UiObject2 AddProfileButton =
+            scrollAndFindUiObject(
+                getResourceFromConfig(APP_NAME, APP_CONFIG, AutoConfigConstants.ADD_PROFILE));
+        Log.v(
+            TAG,
+            String.format(
+                "AddProfileButton = %s ; UI_Obj = %s",
+                AutoConfigConstants.ADD_PROFILE, AddProfileButton));
+        if (AddProfileButton == null) {
+            clickbutton(AutoConfigConstants.MANAGE_OTHER_PROFILES);
+            UiObject2 profileObject = scrollAndFindUiObject(By.text(user));
+            return profileObject != null;
+        }
+        return false;
+    }
+
+    // switch profile from current user to given user
+    @Override
+    public void switchProfile(String userFrom, String userTo) {
+        goToQuickSettings();
+        clickbutton(userFrom);
+        clickbutton(userTo);
+        SystemClock.sleep(UI_RESPONSE_WAIT_MS);
+    }
+
+    // add profile via quick settings
+    @Override
+    public void addProfileQuickSettings(String userFrom) {
+        goToQuickSettings();
+        clickbutton(userFrom);
+        addProfile();
+    }
+
+    // make an existing user admin
+    @Override
+    public void makeUserAdmin(String user) {
+        if (isProfilePresent(user)) {
+            clickbutton(user);
+            clickbutton(AutoConfigConstants.MAKE_ADMIN);
+            clickbutton(AutoConfigConstants.MAKE_ADMIN_CONFIRM);
+        }
+    }
+
+    // click an on-screen element if expected text for that element is present
+    private void clickbutton(String button_text) {
+        UiObject2 buttonObject =
+            scrollAndFindUiObject(getResourceFromConfig(APP_NAME, APP_CONFIG, button_text));
+        if (buttonObject == null) {
+            buttonObject = scrollAndFindUiObject(By.text(button_text));
+        }
+        Log.v(
+            TAG,
+            String.format("button =  %s ; UI_Obj = %s", button_text, buttonObject));
+
+        if (buttonObject == null) {
+            throw new RuntimeException(
+                String.format("Unable to find Object with text: %s", button_text));
+        }
+        clickAndWaitForIdleScreen(buttonObject);
+        SystemClock.sleep(UI_RESPONSE_WAIT_MS);
+    }
+
+    // go to quick Settings for switching profile
+    private void goToQuickSettings() {
+        clickbutton(AutoConfigConstants.TIME_PATTERN);
+    }
+}
diff --git a/libraries/automotive-helpers/settings-app-helper/src/android/platform/helpers/SettingsAppInfoHelperImpl.java b/libraries/automotive-helpers/settings-app-helper/src/android/platform/helpers/SettingsAppInfoHelperImpl.java
new file mode 100644
index 0000000..1c61a6f
--- /dev/null
+++ b/libraries/automotive-helpers/settings-app-helper/src/android/platform/helpers/SettingsAppInfoHelperImpl.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2021 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.helpers;
+
+import static junit.framework.Assert.assertTrue;
+
+import android.app.Instrumentation;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiObject2;
+
+public class SettingsAppInfoHelperImpl extends AbstractAutoStandardAppHelper
+        implements IAutoAppInfoSettingsHelper {
+    public SettingsAppInfoHelperImpl(Instrumentation instr) {
+        super(instr);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void open() {
+        executeShellCommand(getApplicationConfig(AutoConfigConstants.OPEN_SETTINGS_COMMAND));
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getPackage() {
+        return getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getLauncherName() {
+        throw new UnsupportedOperationException("Not Supported");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void showAllApps() {
+        BySelector selector =
+                By.clickable(true)
+                        .hasDescendant(
+                                getResourceFromConfig(
+                                        AutoConfigConstants.SETTINGS,
+                                        AutoConfigConstants.APPS_SETTINGS,
+                                        AutoConfigConstants.VIEW_ALL));
+        UiObject2 show_all_apps_menu = scrollAndFindUiObject(selector, getScrollScreenIndex());
+        clickAndWaitForWindowUpdate(
+                getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), show_all_apps_menu);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void enableDisableApplication(State state) {
+        BySelector enableDisableBtnSelector =
+                getResourceFromConfig(
+                        AutoConfigConstants.SETTINGS,
+                        AutoConfigConstants.APPS_SETTINGS,
+                        AutoConfigConstants.ENABLE_DISABLE_BUTTON);
+        UiObject2 enableDisableBtn =
+                scrollAndFindUiObject(enableDisableBtnSelector, getScrollScreenIndex());
+        clickAndWaitForWindowUpdate(
+                getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE),
+                enableDisableBtn.getParent());
+        if (state == State.ENABLE) {
+            assertTrue(
+                    "application is not enabled",
+                    enableDisableBtn
+                            .getText()
+                            .matches(
+                                    "(?i)"
+                                            + getResourceValue(
+                                                    AutoConfigConstants.SETTINGS,
+                                                    AutoConfigConstants.APPS_SETTINGS,
+                                                    AutoConfigConstants.DISABLE_BUTTON_TEXT)));
+        } else {
+            BySelector disableAppBtnSelector =
+                    getResourceFromConfig(
+                            AutoConfigConstants.SETTINGS,
+                            AutoConfigConstants.APPS_SETTINGS,
+                            AutoConfigConstants.DISABLE_APP_BUTTON);
+            UiObject2 disableAppBtn =
+                    scrollAndFindUiObject(disableAppBtnSelector, getScrollScreenIndex());
+            clickAndWaitForWindowUpdate(
+                    getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), disableAppBtn);
+            assertTrue(
+                    "application is not disabled",
+                    enableDisableBtn
+                            .getText()
+                            .matches(
+                                    "(?i)"
+                                            + getResourceValue(
+                                                    AutoConfigConstants.SETTINGS,
+                                                    AutoConfigConstants.APPS_SETTINGS,
+                                                    AutoConfigConstants.ENABLE_BUTTON_TEXT)));
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isCurrentApplicationRunning() {
+        UiObject2 forceStopButton = getForceStopButton();
+        if (forceStopButton == null) {
+            throw new RuntimeException("Cannot find force stop button");
+        }
+        return forceStopButton.isEnabled() ? true : false;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void forceStop() {
+        UiObject2 forceStopButton = getForceStopButton();
+        if (forceStopButton == null) {
+            throw new RuntimeException("Cannot find force stop button");
+        }
+        clickAndWaitForWindowUpdate(
+                getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), forceStopButton);
+        BySelector okBtnSelector =
+                getResourceFromConfig(
+                        AutoConfigConstants.SETTINGS,
+                        AutoConfigConstants.APPS_SETTINGS,
+                        AutoConfigConstants.OK_BUTTON);
+        UiObject2 okBtn = findUiObject(okBtnSelector);
+        clickAndWaitForWindowUpdate(
+                getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), okBtn);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setAppPermission(String permission, State state) {
+        BySelector permissions_selector =
+                getResourceFromConfig(
+                        AutoConfigConstants.SETTINGS,
+                        AutoConfigConstants.APPS_SETTINGS,
+                        AutoConfigConstants.PERMISSIONS_MENU);
+        UiObject2 permissions_menu =
+                scrollAndFindUiObject(permissions_selector, getScrollScreenIndex());
+        clickAndWaitForWindowUpdate(
+                getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), permissions_menu);
+        BySelector permission_selector = By.text(permission);
+        UiObject2 permission_menu =
+                scrollAndFindUiObject(permission_selector, getScrollScreenIndex());
+        clickAndWaitForWindowUpdate(
+                getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), permission_menu);
+        if (state == State.ENABLE) {
+            UiObject2 allow_btn =
+                    scrollAndFindUiObject(
+                            getResourceFromConfig(
+                                    AutoConfigConstants.SETTINGS,
+                                    AutoConfigConstants.APPS_SETTINGS,
+                                    AutoConfigConstants.ALLOW_BUTTON));
+            clickAndWaitForWindowUpdate(
+                    getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), allow_btn);
+        } else {
+            UiObject2 dont_allow_btn =
+                    scrollAndFindUiObject(
+                            getResourceFromConfig(
+                                    AutoConfigConstants.SETTINGS,
+                                    AutoConfigConstants.APPS_SETTINGS,
+                                    AutoConfigConstants.DONT_ALLOW_BUTTON));
+            clickAndWaitForWindowUpdate(
+                    getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), dont_allow_btn);
+            UiObject2 dont_allow_anyway_btn =
+                    scrollAndFindUiObject(
+                            getResourceFromConfig(
+                                    AutoConfigConstants.SETTINGS,
+                                    AutoConfigConstants.APPS_SETTINGS,
+                                    AutoConfigConstants.DONT_ALLOW_ANYWAY_BUTTON));
+            clickAndWaitForWindowUpdate(
+                    getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE),
+                    dont_allow_anyway_btn);
+        }
+        pressBack();
+        pressBack();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getCurrentPermissions() {
+        BySelector permissions_selector =
+                getResourceFromConfig(
+                        AutoConfigConstants.SETTINGS,
+                        AutoConfigConstants.APPS_SETTINGS,
+                        AutoConfigConstants.PERMISSIONS_MENU);
+        UiObject2 permission_menu =
+                scrollAndFindUiObject(permissions_selector, getScrollScreenIndex());
+        String currentPermissions = permission_menu.getParent().getChildren().get(1).getText();
+        return currentPermissions;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void selectApp(String application) {
+        BySelector selector = By.textContains(application);
+        UiObject2 object = scrollAndFindUiObject(selector, getScrollScreenIndex());
+        if (object == null) {
+            throw new RuntimeException("Cannot find the app menu");
+        }
+        clickAndWaitForWindowUpdate(
+                getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), object);
+    }
+
+    private UiObject2 getForceStopButton() {
+        BySelector forceStopSelector =
+                getResourceFromConfig(
+                        AutoConfigConstants.SETTINGS,
+                        AutoConfigConstants.APPS_SETTINGS,
+                        AutoConfigConstants.FORCE_STOP_BUTTON);
+        UiObject2 forceStopButton =
+                scrollAndFindUiObject(forceStopSelector, getScrollScreenIndex());
+        return forceStopButton;
+    }
+
+    private int getScrollScreenIndex() {
+        int scrollScreenIndex = 0;
+        if (hasSplitScreenSettingsUI()) {
+            scrollScreenIndex = 1;
+        }
+        return scrollScreenIndex;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isApplicationDisabled(String packageName) {
+        boolean applicationDisabled = false;
+        try {
+            PackageManager pm = mInstrumentation.getContext().getPackageManager();
+            applicationDisabled = !(pm.getApplicationInfo(packageName, 0).enabled);
+        } catch (NameNotFoundException e) {
+            throw new RuntimeException(String.format("Failed to find package: %s", packageName), e);
+        }
+        return applicationDisabled;
+    }
+}
diff --git a/libraries/automotive-helpers/settings-app-helper/src/android/platform/helpers/SettingsDateTimeHelperImpl.java b/libraries/automotive-helpers/settings-app-helper/src/android/platform/helpers/SettingsDateTimeHelperImpl.java
new file mode 100644
index 0000000..2d9e1c5
--- /dev/null
+++ b/libraries/automotive-helpers/settings-app-helper/src/android/platform/helpers/SettingsDateTimeHelperImpl.java
@@ -0,0 +1,458 @@
+/*
+ * Copyright (C) 2021 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.helpers;
+
+import static junit.framework.Assert.assertTrue;
+
+import android.app.Instrumentation;
+import android.content.ContentResolver;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiObject;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.UiScrollable;
+import android.support.test.uiautomator.UiSelector;
+import android.text.format.DateFormat;
+import java.time.format.TextStyle;
+import java.time.LocalDate;
+import java.time.Month;
+import java.util.List;
+import java.util.Locale;
+
+public class SettingsDateTimeHelperImpl extends AbstractAutoStandardAppHelper
+        implements IAutoDateTimeSettingsHelper {
+    private static final Locale LOCALE = Locale.ENGLISH;
+    private static final TextStyle TEXT_STYLE = TextStyle.SHORT;
+
+    public SettingsDateTimeHelperImpl(Instrumentation instr) {
+        super(instr);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getPackage() {
+        return getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setDate(LocalDate date) {
+        UiObject2 autoDateTimeSwitchWidget = getAutoDateTimeSwitchWidget();
+        UiObject2 autoDateTimeMenu =
+                getMenu(
+                        getResourceFromConfig(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.DATE_AND_TIME_SETTINGS,
+                                AutoConfigConstants.SET_TIME_AUTOMATICALLY));
+        if (autoDateTimeSwitchWidget.isChecked()) {
+            clickAndWaitForWindowUpdate(
+                    getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), autoDateTimeMenu);
+        }
+        UiObject2 setDateMenu = getSetDateMenu();
+        assertTrue("set date menu is not clickable", setDateMenu.isEnabled()); // from UI
+        clickAndWaitForWindowUpdate(
+                getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), setDateMenu);
+        assertTrue(
+                "automatic date & time is not switched off",
+                !isAutomaticOn("auto_time")); // from API
+        int year = date.getYear();
+        Month month = date.getMonth();
+        int day = date.getDayOfMonth();
+        String month_string = month.getDisplayName(TEXT_STYLE, LOCALE);
+        String day_string = "";
+        if (day < 10) {
+            day_string = "0" + String.valueOf(day);
+        } else {
+            day_string = "" + String.valueOf(day);
+        }
+        String year_string = "" + year;
+        setCalendar(1, day_string);
+        setCalendar(0, month_string);
+        setCalendar(2, year_string);
+        pressBack();
+    }
+
+    private void setCalendar(int index, String s) {
+        UiSelector selector =
+                new UiSelector()
+                        .className(
+                                getResourceValue(
+                                        AutoConfigConstants.SETTINGS,
+                                        AutoConfigConstants.DATE_AND_TIME_SETTINGS,
+                                        AutoConfigConstants.NUMBER_PICKER_WIDGET))
+                        .index(index);
+        boolean scrollForwards = true;
+        if (index == 2) {
+            UiSelector yearSelector =
+                    selector.childSelector(
+                            new UiSelector()
+                                    .className(
+                                            getResourceValue(
+                                                    AutoConfigConstants.SETTINGS,
+                                                    AutoConfigConstants.DATE_AND_TIME_SETTINGS,
+                                                    AutoConfigConstants.EDIT_TEXT_WIDGET)));
+            String curYear = "";
+            try {
+                curYear = new UiObject(yearSelector).getText();
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            }
+            if (Integer.valueOf(curYear) > Integer.valueOf(s)) scrollForwards = false;
+        }
+        scrollToObjectInPicker(index, s, scrollForwards);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public LocalDate getDate() {
+        UiObject2 obj = getSetDateMenu();
+        if (obj == null) {
+            throw new RuntimeException("Unable to find set date menu.");
+        }
+        String uiDate = getMenuSummaryText(obj);
+        String[] arr = uiDate.split(" ");
+        if (arr.length != 3) {
+            throw new RuntimeException("Cannot find date from UI");
+        }
+        int year = Integer.valueOf(arr[2]);
+        int month = Month.valueOf(arr[0].toUpperCase()).getValue();
+        int day = Integer.valueOf(arr[1].substring(0, arr[1].length() - 1));
+        return LocalDate.of(year, month, day);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setTimeInTwelveHourFormat(int hour, int minute, boolean am) {
+        // check Automatic date & time switch is turned off
+        UiObject2 autoDateTimeSwitchWidget = getAutoDateTimeSwitchWidget();
+        UiObject2 autoDateTimeMenu =
+                getMenu(
+                        getResourceFromConfig(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.DATE_AND_TIME_SETTINGS,
+                                AutoConfigConstants.SET_TIME_AUTOMATICALLY));
+        if (autoDateTimeSwitchWidget.isChecked()) {
+            clickAndWaitForWindowUpdate(
+                    getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), autoDateTimeMenu);
+        }
+        // check 24-hour format switch is turned off
+        if (isTwentyFourHourFormatEnabled()) {
+            toggleTwentyFourHourFormatSwitch();
+        }
+
+        UiObject2 setTimeMenu = getSetTimeMenu();
+        assertTrue("set time menu is not clickable", setTimeMenu.isEnabled()); // from UI
+        clickAndWaitForWindowUpdate(
+                getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), setTimeMenu);
+        assertTrue(
+                "automatic date & time is not switched off",
+                !isAutomaticOn("auto_time")); // from API
+        String minute_string = "" + minute;
+        String am_pm = "";
+        am_pm = am ? "AM" : "PM";
+        if (minute < 10) {
+            minute_string = "0" + minute;
+        }
+        setTime(2, minute_string);
+        setTime(0, String.valueOf(hour));
+        setTime(1, am_pm);
+        pressBack();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setTimeInTwentyFourHourFormat(int hour, int minute) {
+        // check Automatic date & time switch is turned off
+        UiObject2 autoDateTimeSwitchWidget = getAutoDateTimeSwitchWidget();
+        UiObject2 autoDateTimeMenu =
+                getMenu(
+                        getResourceFromConfig(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.DATE_AND_TIME_SETTINGS,
+                                AutoConfigConstants.SET_TIME_AUTOMATICALLY));
+        if (autoDateTimeSwitchWidget.isChecked()) {
+            clickAndWaitForWindowUpdate(
+                    getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), autoDateTimeMenu);
+        }
+        // check 24-hour format switch is turned on
+        if (!isTwentyFourHourFormatEnabled()) {
+            toggleTwentyFourHourFormatSwitch();
+        }
+
+        UiObject2 setTimeMenu = getSetTimeMenu();
+        assertTrue("set time menu is not clickable", setTimeMenu.isEnabled()); // from UI
+        clickAndWaitForWindowUpdate(
+                getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), setTimeMenu);
+        assertTrue(
+                "automatic date & time is not switched off",
+                !isAutomaticOn("auto_time")); // from API
+        String minute_string = "" + minute;
+        if (minute < 10) {
+            minute_string = "0" + minute;
+        }
+        String hour_string = "" + hour;
+        if (hour < 10) {
+            hour_string = "0" + hour;
+        }
+        setTime(2, minute_string);
+        setTime(0, hour_string);
+        pressBack();
+    }
+
+    private void setTime(int index, String s) {
+        UiSelector selector =
+                new UiSelector()
+                        .className(
+                                getResourceValue(
+                                        AutoConfigConstants.SETTINGS,
+                                        AutoConfigConstants.DATE_AND_TIME_SETTINGS,
+                                        AutoConfigConstants.NUMBER_PICKER_WIDGET))
+                        .index(index);
+        boolean scrollForwards = true;
+        String curAM_PM;
+        if (index == 1) {
+            UiSelector am_pm_Selector =
+                    selector.childSelector(
+                            new UiSelector()
+                                    .className(
+                                            getResourceValue(
+                                                    AutoConfigConstants.SETTINGS,
+                                                    AutoConfigConstants.DATE_AND_TIME_SETTINGS,
+                                                    AutoConfigConstants.EDIT_TEXT_WIDGET)));
+            try {
+                curAM_PM = new UiObject(am_pm_Selector).getText();
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            }
+            if (curAM_PM.equals("PM")) scrollForwards = false;
+        }
+        scrollToObjectInPicker(index, s, scrollForwards);
+    }
+
+    private void scrollToObjectInPicker(int index, String s, boolean scrollForwards) {
+        UiSelector selector =
+                new UiSelector()
+                        .className(
+                                getResourceValue(
+                                        AutoConfigConstants.SETTINGS,
+                                        AutoConfigConstants.DATE_AND_TIME_SETTINGS,
+                                        AutoConfigConstants.NUMBER_PICKER_WIDGET))
+                        .index(index);
+        UiScrollable scrollable = new UiScrollable(selector);
+        scrollable.setAsVerticalList();
+        UiObject2 obj =
+                findUiObject(
+                        By.text(s)
+                                .clazz(
+                                        getResourceValue(
+                                                AutoConfigConstants.SETTINGS,
+                                                AutoConfigConstants.DATE_AND_TIME_SETTINGS,
+                                                AutoConfigConstants.EDIT_TEXT_WIDGET)));
+        while (obj == null) {
+            try {
+                if (scrollForwards) {
+                    scrollable.scrollForward();
+                } else {
+                    scrollable.scrollBackward();
+                }
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            }
+            obj =
+                    findUiObject(
+                            By.text(s)
+                                    .clazz(
+                                            getResourceValue(
+                                                    AutoConfigConstants.SETTINGS,
+                                                    AutoConfigConstants.DATE_AND_TIME_SETTINGS,
+                                                    AutoConfigConstants.EDIT_TEXT_WIDGET)));
+        }
+        if (obj == null) throw new RuntimeException("cannot find value in the picker");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getTime() {
+        UiObject2 obj = getSetTimeMenu();
+        if (obj == null) {
+            throw new RuntimeException("Unable to find time menu.");
+        }
+        String uiTime = getMenuSummaryText(obj);
+        return uiTime;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setTimeZone(String timezone) {
+        UiObject2 autoTimeZoneSwitchWidget = getAutoTimeZoneSwitchWidget();
+        UiObject2 autoTimeZoneMenu =
+                getMenu(
+                        getResourceFromConfig(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.DATE_AND_TIME_SETTINGS,
+                                AutoConfigConstants.SET_TIME_ZONE_AUTOMATICALLY));
+        if (getAutoTimeZoneSwitchWidget().isChecked()) {
+            clickAndWaitForWindowUpdate(
+                    getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), autoTimeZoneMenu);
+        }
+        UiObject2 selectTimeZoneMenu = getSelectTimeZoneMenu();
+        assertTrue(
+                "select time zone menu is not clickable",
+                selectTimeZoneMenu.isEnabled()); // from UI
+        assertTrue(
+                "automatic time zone is not switched off",
+                !isAutomaticOn("auto_time_zone")); // from API
+        clickAndWaitForWindowUpdate(
+                getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), selectTimeZoneMenu);
+        BySelector selector = By.clickable(true).hasDescendant(By.text(timezone));
+        UiObject2 object = scrollAndFindUiObject(selector, getScrollScreenIndex());
+        if (object == null) {
+            throw new RuntimeException(String.format("Unable to find timezone %s", timezone));
+        }
+        clickAndWaitForWindowUpdate(
+                getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), object);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean toggleTwentyFourHourFormatSwitch() {
+        UiObject2 twentyFourHourFormatSwitch = getUseTwentyFourHourFormatSwitchWidget();
+        if (twentyFourHourFormatSwitch.isChecked()) {
+            assertTrue(
+                    "System time format is different from UI format",
+                    DateFormat.is24HourFormat(mInstrumentation.getContext()));
+        } else {
+            assertTrue(
+                    "System time format is different from UI format",
+                    !DateFormat.is24HourFormat(mInstrumentation.getContext()));
+        }
+        clickAndWaitForWindowUpdate(
+                getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE),
+                twentyFourHourFormatSwitch);
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getTimeZone() {
+        UiObject2 obj = getSelectTimeZoneMenu();
+        if (obj == null) {
+            throw new RuntimeException("Unable to find timezone menu.");
+        }
+        String timeZone = getMenuSummaryText(obj);
+        return timeZone;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isTwentyFourHourFormatEnabled() {
+        UiObject2 twentyFourHourFormatSwitchWidget = getUseTwentyFourHourFormatSwitchWidget();
+        return twentyFourHourFormatSwitchWidget.isChecked();
+    }
+
+    private UiObject2 getSetDateMenu() {
+        return getMenu(
+                getResourceFromConfig(
+                        AutoConfigConstants.SETTINGS,
+                        AutoConfigConstants.DATE_AND_TIME_SETTINGS,
+                        AutoConfigConstants.SET_DATE));
+    }
+
+    private UiObject2 getSetTimeMenu() {
+        return getMenu(
+                getResourceFromConfig(
+                        AutoConfigConstants.SETTINGS,
+                        AutoConfigConstants.DATE_AND_TIME_SETTINGS,
+                        AutoConfigConstants.SET_TIME));
+    }
+
+    private UiObject2 getTwentyFourFormatMenu() {
+        return getMenu(
+                getResourceFromConfig(
+                        AutoConfigConstants.SETTINGS,
+                        AutoConfigConstants.DATE_AND_TIME_SETTINGS,
+                        AutoConfigConstants.USE_24_HOUR_FORMAT));
+    }
+
+    private UiObject2 getSelectTimeZoneMenu() {
+        return getMenu(
+                getResourceFromConfig(
+                        AutoConfigConstants.SETTINGS,
+                        AutoConfigConstants.DATE_AND_TIME_SETTINGS,
+                        AutoConfigConstants.SELECT_TIME_ZONE));
+    }
+
+    private UiObject2 getMenu(BySelector bySelector) {
+        BySelector selector = By.clickable(true).hasDescendant(bySelector);
+        return scrollAndFindUiObject(selector, getScrollScreenIndex());
+    }
+
+    private UiObject2 getAutoDateTimeSwitchWidget() {
+        return getSwitchWidget(
+                getResourceFromConfig(
+                        AutoConfigConstants.SETTINGS,
+                        AutoConfigConstants.DATE_AND_TIME_SETTINGS,
+                        AutoConfigConstants.SET_TIME_AUTOMATICALLY));
+    }
+
+    private UiObject2 getAutoTimeZoneSwitchWidget() {
+        return getSwitchWidget(
+                getResourceFromConfig(
+                        AutoConfigConstants.SETTINGS,
+                        AutoConfigConstants.DATE_AND_TIME_SETTINGS,
+                        AutoConfigConstants.SET_TIME_ZONE_AUTOMATICALLY));
+    }
+
+    private UiObject2 getUseTwentyFourHourFormatSwitchWidget() {
+        return getSwitchWidget(
+                getResourceFromConfig(
+                        AutoConfigConstants.SETTINGS,
+                        AutoConfigConstants.DATE_AND_TIME_SETTINGS,
+                        AutoConfigConstants.USE_24_HOUR_FORMAT));
+    }
+
+    private UiObject2 getSwitchWidget(BySelector bySelector) {
+        BySelector selector = By.hasDescendant(bySelector);
+        UiObject2 object = scrollAndFindUiObject(selector, getScrollScreenIndex());
+        List<UiObject2> list = object.getParent().getChildren();
+        UiObject2 switchWidget = list.get(1).getChildren().get(0);
+        return switchWidget;
+    }
+
+    private int getScrollScreenIndex() {
+        int scrollScreenIndex = 0;
+        if (hasSplitScreenSettingsUI()) {
+            scrollScreenIndex = 1;
+        }
+        return scrollScreenIndex;
+    }
+
+    private String getMenuSummaryText(UiObject2 obj) {
+        return obj.getChildren().get(0).getChildren().get(1).getText();
+    }
+
+    private boolean isAutomaticOn(String name) {
+        ContentResolver cr = mInstrumentation.getContext().getContentResolver();
+        int status = 0;
+        try {
+            status = android.provider.Settings.Global.getInt(cr, name);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+        return status == 1 ? true : false;
+    }
+}
diff --git a/libraries/automotive-helpers/settings-app-helper/src/android/platform/helpers/SettingsSecurityHelperImpl.java b/libraries/automotive-helpers/settings-app-helper/src/android/platform/helpers/SettingsSecurityHelperImpl.java
new file mode 100644
index 0000000..6d82330
--- /dev/null
+++ b/libraries/automotive-helpers/settings-app-helper/src/android/platform/helpers/SettingsSecurityHelperImpl.java
@@ -0,0 +1,204 @@
+package android.platform.helpers;
+
+import android.app.Instrumentation;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiObject2;
+
+import java.util.List;
+
+public class SettingsSecurityHelperImpl extends AbstractAutoStandardAppHelper
+        implements IAutoSecuritySettingsHelper {
+    public SettingsSecurityHelperImpl(Instrumentation instr) {
+        super(instr);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getPackage() {
+        return getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setLockByPassword(String password) {
+        openChooseLockTypeMenu();
+        UiObject2 password_menu =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.SECURITY_SETTINGS,
+                                AutoConfigConstants.LOCK_TYPE_PASSWORD));
+        clickAndWaitForWindowUpdate(
+                getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), password_menu);
+        typePasswordOnTextEditor(password);
+        pressEnter();
+        typePasswordOnTextEditor(password);
+        pressEnter();
+    }
+
+    private void openChooseLockTypeMenu() {
+        List<UiObject2> titles =
+                findUiObjects(
+                        getResourceFromConfig(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.SECURITY_SETTINGS,
+                                AutoConfigConstants.TITLE));
+        if (titles == null || titles.isEmpty()) {
+            throw new RuntimeException("Unable to find Setting title");
+        }
+        UiObject2 title = titles.get(titles.size() - 1);
+        if (title != null
+                && title.getText()
+                        .equalsIgnoreCase(
+                                getResourceValue(
+                                        AutoConfigConstants.SETTINGS,
+                                        AutoConfigConstants.SECURITY_SETTINGS,
+                                        AutoConfigConstants.CHOOSE_LOCK_TYPE))) {
+            // CHOOSE_LOCK_TYPE is already open
+            return;
+        }
+        UiObject2 profileLockMenu =
+                scrollAndFindUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.SECURITY_SETTINGS,
+                                AutoConfigConstants.PROFILE_LOCK));
+        if (profileLockMenu == null) {
+            throw new RuntimeException("Unable to find Choose a lock type menu.");
+        }
+        clickAndWaitForWindowUpdate(
+                getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), profileLockMenu);
+    }
+
+    private void typePasswordOnTextEditor(String password) {
+        UiObject2 textEditor =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.SECURITY_SETTINGS,
+                                AutoConfigConstants.ENTER_PASSWORD));
+        textEditor.setText(password);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setLockByPin(String pin) {
+        openChooseLockTypeMenu();
+        UiObject2 pin_menu =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.SECURITY_SETTINGS,
+                                AutoConfigConstants.LOCK_TYPE_PIN));
+        clickAndWaitForWindowUpdate(
+                getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), pin_menu);
+        selectPinOnPinPad(pin);
+        UiObject2 continue_button =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.SECURITY_SETTINGS,
+                                AutoConfigConstants.CONTINUE_BUTTON));
+        clickAndWaitForWindowUpdate(
+                getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), continue_button);
+        selectPinOnPinPad(pin);
+        UiObject2 confirm_button =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.SECURITY_SETTINGS,
+                                AutoConfigConstants.CONFIRM_BUTTON));
+        clickAndWaitForWindowUpdate(
+                getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), confirm_button);
+    }
+
+    private void selectPinOnPinPad(String pin) {
+        int length = pin.length();
+        for (int i = 0; i < length; i++) {
+            char c = pin.charAt(i);
+            String numberText = "" + c;
+            BySelector number_selector = By.text(numberText);
+            UiObject2 number = findUiObject(number_selector);
+            clickAndWaitForWindowUpdate(
+                    getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), number);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void unlockByPassword(String password) {
+        UiObject2 textEditor =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.SECURITY_SETTINGS,
+                                AutoConfigConstants.ENTER_PASSWORD));
+        textEditor.setText(password);
+        pressEnter();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void unlockByPin(String pin) {
+        selectPinOnPinPad(pin);
+        UiObject2 enter_button =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.SECURITY_SETTINGS,
+                                AutoConfigConstants.ENTER_PIN_BUTTON));
+        clickAndWaitForWindowUpdate(
+                getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), enter_button);
+        UiObject2 pinPad =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.SECURITY_SETTINGS,
+                                AutoConfigConstants.PIN_PAD));
+        if (pinPad != null) {
+            throw new RuntimeException("PIN input is not corrected");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void removeLock() {
+        openChooseLockTypeMenu();
+        UiObject2 none_menu =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.SECURITY_SETTINGS,
+                                AutoConfigConstants.LOCK_TYPE_NONE));
+        clickAndWaitForWindowUpdate(
+                getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), none_menu);
+        UiObject2 remove_button =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.SECURITY_SETTINGS,
+                                AutoConfigConstants.REMOVE_BUTTON));
+        clickAndWaitForWindowUpdate(
+                getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), remove_button);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isDeviceLocked() {
+        openChooseLockTypeMenu();
+        UiObject2 textEditor =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.SECURITY_SETTINGS,
+                                AutoConfigConstants.ENTER_PASSWORD));
+        UiObject2 pinPad =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.SECURITY_SETTINGS,
+                                AutoConfigConstants.PIN_PAD));
+        return textEditor != null || pinPad != null;
+    }
+}
diff --git a/libraries/automotive-helpers/settings-app-helper/src/android/platform/helpers/SettingsSoundsHelperImpl.java b/libraries/automotive-helpers/settings-app-helper/src/android/platform/helpers/SettingsSoundsHelperImpl.java
new file mode 100644
index 0000000..9fce34a
--- /dev/null
+++ b/libraries/automotive-helpers/settings-app-helper/src/android/platform/helpers/SettingsSoundsHelperImpl.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2021 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.helpers;
+
+import static android.media.AudioAttributes.USAGE_ALARM;
+import static android.media.AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE;
+import static android.media.AudioAttributes.USAGE_MEDIA;
+import static android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION;
+
+import android.app.Instrumentation;
+import android.app.UiAutomation;
+import android.car.Car;
+import android.car.media.CarAudioManager;
+import android.content.Context;
+import android.os.SystemClock;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiObject2;
+import androidx.test.InstrumentationRegistry;
+
+import java.util.List;
+
+public class SettingsSoundsHelperImpl extends AbstractAutoStandardAppHelper
+        implements IAutoSoundsSettingHelper {
+    private static final int UI_RESPONSE_WAIT_MS = 5000;
+    private static final int SHORT_UI_RESPONSE_TIME = 1000;
+    private static final int VOLUME_FLAGS = 0;
+    private static final int USAGE_INVALID = -1;
+    private static final int MINIMUM_NUMBER_OF_CHILDREN = 2;
+
+    private Context mContext;
+    private CarAudioManager mCarAudioManager;
+    private final UiAutomation mUiAutomation =
+            InstrumentationRegistry.getInstrumentation().getUiAutomation();
+
+    public SettingsSoundsHelperImpl(Instrumentation instr) {
+        super(instr);
+        mContext = InstrumentationRegistry.getContext();
+        Car car = Car.createCar(mContext);
+        mUiAutomation.adoptShellPermissionIdentity(
+                "android.car.permission.CAR_CONTROL_AUDIO_VOLUME",
+                "android.car.permission.CAR_CONTROL_AUDIO_SETTINGS");
+        mCarAudioManager = (CarAudioManager) car.getCarManager(Car.AUDIO_SERVICE);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getPackage() {
+        return getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setVolume(VolumeType volumeType, int index) {
+        int audioAttribute = USAGE_INVALID;
+        switch (volumeType) {
+            case MEDIA:
+                audioAttribute = USAGE_MEDIA;
+                break;
+            case ALARM:
+                audioAttribute = USAGE_ALARM;
+                break;
+            case NAVIGATION:
+                audioAttribute = USAGE_ASSISTANCE_NAVIGATION_GUIDANCE;
+                break;
+            case INCALL:
+                audioAttribute = USAGE_VOICE_COMMUNICATION;
+                break;
+        }
+        int volumeGroupId = mCarAudioManager.getVolumeGroupIdForUsage(audioAttribute);
+        mCarAudioManager.setGroupVolume(volumeGroupId, index, VOLUME_FLAGS);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int getVolume(VolumeType volumeType) {
+        int audioAttribute = USAGE_INVALID;
+        switch (volumeType) {
+            case MEDIA:
+                audioAttribute = USAGE_MEDIA;
+                break;
+            case ALARM:
+                audioAttribute = USAGE_ALARM;
+                break;
+            case NAVIGATION:
+                audioAttribute = USAGE_ASSISTANCE_NAVIGATION_GUIDANCE;
+                break;
+            case INCALL:
+                audioAttribute = USAGE_VOICE_COMMUNICATION;
+                break;
+        }
+        int volumeGroupId = mCarAudioManager.getVolumeGroupIdForUsage(audioAttribute);
+        int volume = mCarAudioManager.getGroupVolume(volumeGroupId);
+        return volume;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setSound(SoundType soundType, String sound) {
+        String type = "";
+        switch (soundType) {
+            case ALARM:
+                type = "Default alarm sound";
+                break;
+            case NOTIFICATION:
+                type = "Default notification sound";
+                break;
+            case RINGTONE:
+                type = "Phone ringtone";
+                break;
+        }
+        UiObject2 object = scrollAndFindUiObject(By.text(type), getScrollScreenIndex());
+        String currentSound = getSound(soundType);
+        object.click();
+        SystemClock.sleep(SHORT_UI_RESPONSE_TIME);
+        boolean scrollDown = false;
+        if (currentSound.compareToIgnoreCase(sound) < 0) {
+            scrollDown = true;
+        }
+        UiObject2 soundObject = findUiObject(By.text(sound));
+        while (soundObject == null) {
+            if (scrollDown) {
+                scrollDownOnePage(getScrollScreenIndex());
+            } else {
+                scrollUpOnePage(getScrollScreenIndex());
+            }
+            soundObject = findUiObject(By.text(sound));
+        }
+        if (soundObject == null) {
+            throw new RuntimeException(String.format("Unable to find sound %s", sound));
+        }
+        soundObject.click();
+        UiObject2 saveButton =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.SOUND_SETTINGS,
+                                AutoConfigConstants.SAVE_BUTTON));
+        saveButton.click();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getSound(SoundType soundType) {
+        String type = "";
+        switch (soundType) {
+            case ALARM:
+                type = "Default alarm sound";
+                break;
+            case NOTIFICATION:
+                type = "Default notification sound";
+                break;
+            case RINGTONE:
+                type = "Phone ringtone";
+                break;
+        }
+        UiObject2 object = scrollAndFindUiObject(By.text(type), getScrollScreenIndex());
+        List<UiObject2> list = object.getParent().getChildren();
+        if (list.size() < 2) {
+            scrollDownOnePage(1);
+            list = object.getParent().getChildren();
+        }
+        UiObject2 summary = list.get(1);
+        return summary.getText();
+    }
+
+    private int getScrollScreenIndex() {
+        int scrollScreenIndex = 0;
+        if (hasSplitScreenSettingsUI()) {
+            scrollScreenIndex = 1;
+        }
+        return scrollScreenIndex;
+    }
+}
diff --git a/libraries/automotive-helpers/settings-app-helper/src/android/platform/helpers/SettingsSystemHelperImpl.java b/libraries/automotive-helpers/settings-app-helper/src/android/platform/helpers/SettingsSystemHelperImpl.java
new file mode 100644
index 0000000..a7ddc5f
--- /dev/null
+++ b/libraries/automotive-helpers/settings-app-helper/src/android/platform/helpers/SettingsSystemHelperImpl.java
@@ -0,0 +1,326 @@
+/*
+ * Copyright (C) 2021 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.helpers;
+
+import android.app.Instrumentation;
+import android.content.res.Resources;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.UiScrollable;
+import android.support.test.uiautomator.UiSelector;
+
+import java.text.SimpleDateFormat;
+
+import java.time.Month;
+import java.util.Date;
+
+public class SettingsSystemHelperImpl extends AbstractAutoStandardAppHelper
+        implements IAutoSystemSettingsHelper {
+    public SettingsSystemHelperImpl(Instrumentation instr) {
+        super(instr);
+    }
+    /** {@inheritDoc} */
+    @Override
+    public String getPackage() {
+        return getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setDisplayLanguage(String language) {
+        openLanguageMenu();
+        BySelector languageSelector = By.clickable(true).hasDescendant(By.textStartsWith(language));
+        UiObject2 languageObject = getMenu(languageSelector);
+        clickAndWaitForWindowUpdate(
+                getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), languageObject);
+        String systemLanguage =
+                Resources.getSystem().getConfiguration().getLocales().get(0).getDisplayLanguage();
+        if (!language.toLowerCase().contains(systemLanguage.toLowerCase())) {
+            throw new RuntimeException("System language is different from selected language");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getCurrentLanguage() {
+        UiObject2 object =
+                getBtnByText(
+                        getResourceValue(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.SYSTEM_SETTINGS,
+                                AutoConfigConstants.LANGUAGES_MENU),
+                        getResourceValue(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.SYSTEM_SETTINGS,
+                                AutoConfigConstants.LANGUAGES_MENU_IN_SELECTED_LANGUAGE));
+        String currentLanguage = getSummeryText(object);
+        return currentLanguage;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getDeviceModel() {
+        openAboutMenu();
+        UiObject2 object =
+                getMenu(
+                        getResourceFromConfig(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.SYSTEM_SETTINGS,
+                                AutoConfigConstants.DEVICE_MODEL));
+        String modelName = getSummeryText(object);
+        return modelName;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getAndroidVersion() {
+        openAboutMenu();
+        UiObject2 object =
+                getMenu(
+                        getResourceFromConfig(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.SYSTEM_SETTINGS,
+                                AutoConfigConstants.ANDROID_VERSION));
+        String androidVersion = getSummeryText(object);
+        return androidVersion;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Date getAndroidSecurityPatchLevel() {
+        openAboutMenu();
+        UiObject2 object =
+                getMenu(
+                        getResourceFromConfig(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.SYSTEM_SETTINGS,
+                                AutoConfigConstants.ANDROID_SECURITY_PATCH_LEVEL));
+        String androidSecurityPatchLevel = getSummeryText(object);
+        Date patchDate = parseDate(androidSecurityPatchLevel, "MMMM dd, yyyy");
+        if (patchDate == null) {
+            patchDate = parseDate(androidSecurityPatchLevel, "dd MMMM yyyy");
+        }
+        if (patchDate == null) {
+            throw new RuntimeException("Cannot find date from UI");
+        }
+        return formatDate(patchDate, "MMMM dd, yyyy"); // return locale independent date
+    }
+
+    private Date formatDate(Date date, String format) {
+        SimpleDateFormat dateFormatter = new SimpleDateFormat(format);
+        String dateString = dateFormatter.format(date);
+        String[] arr = dateString.split(" ");
+        int year = Integer.valueOf(arr[2]);
+        int month = Month.valueOf(arr[0].toUpperCase()).getValue() - 1;
+        int day = Integer.valueOf(arr[1].substring(0, arr[1].length() - 1));
+        return new Date(year, month, day);
+    }
+
+    private Date parseDate(String date, String format) {
+        Date parsedDate = null;
+        try {
+            SimpleDateFormat dateFormatter = new SimpleDateFormat(format);
+            parsedDate = dateFormatter.parse(date);
+        } catch (Exception e) {
+            // do nothing
+        }
+        return parsedDate;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getKernelVersion() {
+        openAboutMenu();
+        UiObject2 object =
+                getMenu(
+                        getResourceFromConfig(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.SYSTEM_SETTINGS,
+                                AutoConfigConstants.KERNEL_VERSION));
+        String kernelVersion = getSummeryText(object);
+        return kernelVersion;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getBuildNumber() {
+        openAboutMenu();
+        UiObject2 object =
+                getMenu(
+                        getResourceFromConfig(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.SYSTEM_SETTINGS,
+                                AutoConfigConstants.BUILD_NUMBER));
+        String buildNumber = getSummeryText(object);
+        return buildNumber;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void resetNetwork() {
+        openResetOptionsMenu();
+        BySelector resetNetworkSelector =
+                By.clickable(true)
+                        .hasDescendant(
+                                getResourceFromConfig(
+                                        AutoConfigConstants.SETTINGS,
+                                        AutoConfigConstants.SYSTEM_SETTINGS,
+                                        AutoConfigConstants.RESET_NETWORK));
+        UiObject2 resetNetworkMenu = getMenu(resetNetworkSelector);
+        clickAndWaitForWindowUpdate(
+                getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), resetNetworkMenu);
+        BySelector resetSettingsSelector =
+                getResourceFromConfig(
+                        AutoConfigConstants.SETTINGS,
+                        AutoConfigConstants.SYSTEM_SETTINGS,
+                        AutoConfigConstants.RESET_SETTINGS);
+        UiObject2 resetSettingsButton1 = getMenu(resetSettingsSelector);
+        clickAndWaitForWindowUpdate(
+                getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), resetSettingsButton1);
+        UiObject2 resetSettingsButton2 =
+                getMenu(
+                        getResourceFromConfig(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.SYSTEM_SETTINGS,
+                                AutoConfigConstants.RESET_SETTINGS));
+        clickAndWaitForWindowUpdate(
+                getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), resetSettingsButton2);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void resetAppPreferences() {
+        openResetOptionsMenu();
+        BySelector selector =
+                By.clickable(true)
+                        .hasDescendant(
+                                getResourceFromConfig(
+                                        AutoConfigConstants.SETTINGS,
+                                        AutoConfigConstants.SYSTEM_SETTINGS,
+                                        AutoConfigConstants.RESET_APP_PREFERENCES));
+        UiObject2 object = getMenu(selector);
+        clickAndWaitForWindowUpdate(
+                getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), object);
+        BySelector reset_apps_selector =
+                getResourceFromConfig(
+                        AutoConfigConstants.SETTINGS,
+                        AutoConfigConstants.SYSTEM_SETTINGS,
+                        AutoConfigConstants.RESET_APPS);
+        UiObject2 reset_apps_button = getMenu(reset_apps_selector);
+        clickAndWaitForWindowUpdate(
+                getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), reset_apps_button);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void openLanguagesInputMenu() {
+        UiObject2 languagesInputMenu =
+                getMenu(
+                        getResourceFromConfig(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.SYSTEM_SETTINGS,
+                                AutoConfigConstants.LANGUAGES_AND_INPUT_MENU));
+        clickAndWaitForWindowUpdate(
+                getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), languagesInputMenu);
+    }
+
+    private String getSummeryText(UiObject2 object) {
+        UiObject2 parent = object.getParent();
+        if (parent.getChildren().size() < 2) {
+            UiSelector uiSelector =
+                    new UiSelector()
+                            .className(
+                                    getResourceValue(
+                                            AutoConfigConstants.SETTINGS,
+                                            AutoConfigConstants.SYSTEM_SETTINGS,
+                                            AutoConfigConstants.RECYCLER_VIEW_WIDGET));
+            UiScrollable scrollable = new UiScrollable(uiSelector);
+            try {
+                scrollable.scrollForward();
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            }
+        }
+        return object.getParent().getChildren().get(1).getText();
+    }
+
+    private void openResetOptionsMenu() {
+        UiObject2 resetOptionMenu =
+                getMenu(
+                        getResourceFromConfig(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.SYSTEM_SETTINGS,
+                                AutoConfigConstants.RESET_OPTIONS_MENU));
+        clickAndWaitForWindowUpdate(
+                getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), resetOptionMenu);
+    }
+
+    private void openAboutMenu() {
+        UiObject2 aboutMenu =
+                getMenu(
+                        getResourceFromConfig(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.SYSTEM_SETTINGS,
+                                AutoConfigConstants.ABOUT_MENU));
+        clickAndWaitForWindowUpdate(
+                getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), aboutMenu);
+    }
+
+    private void openLanguageMenu() {
+        UiObject2 languageMenu =
+                getBtnByText(
+                        getResourceValue(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.SYSTEM_SETTINGS,
+                                AutoConfigConstants.LANGUAGES_MENU),
+                        getResourceValue(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.SYSTEM_SETTINGS,
+                                AutoConfigConstants.LANGUAGES_MENU_IN_SELECTED_LANGUAGE));
+        clickAndWaitForWindowUpdate(
+                getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), languageMenu);
+    }
+
+    private UiObject2 getBtnByText(String... texts) {
+        for (String text : texts) {
+            BySelector btnSelector = By.text(text);
+            UiObject2 btn = findUiObject(btnSelector);
+            if (btn != null) {
+                return btn;
+            }
+        }
+        throw new RuntimeException("Cannot find button");
+    }
+
+    private UiObject2 getMenu(BySelector selector) {
+        UiObject2 object = scrollAndFindUiObject(selector, getScrollScreenIndex());
+        if (object == null) {
+            throw new RuntimeException(
+                    String.format("Unable to find UI Elemenet %s.", selector.toString()));
+        }
+        return object;
+    }
+
+    private int getScrollScreenIndex() {
+        int scrollScreenIndex = 0;
+        if (hasSplitScreenSettingsUI()) {
+            scrollScreenIndex = 1;
+        }
+        return scrollScreenIndex;
+    }
+}
diff --git a/libraries/automotive-helpers/standard-app-helper/Android.bp b/libraries/automotive-helpers/standard-app-helper/Android.bp
new file mode 100644
index 0000000..3c2d8d9
--- /dev/null
+++ b/libraries/automotive-helpers/standard-app-helper/Android.bp
@@ -0,0 +1,35 @@
+//
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+    name: "automotive-standard-app-helper",
+    libs: [
+        "ub-uiautomator",
+        "androidx.test.runner",
+        "app-helpers-core",
+    ],
+    static_libs: [
+        "automotive-utility-helper",
+    ],
+    srcs: ["src/**/*.java"],
+    sdk_version: "test_current",
+}
+
+//#####################################
diff --git a/libraries/automotive-helpers/standard-app-helper/src/android/platform/helpers/AbstractAutoStandardAppHelper.java b/libraries/automotive-helpers/standard-app-helper/src/android/platform/helpers/AbstractAutoStandardAppHelper.java
new file mode 100644
index 0000000..9d01b3c
--- /dev/null
+++ b/libraries/automotive-helpers/standard-app-helper/src/android/platform/helpers/AbstractAutoStandardAppHelper.java
@@ -0,0 +1,489 @@
+/*
+ * Copyright (C) 2021 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.helpers;
+
+import android.app.Instrumentation;
+import android.content.ActivityNotFoundException;
+import android.os.SystemClock;
+import android.util.Log;
+
+import android.graphics.Rect;
+
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.regex.Pattern;
+
+public abstract class AbstractAutoStandardAppHelper extends AbstractStandardAppHelper {
+    private static final String LOG_TAG = AbstractAutoStandardAppHelper.class.getSimpleName();
+
+    protected Instrumentation mInstrumentation;
+    protected UiDevice mDevice;
+
+    private AutoJsonUtility mAutoJsonUtil;
+
+    private static final int UI_RESPONSE_WAIT_MS = 5000;
+    private static final float DEFAULT_SCROLL_PERCENT = 100f;
+    private static final int DEFAULT_SCROLL_TIME_MS = 500;
+
+    private static final int MAX_SCROLLS = 5;
+
+    public AbstractAutoStandardAppHelper(Instrumentation instrumentation) {
+        super(instrumentation);
+        mInstrumentation = instrumentation;
+        mDevice = UiDevice.getInstance(instrumentation);
+        mAutoJsonUtil = AutoJsonUtility.getInstance();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void open() {
+        // Launch the application as normal.
+        String pkg = getPackage();
+
+        String output = null;
+        try {
+            Log.i(LOG_TAG, String.format("Sending command to launch: %s", pkg));
+            mInstrumentation.getContext().startActivity(getOpenAppIntent());
+        } catch (ActivityNotFoundException e) {
+            throw new RuntimeException(String.format("Failed to find package: %s", pkg), e);
+        }
+
+        // Ensure the package is in the foreground for success.
+        if (!mDevice.wait(Until.hasObject(By.pkg(pkg).depth(0)), 30000)) {
+            throw new IllegalStateException(
+                    String.format("Did not find package, %s, in foreground.", pkg));
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void exit() {
+        pressHome();
+        waitForIdle();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void dismissInitialDialogs() {
+        // Nothing to dismiss
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getLauncherName() {
+        throw new UnsupportedOperationException("Operation not supported.");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getPackage() {
+        throw new UnsupportedOperationException("Operation not supported.");
+    }
+
+    /**
+     * Executes a shell command on device, and return the standard output in string.
+     *
+     * @param command the command to run
+     * @return the standard output of the command, or empty string if failed without throwing an
+     *     IOException
+     */
+    protected String executeShellCommand(String command) {
+        try {
+            return mDevice.executeShellCommand(command);
+        } catch (IOException e) {
+            // ignore
+            Log.e(
+                    LOG_TAG,
+                    String.format(
+                            "The shell command failed to run: %s exception: %s",
+                            command, e.getMessage()));
+            return "";
+        }
+    }
+
+    /** Press Home Button on the Device */
+    protected void pressHome() {
+        mDevice.pressHome();
+    }
+
+    /** Press Back Button on the Device */
+    protected void pressBack() {
+        mDevice.pressBack();
+    }
+
+    /** Press Enter Button on the Device */
+    protected void pressEnter() {
+        mDevice.pressEnter();
+    }
+
+    /** Press power button */
+    protected void pressPowerButton() {
+        executeShellCommand("input keyevent KEYCODE_POWER");
+        SystemClock.sleep(UI_RESPONSE_WAIT_MS);
+    }
+
+    /** Wait for the device to be idle */
+    protected void waitForIdle() {
+        mDevice.waitForIdle();
+    }
+
+    /** Wait for the given selector to be gone */
+    protected void waitForGone(BySelector selector) {
+        mDevice.wait(Until.gone(selector), UI_RESPONSE_WAIT_MS);
+    }
+
+    /** Wait for window change on the device */
+    protected void waitForWindowUpdate(String applicationPackage) {
+        mDevice.waitForWindowUpdate(applicationPackage, UI_RESPONSE_WAIT_MS);
+    }
+
+    /**
+     * Scroll in given direction by specified percent of the whole scrollable region in given time.
+     *
+     * @param direction The direction in which to perform scrolling, it's either up or down.
+     * @param percent The percentage of the whole scrollable region by which to scroll, ranging from
+     *     0 - 100. For instance, percent = 50 would scroll up/down by half of the screen.
+     * @param timeMs The duration in milliseconds to perform the scrolling gesture.
+     * @param index Index required for split screen.
+     */
+    private boolean scroll(Direction direction, float percent, long timeMs, int index) {
+        boolean canScrollMoreInGivenDircetion = false;
+        List<UiObject2> upButtons =
+                findUiObjects(
+                        getResourceFromConfig(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.FULL_SETTINGS,
+                                AutoConfigConstants.UP_BUTTON));
+        List<UiObject2> downButtons =
+                findUiObjects(
+                        getResourceFromConfig(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.FULL_SETTINGS,
+                                AutoConfigConstants.DOWN_BUTTON));
+        List<UiObject2> scrollableObjects = findUiObjects(By.scrollable(true));
+        if (scrollableObjects == null || upButtons == null || scrollableObjects.size() == 0) {
+            return canScrollMoreInGivenDircetion;
+        }
+        if (upButtons.size() == 1 || (scrollableObjects.size() - 1) < index) {
+            // reset index as it is invalid
+            index = 0;
+        }
+        if (upButtons != null) {
+            UiObject2 upButton = upButtons.get(index);
+            UiObject2 downButton = downButtons.get(index);
+            if (direction == Direction.UP) {
+                clickAndWaitForIdleScreen(upButton);
+            } else if (direction == Direction.DOWN) {
+                clickAndWaitForIdleScreen(downButton);
+            }
+        } else {
+            UiObject2 scrollable = scrollableObjects.get(index);
+            if (scrollable != null) {
+                scrollable.setGestureMargins(
+                        getScrollableMargin(scrollable, false), // left
+                        getScrollableMargin(scrollable, true), // top
+                        getScrollableMargin(scrollable, false), // right
+                        getScrollableMargin(scrollable, true)); // bottom
+                int scrollSpeed = getScrollSpeed(scrollable, timeMs);
+                canScrollMoreInGivenDircetion =
+                        scrollable.scroll(direction, percent / 100, scrollSpeed);
+            }
+        }
+        return canScrollMoreInGivenDircetion;
+    }
+
+    /**
+     * Return the margin for scrolling.
+     *
+     * @param scrollable The given scrollable object to scroll through.
+     * @param isVertical If true, then vertical else horizontal
+     */
+    private int getScrollableMargin(UiObject2 scrollable, boolean isVertical) {
+        Rect bounds = scrollable.getVisibleBounds();
+        int margin = (int) (Math.abs(bounds.width()) / 4);
+        if (isVertical) {
+            margin = (int) (Math.abs(bounds.height()) / 4);
+        }
+        return margin;
+    }
+
+    /**
+     * Return the scroll speed such that it takes given time for the device to scroll through the
+     * whole scrollable region(i.e. from the top of the scrollable region to bottom).
+     *
+     * @param scrollable The given scrollable object to scroll through.
+     * @param timeMs The duration in milliseconds to perform the scrolling gesture.
+     */
+    private int getScrollSpeed(UiObject2 scrollable, long timeMs) {
+        Rect bounds = scrollable.getVisibleBounds();
+        double timeSeconds = (double) timeMs / 1000;
+        int scrollSpeed = (int) (bounds.height() / timeSeconds);
+        return scrollSpeed;
+    }
+
+    /**
+     * Scroll down from the top of the scrollable region to bottom of the scrollable region (i.e. by
+     * one page).
+     */
+    public boolean scrollDownOnePage() {
+        return scrollDownOnePage(0);
+    }
+
+    /**
+     * Scroll down from the top of the scrollable region to bottom of the scrollable region (i.e. by
+     * one page). Index - required for split screen
+     */
+    protected boolean scrollDownOnePage(int index) {
+        return scroll(Direction.DOWN, DEFAULT_SCROLL_PERCENT, DEFAULT_SCROLL_TIME_MS, index);
+    }
+
+    /**
+     * Scroll up from the bottom of the scrollable region to top of the scrollable region (i.e. by
+     * one page).
+     */
+    public boolean scrollUpOnePage() {
+        return scrollUpOnePage(0);
+    }
+
+    /**
+     * Scroll up from the bottom of the scrollable region to top of the scrollable region (i.e. by
+     * one page). Index - required for split screen
+     */
+    protected boolean scrollUpOnePage(int index) {
+        return scroll(Direction.UP, DEFAULT_SCROLL_PERCENT, DEFAULT_SCROLL_TIME_MS, index);
+    }
+
+    /** Find UI Object in a Scrollable view */
+    protected UiObject2 scrollAndFindUiObject(BySelector selector) {
+        return scrollAndFindUiObject(selector, 0);
+    }
+
+    /** Find UI Object in a Scrollable view with Index ( required for split screen ) */
+    protected UiObject2 scrollAndFindUiObject(BySelector selector, int index) {
+        if (selector == null) {
+            return null;
+        }
+        // Find the object on current page
+        UiObject2 uiObject = findUiObject(selector);
+        if (uiObject != null) {
+            return uiObject;
+        }
+        // Scroll To Top
+        scrollToTop(index);
+        // Find UI Object on the first page
+        uiObject = findUiObject(selector);
+        // Try finding UI Object until it's found or all the pages are checked
+        int scrollCount = 0;
+        while (uiObject == null && scrollCount < MAX_SCROLLS) {
+            // Scroll down to next page
+            scrollDownOnePage(index);
+
+            // Find UI Object
+            uiObject = findUiObject(selector);
+
+            scrollCount++;
+        }
+        return uiObject;
+    }
+
+    /** Scroll to top of the scrollable region. */
+    protected void scrollToTop() {
+        scrollToTop(0);
+    }
+
+    /** Scroll to top of the scrollable region with index. ( Required for Split Screen ) */
+    protected void scrollToTop(int index) {
+        int scrollCount = 0;
+        while (scrollCount < MAX_SCROLLS) {
+            scrollUpOnePage(index);
+            scrollCount++;
+        }
+    }
+
+    /** Find the UI Object that matches the given selector */
+    protected UiObject2 findUiObject(BySelector selector) {
+        if (selector == null) {
+            return null;
+        }
+        UiObject2 uiObject = mDevice.wait(Until.findObject(selector), UI_RESPONSE_WAIT_MS);
+        return uiObject;
+    }
+
+    /** Find the list of UI object that matches the given selector */
+    protected List<UiObject2> findUiObjects(BySelector selector) {
+        if (selector == null) {
+            return null;
+        }
+        List<UiObject2> uiObjects = mDevice.wait(Until.findObjects(selector), UI_RESPONSE_WAIT_MS);
+        return uiObjects;
+    }
+
+    /** Find the list of UI object that matches the given selector for given depth */
+    protected List<UiObject2> findUiObjects(BySelector selector, int depth) {
+        if (selector == null) {
+            return null;
+        }
+        List<UiObject2> uiObjects =
+                mDevice.wait(Until.findObjects(selector.maxDepth(depth)), UI_RESPONSE_WAIT_MS);
+        return uiObjects;
+    }
+
+    /**
+     * This method is used to click on an UiObject2 and wait for device idle after click.
+     *
+     * @param uiObject UiObject2 to click.
+     */
+    protected void clickAndWaitForIdleScreen(UiObject2 uiObject2) {
+        uiObject2.click();
+        waitForIdle();
+    }
+
+    /**
+     * This method is used to click on an UiObject2 and wait for window update.
+     *
+     * @param appPackage application package for window update
+     * @param uiObject2 UiObject2 to click.
+     */
+    protected void clickAndWaitForWindowUpdate(String appPackage, UiObject2 uiObject2) {
+        uiObject2.click();
+        waitForWindowUpdate(appPackage);
+        waitForIdle();
+    }
+
+    /**
+     * This method is used to click on an UiObject2 and wait until it is gone
+     *
+     * @param uiObject2 uiObject to be clicked
+     * @param selector BySelector to be gone
+     */
+    protected void clickAndWaitForGone(UiObject2 uiObject2, BySelector selector) {
+        uiObject2.click();
+        waitForGone(selector);
+    }
+
+    /**
+     * This method is used check if given object is visible on the device screen
+     *
+     * @param selector BySelector to be gone
+     */
+    protected boolean hasUiObject(BySelector selector) {
+        return mDevice.hasObject(selector);
+    }
+
+    /** Get path for the given setting */
+    protected String[] getSettingPath(String setting) {
+        return mAutoJsonUtil.getSettingPath(setting);
+    }
+
+    /** Get available options for given settings */
+    protected String[] getSettingOptions(String setting) {
+        return mAutoJsonUtil.getSettingOptions(setting);
+    }
+
+    /** Get application config value for given configuration */
+    protected String getApplicationConfig(String config) {
+        return mAutoJsonUtil.getApplicationConfig(config);
+    }
+
+    /** Get resource for given configuration resource in given application */
+    protected BySelector getResourceFromConfig(
+            String appName, String appConfig, String appResource) {
+        AutoConfigResource configResource =
+                mAutoJsonUtil.getResourceFromConfig(appName, appConfig, appResource);
+
+        // RESOURCE_ID
+        if (configResource != null
+                && AutoConfigConstants.RESOURCE_ID.equals(configResource.getResourceType())) {
+            return By.res(configResource.getResourcePackage(), configResource.getResourceValue());
+        }
+
+        // TEXT
+        if (configResource != null
+                && AutoConfigConstants.TEXT.equals(configResource.getResourceType())) {
+            return By.text(
+                    Pattern.compile(configResource.getResourceValue(), Pattern.CASE_INSENSITIVE));
+        }
+
+        // TEXT_CONTAINS
+        if (configResource != null
+                && AutoConfigConstants.TEXT_CONTAINS.equals(configResource.getResourceType())) {
+            return By.textContains(configResource.getResourceValue());
+        }
+
+        // DESCRIPTION
+        if (configResource != null
+                && AutoConfigConstants.DESCRIPTION.equals(configResource.getResourceType())) {
+            return By.desc(
+                    Pattern.compile(configResource.getResourceValue(), Pattern.CASE_INSENSITIVE));
+        }
+
+        // CLASS
+        if (configResource != null
+                && AutoConfigConstants.CLASS.equals(configResource.getResourceType())) {
+            if (configResource.getResourcePackage() != null
+                    && !configResource.getResourcePackage().isEmpty()) {
+                return By.clazz(
+                        configResource.getResourcePackage(), configResource.getResourceValue());
+            }
+            return By.clazz(configResource.getResourceValue());
+        }
+
+        return null;
+    }
+
+    /** Get resource value for given configuration resource in given application */
+    protected String getResourceValue(String appName, String appConfig, String appResource) {
+        AutoConfigResource configResource =
+                mAutoJsonUtil.getResourceFromConfig(appName, appConfig, appResource);
+
+        if (configResource != null) {
+            return configResource.getResourceValue();
+        }
+
+        return null;
+    }
+
+    /** Get resource package for given configuration resource in given application */
+    protected String getResourcePackage(String appName, String appConfig, String appResource) {
+        AutoConfigResource configResource =
+                mAutoJsonUtil.getResourceFromConfig(appName, appConfig, appResource);
+
+        if (configResource != null) {
+            return configResource.getResourcePackage();
+        }
+
+        return null;
+    }
+
+    /** Check for Split Screen UI in Settings Application */
+    protected boolean hasSplitScreenSettingsUI() {
+        boolean isSplitScreen = false;
+        if ("TRUE"
+                .equalsIgnoreCase(
+                        mAutoJsonUtil.getApplicationConfig(AutoConfigConstants.SPLIT_SCREEN_UI))) {
+            isSplitScreen = true;
+        }
+        return isSplitScreen;
+    }
+}
diff --git a/libraries/automotive-helpers/utility-helper/Android.bp b/libraries/automotive-helpers/utility-helper/Android.bp
new file mode 100644
index 0000000..a708628
--- /dev/null
+++ b/libraries/automotive-helpers/utility-helper/Android.bp
@@ -0,0 +1,29 @@
+//
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+    name: "automotive-utility-helper",
+    libs: ["ub-uiautomator"],
+    static_libs: [
+        "androidx.test.runner",
+    ],
+    srcs: ["src/**/*.java"],
+    sdk_version: "test_current",
+}
diff --git a/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoAppGridConfigUtility.java b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoAppGridConfigUtility.java
new file mode 100644
index 0000000..a77dc39
--- /dev/null
+++ b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoAppGridConfigUtility.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2021 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.helpers;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/** Configuration for App Grid */
+public class AutoAppGridConfigUtility implements IAutoConfigUtility {
+
+    private static final String LOG_TAG = AutoAppGridConfigUtility.class.getSimpleName();
+
+    // App Grid Config
+    private static final String APP_GRID_PACKAGE = "com.android.car.carlauncher";
+    private static final String APP_GRID_ACTIVITY = "com.android.car.carlauncher/.AppGridActivity";
+    private static final String OPEN_APP_GRID_COMMAND =
+            "am start -n com.android.car.carlauncher/.AppGridActivity";
+
+    // Config Map
+    private Map<String, AutoConfiguration> mAppGridConfigMap;
+
+    private static AutoAppGridConfigUtility sAutoAppGridConfigInstance = null;
+
+    private AutoAppGridConfigUtility() {
+        // Initialize App Grid Config Map
+        mAppGridConfigMap = new HashMap<String, AutoConfiguration>();
+    }
+
+    /** Get instance of Auto App Grid Utility */
+    public static IAutoConfigUtility getInstance() {
+        if (sAutoAppGridConfigInstance == null) {
+            sAutoAppGridConfigInstance = new AutoAppGridConfigUtility();
+        }
+        return sAutoAppGridConfigInstance;
+    }
+
+    /**
+     * Get Path for given menu
+     *
+     * @param menu Menu Name
+     */
+    public String[] getPath(String menu) {
+        throw new UnsupportedOperationException("Operation not supported for App Grid");
+    }
+
+    /**
+     * Add path for given Menu
+     *
+     * @param menu Menu Name
+     * @param path Path
+     */
+    public void addPath(String menu, String[] path) {
+        throw new UnsupportedOperationException("Operation not supported for App Grid");
+    }
+
+    /**
+     * Get available menu option
+     *
+     * @param menu Menu Name
+     */
+    public String[] getAvailableOptions(String menu) {
+        throw new UnsupportedOperationException("Operation not supported for App Grid");
+    }
+
+    /**
+     * Add available menu options
+     *
+     * @param menu Menu Name
+     * @param options Available Options
+     */
+    public void addAvailableOptions(String menu, String[] options) {
+        throw new UnsupportedOperationException("Operation not supported for App Grid");
+    }
+
+    /**
+     * Get App Grid Config Resource
+     *
+     * @param configName Configuration Name
+     * @param resourceName Resource Name
+     */
+    public AutoConfigResource getResourceConfiguration(String configName, String resourceName) {
+        if (mAppGridConfigMap.get(configName) == null) {
+            // Unknown Configuration
+            return null;
+        }
+        return mAppGridConfigMap.get(configName).getResource(resourceName);
+    }
+
+    /**
+     * Validate App Grid Configuration
+     *
+     * @param configName Configuration Name
+     */
+    public boolean isValidConfiguration(String configName) {
+        return (mAppGridConfigMap.get(configName) != null);
+    }
+
+    /**
+     * Validate App Grid Configuration Resource
+     *
+     * @param configName Configuration Name
+     * @param resourceName Resource Name
+     */
+    public boolean isValidResource(String configName, String resourceName) {
+        if (mAppGridConfigMap.get(configName) == null) {
+            return false;
+        }
+        return (mAppGridConfigMap.get(configName).getResource(resourceName) != null);
+    }
+
+    /**
+     * Validate Menu Options
+     *
+     * @param menu Menu Name
+     */
+    public boolean isValidOption(String menu) {
+        throw new UnsupportedOperationException("Operation not supported for App Grid Application");
+    }
+
+    /**
+     * Validate Menu Path
+     *
+     * @param menu Menu Name
+     */
+    public boolean isValidPath(String menu) {
+        throw new UnsupportedOperationException("Operation not supported for App Grid Application");
+    }
+
+    /** Load default configuration for App Grid application */
+    public void loadDefaultConfig(Map<String, String> mApplicationConfigMap) {
+        // Default App Grid Application Config
+        loadDefaultAppGridConfig(mApplicationConfigMap);
+        // Default App Grid view Config
+        loadDefaultAppGridViewConfig(mAppGridConfigMap);
+    }
+
+    private void loadDefaultAppGridConfig(Map<String, String> mApplicationConfigMap) {
+        // Add default App Grid package
+        mApplicationConfigMap.put(AutoConfigConstants.APP_GRID_PACKAGE, APP_GRID_PACKAGE);
+        // Add default App Grid activity
+        mApplicationConfigMap.put(AutoConfigConstants.APP_GRID_ACTIVITY, APP_GRID_ACTIVITY);
+        // Add default command to open app grid
+        mApplicationConfigMap.put(AutoConfigConstants.OPEN_APP_GRID_COMMAND, OPEN_APP_GRID_COMMAND);
+    }
+
+    private void loadDefaultAppGridViewConfig(
+            Map<String, AutoConfiguration> mApplicationConfigMap) {
+        AutoConfiguration appGridViewConfiguration = new AutoConfiguration();
+        appGridViewConfiguration.addResource(
+                AutoConfigConstants.APP_GRID_VIEW_ID,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "apps_grid", APP_GRID_PACKAGE));
+        appGridViewConfiguration.addResource(
+                AutoConfigConstants.APPLICATION_NAME,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "app_name", APP_GRID_PACKAGE));
+        mAppGridConfigMap.put(AutoConfigConstants.APP_GRID_VIEW, appGridViewConfiguration);
+    }
+}
diff --git a/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoConfigConstants.java b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoConfigConstants.java
new file mode 100644
index 0000000..da44880
--- /dev/null
+++ b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoConfigConstants.java
@@ -0,0 +1,248 @@
+/*
+ * Copyright (C) 2021 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.helpers;
+
+// Automotive Configuration Constants
+public class AutoConfigConstants {
+    // Resource Types
+    public static final String RESOURCE_ID = "RESOURCE_ID";
+    public static final String TEXT = "TEXT";
+    public static final String TEXT_CONTAINS = "TEXT_CONTAINS";
+    public static final String DESCRIPTION = "DESCRIPTION";
+    public static final String CLASS = "CLASS";
+
+    // App Grid
+    public static final String APP_GRID = "APP_GRID";
+    public static final String APP_GRID_ACTIVITY = "APP_GRID_ACTIVITY";
+    public static final String APP_GRID_PACKAGE = "APP_GRID_PACKAGE";
+    public static final String APP_GRID_VIEW = "APP_GRID_VIEW";
+    public static final String APP_GRID_VIEW_ID = "APP_GRID_VIEW_ID";
+    public static final String APPLICATION_NAME = "APPLICATION_NAME";
+    public static final String OPEN_APP_GRID_COMMAND = "OPEN_APP_GRID_COMMAND";
+
+    // Home
+    public static final String BOTTOM_CARD = "BOTTOM_CARD";
+    public static final String HOME = "HOME";
+    public static final String HOME_PACKAGE = "HOME_PACKAGE";
+    public static final String HOME_VIEW = "HOME_VIEW";
+    public static final String MAP_CARD = "MAP_CARD";
+    public static final String TOP_CARD = "TOP_CARD";
+
+    // Lock Screen
+    public static final String LOCK_SCREEN = "LOCK_SCREEN";
+    public static final String LOCK_SCREEN_PACKAGE = "LOCK_SCREEN_PACKAGE";
+    public static final String LOCK_SCREEN_VIEW = "LOCK_SCREEN_VIEW";
+    public static final String ENTER_KEY = "ENTER_KEY";
+
+    // SETTINGS
+    public static final String SETTINGS = "SETTINGS";
+    public static final String SETTINGS_TITLE_TEXT = "SETTINGS_TITLE_TEXT";
+    public static final String SETTINGS_PACKAGE = "SETTINGS_PACKAGE";
+    public static final String SETTINGS_RRO_PACKAGE = "SETTINGS_RRO_PACKAGE";
+    public static final String OPEN_SETTINGS_COMMAND = "OPEN_SETTINGS_COMMAND";
+    public static final String OPEN_QUICK_SETTINGS_COMMAND = "OPEN_QUICK_SETTINGS_COMMAND";
+    public static final String SPLIT_SCREEN_UI = "SPLIT_SCREEN_UI";
+    // Full Settings
+    public static final String FULL_SETTINGS = "FULL_SETTINGS";
+    public static final String PAGE_TITLE = "PAGE_TITLE";
+    public static final String SEARCH = "SEARCH";
+    public static final String SEARCH_BOX = "SEARCH_BOX";
+    public static final String SEARCH_RESULTS = "SEARCH_RESULTS";
+    public static final String UP_BUTTON = "UP_BUTTON";
+    public static final String DOWN_BUTTON = "DOWN_BUTTON";
+    // Quick Settings
+    public static final String QUICK_SETTINGS = "QUICK_SETTINGS";
+    public static final String OPEN_MORE_SETTINGS = "OPEN_MORE_SETTINGS";
+    public static final String NIGHT_MODE = "NIGHT_MODE";
+    // Display
+    public static final String DISPLAY_SETTINGS = "DISPLAY";
+    public static final String BRIGHTNESS_LEVEL = "BRIGHTNESS_LEVEL";
+    // Sound
+    public static final String SOUND_SETTINGS = "SOUND";
+    public static final String SAVE_BUTTON = "SAVE";
+    // Network and Internet
+    public static final String NETWORK_AND_INTERNET_SETTINGS = "NETWORK_AND_INTERNET";
+    public static final String TOGGLE_WIFI = "TOGGLE_WIFI";
+    public static final String TOGGLE_HOTSPOT = "TOGGLE_HOTSPOT";
+    // Bluetooth
+    public static final String BLUETOOTH_SETTINGS = "BLUETOOTH";
+    public static final String TOGGLE_BLUETOOTH = "TOGGLE_BLUETOOTH";
+    // Apps
+    public static final String APPS_SETTINGS = "APPS";
+    public static final String PERMISSIONS_PAGE_TITLE = "PERMISSIONS_PAGE_TITLE";
+    public static final String VIEW_ALL = "VIEW_ALL";
+    public static final String ENABLE_DISABLE_BUTTON = "ENABLE_DISABLE_BUTTON";
+    public static final String DISABLE_BUTTON_TEXT = "DISABLE_BUTTON_TEXT";
+    public static final String ENABLE_BUTTON_TEXT = "ENABLE_BUTTON_TEXT";
+    public static final String DISABLE_APP_BUTTON = "DISABLE_APP_BUTTON";
+    public static final String FORCE_STOP_BUTTON = "FORCE_STOP_BUTTON";
+    public static final String OK_BUTTON = "OK_BUTTON";
+    public static final String PERMISSIONS_MENU = "PERMISSIONS_MENU";
+    public static final String ALLOW_BUTTON = "ALLOW_BUTTON";
+    public static final String DONT_ALLOW_BUTTON = "DONT_ALLOW_BUTTON";
+    public static final String DONT_ALLOW_ANYWAY_BUTTON = "DONT_ALLOW_ANYWAY_BUTTON";
+    // Date and Time
+    public static final String DATE_AND_TIME_SETTINGS = "DATE_AND_TIME";
+    public static final String SET_TIME_AUTOMATICALLY = "SET_TIME_AUTOMATICALLY";
+    public static final String SET_TIME_ZONE_AUTOMATICALLY = "SET_TIME_ZONE_AUTOMATICALLY";
+    public static final String SET_DATE = "SET_DATE";
+    public static final String SET_TIME = "SET_TIME";
+    public static final String SELECT_TIME_ZONE = "SELECT_TIME_ZONE";
+    public static final String USE_24_HOUR_FORMAT = "USE_24_HOUR_FORMAT";
+    // OK_BUTTON from Apps and Info Settings
+    public static final String NUMBER_PICKER_WIDGET = "NUMBER_PICKER_WIDGET";
+    public static final String EDIT_TEXT_WIDGET = "EDIT_TEXT_WIDGET";
+    // System
+    public static final String SYSTEM_SETTINGS = "SYSTEM";
+    public static final String ABOUT_MENU = "ABOUT_MENU";
+    public static final String RESET_OPTIONS_MENU = "RESET_OPTIONS_MENU";
+    public static final String LANGUAGES_AND_INPUT_MENU = "LANGUAGES_AND_INPUT_MENU";
+    public static final String DEVICE_MODEL = "DEVICE_MODEL";
+    public static final String ANDROID_VERSION = "ANDROID_VERSION";
+    public static final String ANDROID_SECURITY_PATCH_LEVEL = "ANDROID_SECURITY_PATCH_LEVEL";
+    public static final String KERNEL_VERSION = "KERNEL_VERSION";
+    public static final String BUILD_NUMBER = "BUILD_NUMBER";
+    public static final String RECYCLER_VIEW_WIDGET = "RECYCLER_VIEW_WIDGET";
+    public static final String RESET_NETWORK = "RESET_NETWORK";
+    public static final String RESET_SETTINGS = "RESET_SETTINGS";
+    public static final String RESET_APP_PREFERENCES = "RESET_APP_PREFERENCES";
+    public static final String RESET_APPS = "RESET_APPS";
+    public static final String LANGUAGES_MENU = "LANGUAGES_MENU";
+    public static final String LANGUAGES_MENU_IN_SELECTED_LANGUAGE =
+            "LANGUAGES_MENU_IN_SELECTED_LANGUAGE";
+
+    // MU profiles
+    public static final String MANAGE_OTHER_PROFILES = "MANAGE_OTHER_PROFILES";
+    public static final String ADD_PROFILE = "ADD_PROFILE";
+    public static final String OK = "OK";
+    public static final String DELETE_SELF = "DELETE_SELF";
+    public static final String DELETE = "DELETE";
+    public static final String MAKE_ADMIN = "MAKE_ADMIN";
+    public static final String MAKE_ADMIN_CONFIRM = "MAKE_ADMIN_CONFIRM";
+    public static final String TIME_PATTERN = "TIME_PATTERN";
+
+    // Accounts
+    public static final String PROFILE_ACCOUNT_SETTINGS = "PROFILES_ACCOUNTS";
+    public static final String ADD_ACCOUNT = "ADD_ACCOUNT";
+    public static final String ADD_GOOGLE_ACCOUNT = "ADD_GOOGLE_ACCOUNT";
+    public static final String SIGN_IN_ON_CAR_SCREEN = "SIGN_IN_ON_CAR_SCREEN";
+    public static final String GOOGLE_SIGN_IN_SCREEN = "GOOGLE_SIGN_IN_SCREEN";
+    public static final String ENTER_EMAIL = "ENTER_EMAIL";
+    // Same ENTER_PASSWORD is applicable for Security Settings
+    public static final String ENTER_PASSWORD = "ENTER_PASSWORD";
+    // Same NEXT_BUTTON is applicable for Media Center
+    public static final String NEXT_BUTTON = "NEXT_BUTTON";
+    public static final String DONE_BUTTON = "DONE_BUTTON";
+    // Same REMOVE_BUTTON is applicable for Security Settings
+    public static final String REMOVE_BUTTON = "REMOVE_BUTTON";
+    public static final String REMOVE_ACCOUNT_BUTTON = "REMOVE_ACCOUNT_BUTTON";
+    // Security
+    public static final String SECURITY_SETTINGS = "SECURITY";
+    public static final String TITLE = "TITLE";
+    public static final String CHOOSE_LOCK_TYPE = "CHOOSE_LOCK_TYPE";
+    public static final String PROFILE_LOCK = "PROFILE_LOCK";
+    public static final String LOCK_TYPE_PASSWORD = "LOCK_TYPE_PASSWORD";
+    public static final String LOCK_TYPE_PIN = "LOCK_TYPE_PIN";
+    public static final String LOCK_TYPE_NONE = "LOCK_TYPE_NONE";
+    public static final String CONTINUE_BUTTON = "CONTINUE_BUTTON";
+    public static final String CONFIRM_BUTTON = "CONFIRM_BUTTON";
+    // ENTER_PASSWORD from Account Settings
+    public static final String PIN_PAD = "PIN_PAD";
+    public static final String ENTER_PIN_BUTTON = "ENTER_PIN_BUTTON";
+    // REMOVE_BUTTON from Account Settings
+
+    // PHONE
+    public static final String PHONE = "PHONE";
+    public static final String DIAL_PACKAGE = "DIAL_PACKAGE";
+    public static final String OPEN_DIAL_PAD_COMMAND = "OPEN_DIAL_PAD_COMMAND";
+    public static final String PHONE_ACTIVITY = "PHONE_ACTIVITY";
+    // In Call Screen
+    public static final String IN_CALL_VIEW = "IN_CALL_VIEW";
+    public static final String DIALED_CONTACT_TITLE = "DIALED_CONTACT_TITLE";
+    public static final String DIALED_CONTACT_NUMBER = "DIALED_CONTACT_NUMBER";
+    public static final String END_CALL = "END_CALL";
+    public static final String MUTE_CALL = "MUTE_CALL";
+    public static final String SWITCH_TO_DIAL_PAD = "SWITCH_TO_DIAL_PAD";
+    public static final String CHANGE_VOICE_CHANNEL = "CHANGE_VOICE_CHANNEL";
+    public static final String VOICE_CHANNEL_CAR = "VOICE_CHANNEL_CAR";
+    public static final String VOICE_CHANNEL_PHONE = "VOICE_CHANNEL_PHONE";
+    // Dial Pad Screen
+    public static final String DIAL_PAD_VIEW = "DIAL_PAD_VIEW";
+    public static final String DIAL_PAD_MENU = "DIAL_PAD_MENU";
+    public static final String DIAL_PAD_FRAGMENT = "DIAL_PAD_FRAGMENT";
+    public static final String DIALED_NUMBER = "DIALED_NUMBER";
+    public static final String MAKE_CALL = "MAKE_CALL";
+    public static final String DELETE_NUMBER = "DELETE_NUMBER";
+    // Contacts Screen
+    public static final String CONTACTS_VIEW = "CONTACTS_VIEW";
+    public static final String CONTACTS_MENU = "CONTACTS_MENU";
+    public static final String CONTACT_INFO = "CONTACT_INFO";
+    public static final String CONTACT_NAME = "CONTACT_NAME";
+    public static final String CONTACT_DETAIL = "CONTACT_DETAIL";
+    public static final String ADD_CONTACT_TO_FAVORITE = "ADD_CONTACT_TO_FAVORITE";
+    public static final String SEARCH_CONTACT = "SEARCH_CONTACT";
+    public static final String CONTACT_SEARCH_BAR = "CONTACT_SEARCH_BAR";
+    public static final String SEARCH_RESULT = "SEARCH_RESULT";
+    public static final String CONTACT_SETTINGS = "CONTACT_SETTINGS";
+    public static final String CONTACT_ORDER = "CONTACT_ORDER";
+    public static final String SORT_BY_FIRST_NAME = "SORT_BY_FIRST_NAME";
+    public static final String SORT_BY_LAST_NAME = "SORT_BY_LAST_NAME";
+    public static final String CONTACT_TYPE_WORK = "CONTACT_TYPE_WORK";
+    public static final String CONTACT_TYPE_MOBILE = "CONTACT_TYPE_MOBILE";
+    public static final String CONTACT_TYPE_HOME = "CONTACT_TYPE_HOME";
+    // Call History Screen
+    public static final String CALL_HISTORY_VIEW = "CALL_HISTORY_VIEW";
+    public static final String CALL_HISTORY_MENU = "CALL_HISTORY_MENU";
+    public static final String CALL_HISTORY_INFO = "CALL_HISTORY_INFO";
+    // Favorites Screen
+    public static final String FAVORITES_VIEW = "FAVORITES_VIEW";
+    public static final String FAVORITES_MENU = "FAVORITES_MENU";
+
+    // Notifications
+    public static final String NOTIFICATIONS = "NOTIFICATIONS";
+    public static final String OPEN_NOTIFICATIONS_COMMAND = "OPEN_NOTIFICATIONS_COMMAND";
+    public static final String RECYCLER_VIEW_CLASS = "RECYCLER_VIEW_CLASS";
+    // Expanded Notifications Screen
+    public static final String EXPANDED_NOTIFICATIONS_SCREEN = "EXPANDED_NOTIFICATIONS_SCREEN";
+    public static final String NOTIFICATION_LIST = "NOTIFICATION_LIST";
+    public static final String NOTIFICATION_VIEW = "NOTIFICATION_VIEW";
+    public static final String CLEAR_ALL_BUTTON = "CLEAR_ALL_BUTTON";
+    public static final String STATUS_BAR = "STATUS_BAR";
+    public static final String APP_ICON = "APP_ICON";
+    public static final String APP_NAME = "APP_NAME";
+    public static final String NOTIFICATION_TITLE = "NOTIFICATION_TITLE";
+    public static final String NOTIFICATION_BODY = "NOTIFICATION_BODY";
+    public static final String CARD_VIEW = "CARD_VIEW";
+
+    // Media Center
+    public static final String MEDIA_CENTER = "MEDIA_CENTER";
+    public static final String MEDIA_CENTER_PACKAGE = "MEDIA_CENTER_PACKAGE";
+    public static final String MEDIA_ACTIVITY = "MEDIA_ACTIVITY";
+    // Media Center Screen
+    public static final String MEDIA_CENTER_SCREEN = "MEDIA_CENTER_SCREEN";
+    public static final String PLAY_PAUSE_BUTTON = "PLAY_PAUSE_BUTTON";
+    // NEXT_BUTTON from Account Settings
+    public static final String PREVIOUS_BUTTON = "PREVIOUS_BUTTON";
+    public static final String SHUFFLE_BUTTON = "SHUFFLE_BUTTON";
+    public static final String PLAY_QUEUE_BUTTON = "PLAY_QUEUE_BUTTON";
+    public static final String MINIMIZED_MEDIA_CONTROLS = "MINIMIZED_MEDIA_CONTROLS";
+    public static final String TRACK_NAME = "TRACK_NAME";
+    public static final String TRACK_NAME_MINIMIZED_CONTROL = "TRACK_NAME_MINIMIZED_CONTROL";
+    public static final String BACK_BUTTON = "BACK_BUTTON";
+    // Media Center On Home Screen
+    public static final String MEDIA_CENTER_ON_HOME_SCREEN = "MEDIA_CENTER_ON_HOME_SCREEN";
+}
diff --git a/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoConfigResource.java b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoConfigResource.java
new file mode 100644
index 0000000..ba4c8ae
--- /dev/null
+++ b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoConfigResource.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2021 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.helpers;
+
+/** Automotive UI Resource Config Class */
+public class AutoConfigResource {
+    // Type of UI Resource - Resource ID, Text, Description
+    private String resourceType;
+
+    // Value for the UI Resource - id, text value or description for the resource
+    private String resourceValue;
+
+    // Application Package for the UI Resource if the type is Resource ID,
+    private String resourcePackage;
+
+    // Constructors
+    public AutoConfigResource(String resourceType, String resourceValue) {
+        this.resourceType = resourceType;
+        this.resourceValue = resourceValue;
+    }
+
+    public AutoConfigResource(String resourceType, String resourceValue, String resourcePackage) {
+        this.resourceType = resourceType;
+        this.resourceValue = resourceValue;
+        this.resourcePackage = resourcePackage;
+    }
+
+    /** Get Resource Type ( RESOURCE_ID, TEXT, DESCRIPTION ) */
+    public String getResourceType() {
+        return this.resourceType;
+    }
+
+    /** Get Resource Value ( resource id, text value, description ) */
+    public String getResourceValue() {
+        return this.resourceValue;
+    }
+
+    /** Get Resource Package */
+    public String getResourcePackage() {
+        return this.resourcePackage;
+    }
+
+    /** Set Resource Type ( RESOURCE_ID, TEXT, DESCRIPTION ) */
+    public void setResourceType(String resourceType) {
+        this.resourceType = resourceType;
+    }
+
+    /** Set Resource Value ( resource id, text value, description ) */
+    public void setResourceValue(String resourceValue) {
+        this.resourceValue = resourceValue;
+    }
+
+    /** Set Resource Package */
+    public void setResourcePackage(String resourcePackage) {
+        this.resourcePackage = resourcePackage;
+    }
+}
diff --git a/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoConfiguration.java b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoConfiguration.java
new file mode 100644
index 0000000..a8eb9d0
--- /dev/null
+++ b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoConfiguration.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2021 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.helpers;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/** Configuration Class for mapping Resources to Automotive Device UI */
+public class AutoConfiguration {
+    // Configuration Map
+    private Map<String, AutoConfigResource> configMap;
+
+    // Constructor
+    public AutoConfiguration() {
+        this.configMap = new HashMap<String, AutoConfigResource>();
+    }
+
+    /** Add the resource to map */
+    public void addResource(String key, AutoConfigResource resource) {
+        this.configMap.put(key, resource);
+    }
+
+    /** Get mapped Resource */
+    public AutoConfigResource getResource(String key) {
+        return this.configMap.get(key);
+    }
+}
diff --git a/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoDialConfigUtility.java b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoDialConfigUtility.java
new file mode 100644
index 0000000..47bbc04
--- /dev/null
+++ b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoDialConfigUtility.java
@@ -0,0 +1,321 @@
+/*
+ * Copyright (C) 2021 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.helpers;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/** Configuration for Dialer/Phone Application */
+public class AutoDialConfigUtility implements IAutoConfigUtility {
+    private static final String LOG_TAG = AutoDialConfigUtility.class.getSimpleName();
+
+    // Dialer/Phone Config
+    private static final String DIAL_APP_PACKAGE = "com.android.car.dialer";
+    private static final String PHONE_ACTIVITY = "com.android.car.dialer/.ui.TelecomActivity";
+    private static final String OPEN_DIAL_PAD_COMMAND = "am start -a android.intent.action.DIAL";
+
+    // Config Map
+    private Map<String, AutoConfiguration> mDialConfigMap;
+
+    private static AutoDialConfigUtility sAutoDialConfigInstance = null;
+
+    private AutoDialConfigUtility() {
+        // Initialize Dial Config Map
+        mDialConfigMap = new HashMap<String, AutoConfiguration>();
+    }
+
+    /** Get instance of Auto Dial Utility */
+    public static IAutoConfigUtility getInstance() {
+        if (sAutoDialConfigInstance == null) {
+            sAutoDialConfigInstance = new AutoDialConfigUtility();
+        }
+        return sAutoDialConfigInstance;
+    }
+
+    /**
+     * Get Path for given menu
+     *
+     * @param menu Menu Name
+     */
+    public String[] getPath(String menu) {
+        throw new UnsupportedOperationException(
+                "Operation not supported for Phone/Dialer Application");
+    }
+
+    /**
+     * Add path for given Menu
+     *
+     * @param menu Menu Name
+     * @param path Path
+     */
+    public void addPath(String menu, String[] path) {
+        throw new UnsupportedOperationException(
+                "Operation not supported for Phone/Dialer Application");
+    }
+
+    /**
+     * Get available menu option
+     *
+     * @param menu Menu Name
+     */
+    public String[] getAvailableOptions(String menu) {
+        throw new UnsupportedOperationException(
+                "Operation not supported for Phone/Dialer Application");
+    }
+
+    /**
+     * Add available menu options
+     *
+     * @param menu Menu Name
+     * @param options Available Options
+     */
+    public void addAvailableOptions(String menu, String[] options) {
+        throw new UnsupportedOperationException(
+                "Operation not supported for Phone/Dialer Application");
+    }
+
+    /**
+     * Get Phone/Dialer Config Resource
+     *
+     * @param configName Configuration Name
+     * @param resourceName Resource Name
+     */
+    public AutoConfigResource getResourceConfiguration(String configName, String resourceName) {
+        if (mDialConfigMap.get(configName) == null) {
+            // Unknown Configuration
+            return null;
+        }
+        return mDialConfigMap.get(configName).getResource(resourceName);
+    }
+
+    /**
+     * Validate Phone/Dialer Configuration
+     *
+     * @param configName Configuration Name
+     */
+    public boolean isValidConfiguration(String configName) {
+        return (mDialConfigMap.get(configName) != null);
+    }
+
+    /**
+     * Validate Phone/Dialer Configuration Resource
+     *
+     * @param configName Configuration Name
+     * @param resourceName Resource Name
+     */
+    public boolean isValidResource(String configName, String resourceName) {
+        if (mDialConfigMap.get(configName) == null) {
+            return false;
+        }
+        return (mDialConfigMap.get(configName).getResource(resourceName) != null);
+    }
+
+    /**
+     * Validate Menu Options
+     *
+     * @param menu Menu Name
+     */
+    public boolean isValidOption(String menu) {
+        throw new UnsupportedOperationException(
+                "Operation not supported for Phone/Dialer Application");
+    }
+
+    /**
+     * Validate Menu Path
+     *
+     * @param menu Menu Name
+     */
+    public boolean isValidPath(String menu) {
+        throw new UnsupportedOperationException(
+                "Operation not supported for Phone/Dialer Application");
+    }
+
+    /** Load default configuration for Dialer/Phone application */
+    public void loadDefaultConfig(Map<String, String> mApplicationConfigMap) {
+        // Default Dial Application Config
+        loadDefaultDialAppConfig(mApplicationConfigMap);
+
+        // Default In Call Screen Config
+        loadDefaultInCallConfig(mDialConfigMap);
+
+        // Default Dial Pad Screen Config
+        loadDefaultDialPadConfig(mDialConfigMap);
+
+        // Default Contacts Screen Config
+        loadDefaultContactsConfig(mDialConfigMap);
+
+        // Default Call History Screen Config
+        loadDefaultCallHistoryConfig(mDialConfigMap);
+
+        // Default Favorites Screen Config
+        loadDefaultFavoritesConfig(mDialConfigMap);
+    }
+
+    private void loadDefaultDialAppConfig(Map<String, String> mApplicationConfigMap) {
+        // Add default Dial package
+        mApplicationConfigMap.put(AutoConfigConstants.DIAL_PACKAGE, DIAL_APP_PACKAGE);
+        // Add default Phone package
+        mApplicationConfigMap.put(AutoConfigConstants.PHONE_ACTIVITY, PHONE_ACTIVITY);
+        // Add default command to open Dial Pad
+        mApplicationConfigMap.put(AutoConfigConstants.OPEN_DIAL_PAD_COMMAND, OPEN_DIAL_PAD_COMMAND);
+    }
+
+    private void loadDefaultInCallConfig(Map<String, AutoConfiguration> mDialConfigMap) {
+        AutoConfiguration inCallScreenConfiguration = new AutoConfiguration();
+        inCallScreenConfiguration.addResource(
+                AutoConfigConstants.DIALED_CONTACT_TITLE,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "user_profile_title", DIAL_APP_PACKAGE));
+        inCallScreenConfiguration.addResource(
+                AutoConfigConstants.DIALED_CONTACT_NUMBER,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID,
+                        "user_profile_phone_number",
+                        DIAL_APP_PACKAGE));
+        inCallScreenConfiguration.addResource(
+                AutoConfigConstants.END_CALL,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "end_call_button", DIAL_APP_PACKAGE));
+        inCallScreenConfiguration.addResource(
+                AutoConfigConstants.MUTE_CALL,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "mute_button", DIAL_APP_PACKAGE));
+        inCallScreenConfiguration.addResource(
+                AutoConfigConstants.SWITCH_TO_DIAL_PAD,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID,
+                        "toggle_dialpad_button",
+                        DIAL_APP_PACKAGE));
+        inCallScreenConfiguration.addResource(
+                AutoConfigConstants.CHANGE_VOICE_CHANNEL,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "voice_channel_view", DIAL_APP_PACKAGE));
+        inCallScreenConfiguration.addResource(
+                AutoConfigConstants.VOICE_CHANNEL_CAR,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Car speakers"));
+        inCallScreenConfiguration.addResource(
+                AutoConfigConstants.VOICE_CHANNEL_PHONE,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Phone"));
+        mDialConfigMap.put(AutoConfigConstants.IN_CALL_VIEW, inCallScreenConfiguration);
+    }
+
+    private void loadDefaultDialPadConfig(Map<String, AutoConfiguration> mDialConfigMap) {
+        AutoConfiguration dialPadScreenConfiguration = new AutoConfiguration();
+        dialPadScreenConfiguration.addResource(
+                AutoConfigConstants.DIAL_PAD_MENU,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Dial.?pad"));
+        dialPadScreenConfiguration.addResource(
+                AutoConfigConstants.DIAL_PAD_FRAGMENT,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "dialpad_fragment", DIAL_APP_PACKAGE));
+        dialPadScreenConfiguration.addResource(
+                AutoConfigConstants.DIALED_NUMBER,
+                new AutoConfigResource(AutoConfigConstants.RESOURCE_ID, "title", DIAL_APP_PACKAGE));
+        dialPadScreenConfiguration.addResource(
+                AutoConfigConstants.MAKE_CALL,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "call_button", DIAL_APP_PACKAGE));
+        dialPadScreenConfiguration.addResource(
+                AutoConfigConstants.DELETE_NUMBER,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "delete_button", DIAL_APP_PACKAGE));
+        mDialConfigMap.put(AutoConfigConstants.DIAL_PAD_VIEW, dialPadScreenConfiguration);
+    }
+
+    private void loadDefaultContactsConfig(Map<String, AutoConfiguration> mDialConfigMap) {
+        AutoConfiguration contactsScreenConfig = new AutoConfiguration();
+        contactsScreenConfig.addResource(
+                AutoConfigConstants.CONTACTS_MENU,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Contacts"));
+        contactsScreenConfig.addResource(
+                AutoConfigConstants.CONTACT_INFO,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "call_action_id", DIAL_APP_PACKAGE));
+        contactsScreenConfig.addResource(
+                AutoConfigConstants.CONTACT_NAME,
+                new AutoConfigResource(AutoConfigConstants.RESOURCE_ID, "title", DIAL_APP_PACKAGE));
+        contactsScreenConfig.addResource(
+                AutoConfigConstants.CONTACT_DETAIL,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID,
+                        "show_contact_detail_id",
+                        DIAL_APP_PACKAGE));
+        contactsScreenConfig.addResource(
+                AutoConfigConstants.ADD_CONTACT_TO_FAVORITE,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID,
+                        "contact_details_favorite_button",
+                        DIAL_APP_PACKAGE));
+        contactsScreenConfig.addResource(
+                AutoConfigConstants.SEARCH_CONTACT,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "menu_item_search", DIAL_APP_PACKAGE));
+        contactsScreenConfig.addResource(
+                AutoConfigConstants.CONTACT_SEARCH_BAR,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID,
+                        "car_ui_toolbar_search_bar",
+                        DIAL_APP_PACKAGE));
+        contactsScreenConfig.addResource(
+                AutoConfigConstants.SEARCH_RESULT,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "contact_name", DIAL_APP_PACKAGE));
+        contactsScreenConfig.addResource(
+                AutoConfigConstants.CONTACT_SETTINGS,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "menu_item_setting", DIAL_APP_PACKAGE));
+        contactsScreenConfig.addResource(
+                AutoConfigConstants.CONTACT_ORDER,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Contact order"));
+        contactsScreenConfig.addResource(
+                AutoConfigConstants.SORT_BY_FIRST_NAME,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "First name"));
+        contactsScreenConfig.addResource(
+                AutoConfigConstants.SORT_BY_LAST_NAME,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Last name"));
+        contactsScreenConfig.addResource(
+                AutoConfigConstants.CONTACT_TYPE_WORK,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Work"));
+        contactsScreenConfig.addResource(
+                AutoConfigConstants.CONTACT_TYPE_MOBILE,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Mobile"));
+        contactsScreenConfig.addResource(
+                AutoConfigConstants.CONTACT_TYPE_HOME,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Home"));
+        mDialConfigMap.put(AutoConfigConstants.CONTACTS_VIEW, contactsScreenConfig);
+    }
+
+    private void loadDefaultCallHistoryConfig(Map<String, AutoConfiguration> mDialConfigMap) {
+        AutoConfiguration callHistoryScreenConfiguration = new AutoConfiguration();
+        callHistoryScreenConfiguration.addResource(
+                AutoConfigConstants.CALL_HISTORY_MENU,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Recents"));
+        callHistoryScreenConfiguration.addResource(
+                AutoConfigConstants.CALL_HISTORY_INFO,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "call_action_id", DIAL_APP_PACKAGE));
+        mDialConfigMap.put(AutoConfigConstants.CALL_HISTORY_VIEW, callHistoryScreenConfiguration);
+    }
+
+    private void loadDefaultFavoritesConfig(Map<String, AutoConfiguration> mDialConfigMap) {
+        AutoConfiguration favoritesScreenConfiguration = new AutoConfiguration();
+        favoritesScreenConfiguration.addResource(
+                AutoConfigConstants.FAVORITES_MENU,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Favo.?rite.?"));
+        mDialConfigMap.put(AutoConfigConstants.FAVORITES_VIEW, favoritesScreenConfiguration);
+    }
+}
diff --git a/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoHomeConfigUtility.java b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoHomeConfigUtility.java
new file mode 100644
index 0000000..ee699fb
--- /dev/null
+++ b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoHomeConfigUtility.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2021 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.helpers;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/** Configuration for Home app */
+public class AutoHomeConfigUtility implements IAutoConfigUtility {
+
+    private static final String LOG_TAG = AutoHomeConfigUtility.class.getSimpleName();
+
+    // HOME Config
+    private static final String HOME_PACKAGE = "com.android.car.carlauncher";
+
+    // HOME Map
+    private Map<String, AutoConfiguration> mHomeConfigMap;
+
+    private static AutoHomeConfigUtility sAutoHomeConfigInstance = null;
+
+    private AutoHomeConfigUtility() {
+        // Initialize Home Config Map
+        mHomeConfigMap = new HashMap<String, AutoConfiguration>();
+    }
+
+    /** Get instance of Auto Home Utility */
+    public static IAutoConfigUtility getInstance() {
+        if (sAutoHomeConfigInstance == null) {
+            sAutoHomeConfigInstance = new AutoHomeConfigUtility();
+        }
+        return sAutoHomeConfigInstance;
+    }
+
+    /**
+     * Get Path for given menu
+     *
+     * @param menu Menu Name
+     */
+    public String[] getPath(String menu) {
+        throw new UnsupportedOperationException("Operation not supported for Home");
+    }
+
+    /**
+     * Add path for given Menu
+     *
+     * @param menu Menu Name
+     * @param path Path
+     */
+    public void addPath(String menu, String[] path) {
+        throw new UnsupportedOperationException("Operation not supported for Home");
+    }
+
+    /**
+     * Get available menu option
+     *
+     * @param menu Menu Name
+     */
+    public String[] getAvailableOptions(String menu) {
+        throw new UnsupportedOperationException("Operation not supported for Home");
+    }
+
+    /**
+     * Add available menu options
+     *
+     * @param menu Menu Name
+     * @param options Available Options
+     */
+    public void addAvailableOptions(String menu, String[] options) {
+        throw new UnsupportedOperationException("Operation not supported for Home");
+    }
+
+    /**
+     * Get App Grid Config Resource
+     *
+     * @param configName Configuration Name
+     * @param resourceName Resource Name
+     */
+    public AutoConfigResource getResourceConfiguration(String configName, String resourceName) {
+        if (mHomeConfigMap.get(configName) == null) {
+            // Unknown Configuration
+            return null;
+        }
+        return mHomeConfigMap.get(configName).getResource(resourceName);
+    }
+
+    /**
+     * Validate Home Configuration
+     *
+     * @param configName Configuration Name
+     */
+    public boolean isValidConfiguration(String configName) {
+        return (mHomeConfigMap.get(configName) != null);
+    }
+
+    /**
+     * Validate Home Configuration Resource
+     *
+     * @param configName Configuration Name
+     * @param resourceName Resource Name
+     */
+    public boolean isValidResource(String configName, String resourceName) {
+        if (mHomeConfigMap.get(configName) == null) {
+            return false;
+        }
+        return (mHomeConfigMap.get(configName).getResource(resourceName) != null);
+    }
+
+    /**
+     * Validate Menu Options
+     *
+     * @param menu Menu Name
+     */
+    public boolean isValidOption(String menu) {
+        throw new UnsupportedOperationException("Operation not supported for Home Application");
+    }
+
+    /**
+     * Validate Menu Path
+     *
+     * @param menu Menu Name
+     */
+    public boolean isValidPath(String menu) {
+        throw new UnsupportedOperationException("Operation not supported for Home Application");
+    }
+
+    /** Load default configuration for App Grid application */
+    public void loadDefaultConfig(Map<String, String> mApplicationConfigMap) {
+        // Default App Grid Application Config
+        loadDefaultHomeConfig(mApplicationConfigMap);
+        // Default App Grid view Config
+        loadDefaultHomeViewConfig(mHomeConfigMap);
+    }
+
+    private void loadDefaultHomeConfig(Map<String, String> mApplicationConfigMap) {
+        // Add default App Grid package
+        mApplicationConfigMap.put(AutoConfigConstants.HOME_PACKAGE, HOME_PACKAGE);
+    }
+
+    private void loadDefaultHomeViewConfig(Map<String, AutoConfiguration> mHomeConfigMap) {
+        AutoConfiguration homeViewConfiguration = new AutoConfiguration();
+        homeViewConfiguration.addResource(
+                AutoConfigConstants.TOP_CARD,
+                new AutoConfigResource(AutoConfigConstants.RESOURCE_ID, "top_card", HOME_PACKAGE));
+        homeViewConfiguration.addResource(
+                AutoConfigConstants.BOTTOM_CARD,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "bottom_card", HOME_PACKAGE));
+        homeViewConfiguration.addResource(
+                AutoConfigConstants.MAP_CARD,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "scene_card", HOME_PACKAGE));
+        mHomeConfigMap.put(AutoConfigConstants.HOME_VIEW, homeViewConfiguration);
+    }
+}
diff --git a/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoJsonUtility.java b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoJsonUtility.java
new file mode 100644
index 0000000..306a801
--- /dev/null
+++ b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoJsonUtility.java
@@ -0,0 +1,476 @@
+/*
+ * Copyright (C) 2021 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.helpers;
+
+import androidx.test.InstrumentationRegistry;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public class AutoJsonUtility {
+    private static final String LOG_TAG = AutoJsonUtility.class.getSimpleName();
+
+    // Config File Path Param
+    private static final String CONFIG_FILE_PATH_PARAM = "config-file-path";
+
+    // Application Config
+    public static final String APPLICATION_CONFIG = "APPLICATION_CONFIG";
+
+    private Map<String, String> mApplicationConfigMap;
+
+    // Config Utility Map
+    private Map<String, IAutoConfigUtility> configUtilityMap;
+
+    // Resources
+    private static final String PATH_KEY = "PATH";
+    private static final String OPTIONS_KEY = "OPTIONS";
+    private static final String PATH_DELIMETER = ">";
+    private static final String TYPE_KEY = "TYPE";
+    private static final String VALUE_KEY = "VALUE";
+    private static final String PACKAGE_KEY = "PACKAGE";
+
+    private static AutoJsonUtility sAutoJsonUtilityInstance = null;
+
+    private AutoJsonUtility() {
+        initializeConfigMaps();
+        buildConfigMaps();
+    }
+
+    /** Get Auto Json Utility Instance */
+    public static AutoJsonUtility getInstance() {
+        if (sAutoJsonUtilityInstance == null) {
+            sAutoJsonUtilityInstance = new AutoJsonUtility();
+        }
+        return sAutoJsonUtilityInstance;
+    }
+
+    /** Get Path for given Setting */
+    public String[] getSettingPath(String setting) {
+        return configUtilityMap.get(AutoConfigConstants.SETTINGS).getPath(setting);
+    }
+
+    /** Get menu options in given setting */
+    public String[] getSettingOptions(String setting) {
+        return configUtilityMap.get(AutoConfigConstants.SETTINGS).getAvailableOptions(setting);
+    }
+
+    /** Get config value for given config */
+    public String getApplicationConfig(String config) {
+        return mApplicationConfigMap.get(config);
+    }
+
+    /** Get the UI resource configuration */
+    public AutoConfigResource getResourceFromConfig(
+            String application, String config, String resource) {
+        if (configUtilityMap.get(application) == null) {
+            // Unknown Application
+            return null;
+        }
+        return configUtilityMap.get(application).getResourceConfiguration(config, resource);
+    }
+
+    /** Initialize Config Maps */
+    private void initializeConfigMaps() {
+        // Initialize Application Config Map
+        mApplicationConfigMap = new HashMap<String, String>();
+
+        // Initialize Config Utility Map
+        configUtilityMap = new HashMap<String, IAutoConfigUtility>();
+
+        // load config uitilities for supported applications
+        loadConfigUtilitiesForSupportedApplications();
+    }
+
+    /** Load config uitilities for supported applications */
+    private void loadConfigUtilitiesForSupportedApplications() {
+        // Phone/Dialer
+        configUtilityMap.put(AutoConfigConstants.PHONE, AutoDialConfigUtility.getInstance());
+
+        // Settings
+        configUtilityMap.put(AutoConfigConstants.SETTINGS, AutoSettingsConfigUtility.getInstance());
+
+        // Notifications
+        configUtilityMap.put(
+                AutoConfigConstants.NOTIFICATIONS, AutoNotificationsConfigUtility.getInstance());
+
+        // Media Center
+        configUtilityMap.put(
+                AutoConfigConstants.MEDIA_CENTER, AutoMediaCenterConfigUtility.getInstance());
+
+        // App Grid
+        configUtilityMap.put(AutoConfigConstants.APP_GRID, AutoAppGridConfigUtility.getInstance());
+
+        // Home
+        configUtilityMap.put(AutoConfigConstants.HOME, AutoHomeConfigUtility.getInstance());
+
+        // Lock Screen
+        configUtilityMap.put(
+                AutoConfigConstants.LOCK_SCREEN, AutoLockScreenConfigUtility.getInstance());
+    }
+
+    /** Build Config Maps */
+    private void buildConfigMaps() {
+        // Build Config Maps with default values ( based on Reference Devices )
+        buildDefaultConfigMaps();
+
+        // Read Config File Path Param
+        String configFilePath =
+                InstrumentationRegistry.getArguments().getString(CONFIG_FILE_PATH_PARAM, null);
+
+        // If Config File is provided update the config maps with data from the file
+        if (configFilePath != null) {
+            updateConfigMapsFromFile(configFilePath);
+        }
+    }
+
+    /** Build Default Config Maps ( Based on Reference Device ) */
+    private void buildDefaultConfigMaps() {
+        for (String application : configUtilityMap.keySet()) {
+            configUtilityMap.get(application).loadDefaultConfig(mApplicationConfigMap);
+        }
+    }
+
+    /**
+     * Reads and parses the Config JSON file and update the Config Maps Sample JSON {
+     * APPLICATION_NAME_1: { APPLICATION_CONFIG: { APPLICATION_PACKAGE:
+     * "android.application.package" }, APPLICATION_RESOURCES: { PATH: "APPLICATION_PATH [ e.g.
+     * Settings > System ]", OPTIONS: [ "APPLICATION_SETTINGS_OPTION_1"
+     * "APPLICATION_SETTINGS_OPTION_2" ], APPLICATION_RESOURCE_1: { TYPE: "RESOURCE_ID", VALUE:
+     * "RESOURCE_ID_VALUE", PACKAGE: "RESOURCE_PACKAGE" }, APPLICATION_RESOURCE_2: { TYPE: "TEXT",
+     * VALUE: "TEXT_VALUE" } } }, APPLICATION_NAME_2: { ... } }
+     *
+     * @param configFilePath Path for the config file
+     */
+    private void updateConfigMapsFromFile(String configFilePath) {
+        // Read data from config file
+        String configFileData = readConfigFile(configFilePath);
+        if (configFileData == null || configFileData.isEmpty()) {
+            throw new RuntimeException("Config file is empty.");
+        }
+        try {
+            // Parse json data from config file
+            // and update config maps from the parsed JSON config data
+            JSONObject configFileDataJson = new JSONObject(configFileData);
+
+            Iterator<String> applicationNames = configFileDataJson.keys();
+            while (applicationNames.hasNext()) {
+                String applicationName = applicationNames.next();
+                if (!isApplicationSupported(applicationName)) {
+                    throwUnknownObjectException(applicationName);
+                }
+                Object applicationConfig = configFileDataJson.get(applicationName);
+                if (applicationConfig == null || !isJsonObject(applicationConfig)) {
+                    throwJsonObjectException(applicationName);
+                }
+                parseJsonAndUpdateConfigMaps(applicationName, (JSONObject) applicationConfig);
+            }
+        } catch (JSONException e) {
+            throw new RuntimeException(e);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /** Parse JSON data and Update Config Maps with data from JSON */
+    private void parseJsonAndUpdateConfigMaps(String appName, JSONObject appConfiguration)
+            throws JSONException {
+        Iterator<String> appConfigAndResourceKeys = appConfiguration.keys();
+        while (appConfigAndResourceKeys.hasNext()) {
+            String appConfigAndResourceKey = appConfigAndResourceKeys.next();
+            Object appConfigAndResource = appConfiguration.get(appConfigAndResourceKey);
+            if (appConfigAndResource == null || !isJsonObject(appConfigAndResource)) {
+                throwJsonObjectException(appName, appConfigAndResourceKey);
+            }
+            if (APPLICATION_CONFIG.equals(appConfigAndResourceKey)) {
+                parseJsonAndUpdateApplicationConfigMap(
+                        appName, appConfigAndResourceKey, (JSONObject) appConfigAndResource);
+                continue;
+            }
+            if (!isValidConfiguration(appName, appConfigAndResourceKey)) {
+                throwUnknownObjectException(appName, appConfigAndResourceKey);
+            }
+            parseJsonAndUpdateApplicationResources(
+                    appName, appConfigAndResourceKey, (JSONObject) appConfigAndResource);
+        }
+    }
+
+    /** Parse JSON and Update Application Config Map with data from JSON */
+    private void parseJsonAndUpdateApplicationConfigMap(
+            String appName, String configName, JSONObject config) throws JSONException {
+        Iterator<String> configKeys = config.keys();
+        while (configKeys.hasNext()) {
+            String configKey = configKeys.next();
+            if (!isValidApplicationConfig(configKey)) {
+                throwUnknownObjectException(appName, configName, configKey);
+            }
+            Object applicationConfig = config.get(configKey);
+            if (applicationConfig == null || !isJsonString(applicationConfig)) {
+                throwJsonStringException(appName, configName, configKey);
+            }
+            String appConfig = (String) applicationConfig;
+            mApplicationConfigMap.put(configKey, appConfig);
+        }
+    }
+
+    /** Parse JSON and Update Resources for given application with the data from JSON */
+    private void parseJsonAndUpdateApplicationResources(
+            String appName, String configName, JSONObject config) throws JSONException {
+        Iterator<String> resourceNames = config.keys();
+        while (resourceNames.hasNext()) {
+            String resourceName = resourceNames.next();
+            if (PATH_KEY.equals(resourceName)) {
+                parseJsonAndUpdateConfigPath(appName, configName, config);
+                continue;
+            }
+            if (OPTIONS_KEY.equals(resourceName)) {
+                parseJsonAndUpdateConfigOptions(appName, configName, config);
+                continue;
+            }
+            if (!isValidResourceConfig(appName, configName, resourceName)) {
+                throwUnknownObjectException(appName, configName, resourceName);
+            }
+            Object applicationResource = config.get(resourceName);
+            if (applicationResource == null || !isJsonObject(applicationResource)) {
+                throwJsonObjectException(appName, configName, resourceName);
+            }
+            parseJsonAndUpdateResourcesInConfigMaps(
+                    appName, configName, resourceName, (JSONObject) applicationResource);
+        }
+    }
+
+    /** Validate if given resource exists in the configuration for the given application */
+    private boolean isValidResourceConfig(String appName, String configName, String resourceName) {
+        return configUtilityMap.get(appName).isValidResource(configName, resourceName);
+    }
+
+    /** Validate if given config exists for the application */
+    private boolean isValidConfiguration(String appName, String configName) {
+        return configUtilityMap.get(appName).isValidConfiguration(configName);
+    }
+
+    /** Parse Json and Update resources in Config Maps for given Application */
+    private void parseJsonAndUpdateResourcesInConfigMaps(
+            String appName, String configName, String resourceName, JSONObject config)
+            throws JSONException {
+        Object resourceType = config.get(TYPE_KEY);
+        if (resourceType == null || !isJsonString(resourceType)) {
+            throwJsonStringException(appName, configName, resourceName, TYPE_KEY);
+        }
+        Object resourceValue = config.get(VALUE_KEY);
+        if (resourceValue == null || !isJsonString(resourceValue)) {
+            throwJsonStringException(appName, configName, resourceName, VALUE_KEY);
+        }
+        configUtilityMap
+                .get(appName)
+                .getResourceConfiguration(configName, resourceName)
+                .setResourceType((String) resourceType);
+        configUtilityMap
+                .get(appName)
+                .getResourceConfiguration(configName, resourceName)
+                .setResourceValue((String) resourceValue);
+        if (AutoConfigConstants.RESOURCE_ID.equals((String) resourceType)) {
+            if (!config.has(PACKAGE_KEY) || config.isNull(PACKAGE_KEY)) {
+                throw new RuntimeException(
+                        String.format(
+                                "Config file is invalid. %s is required for "
+                                        + "Resource %s for %s in %s application.",
+                                PACKAGE_KEY, resourceName, configName, appName));
+            }
+            Object resourcePackage = config.get(PACKAGE_KEY);
+            if (resourcePackage == null || !isJsonString(resourcePackage)) {
+                throwJsonStringException(appName, configName, resourceName, PACKAGE_KEY);
+            }
+            configUtilityMap
+                    .get(appName)
+                    .getResourceConfiguration(configName, resourceName)
+                    .setResourcePackage((String) resourcePackage);
+        }
+    }
+
+    /** Parse JSON and update config options for the given application */
+    private void parseJsonAndUpdateConfigOptions(
+            String appName, String configName, JSONObject config) throws JSONException {
+        if (!isValidOptionsConfig(appName, configName)) {
+            throwUnknownObjectException(appName, configName, OPTIONS_KEY);
+        }
+        Object configOptions = config.get(OPTIONS_KEY);
+        if (configOptions == null || !isJsonArray(configOptions)) {
+            throwJsonArrayException(appName, configName, OPTIONS_KEY);
+        }
+        JSONArray options = (JSONArray) configOptions;
+        String[] optionsArray = new String[options.length()];
+        for (int i = 0; i < options.length(); i++) {
+            Object option = options.get(i);
+            if (option == null || !isJsonString(option)) {
+                throwJsonStringException(appName, configName, OPTIONS_KEY, "index-" + i);
+            }
+            String configOption = (String) option;
+            if (configOption == null || configOption.trim().isEmpty()) {
+                throw new RuntimeException(
+                        String.format(
+                                "Config file is invalid. %s in %s for %s contains null or empty"
+                                        + " string.",
+                                OPTIONS_KEY, configName, appName));
+            }
+            optionsArray[i] = configOption.trim();
+        }
+        addOptionsToMap(appName, configName, optionsArray);
+    }
+
+    /** Add given options to application options map */
+    private void addOptionsToMap(String appName, String configName, String[] options) {
+        configUtilityMap.get(appName).addAvailableOptions(configName, options);
+    }
+
+    /** Validate if given config exist in application options map */
+    private boolean isValidOptionsConfig(String appName, String configName) {
+        return configUtilityMap.get(appName).isValidOption(configName);
+    }
+
+    /** Parse JSON and update path for config in the given application */
+    private void parseJsonAndUpdateConfigPath(String appName, String configName, JSONObject config)
+            throws JSONException {
+        if (!isValidPathConfig(appName, configName)) {
+            throwUnknownObjectException(appName, configName, PATH_KEY);
+        }
+        Object configPath = config.get(PATH_KEY);
+        if (configPath == null || !isJsonString(configPath)) {
+            throwJsonStringException(appName, configName, PATH_KEY);
+        }
+        String path = (String) configPath;
+        String[] paths = path.split(PATH_DELIMETER);
+        if (paths.length <= 1) {
+            throw new RuntimeException(
+                    String.format(
+                            "Config file is invalid. %s in %s for %s should be a valid path.",
+                            PATH_KEY, configName, appName));
+        }
+        String[] pathsArray = new String[paths.length - 1];
+        for (int i = 1; i < paths.length; i++) {
+            pathsArray[i - 1] = paths[i].trim();
+        }
+        addPathToMap(appName, configName, pathsArray);
+    }
+
+    /** Add the given path for Application Paths Map */
+    private void addPathToMap(String appName, String configName, String[] paths) {
+        configUtilityMap.get(appName).addPath(configName, paths);
+    }
+
+    /** Read data from config file */
+    private String readConfigFile(String configFilePath) {
+        String configFileData = null;
+        try {
+            File configFile = new File(configFilePath);
+            if (configFile == null || !configFile.canRead()) {
+                throw new IOException("Cannot read the Resource file - " + configFilePath);
+            }
+            FileReader configFileReader = new FileReader(configFile);
+            BufferedReader configFileBufferedReader = new BufferedReader(configFileReader);
+            StringBuilder stringBuilderObj = new StringBuilder();
+            String configFileDataLine = configFileBufferedReader.readLine();
+            while (configFileDataLine != null) {
+                stringBuilderObj.append(configFileDataLine).append("\n");
+                configFileDataLine = configFileBufferedReader.readLine();
+            }
+            configFileBufferedReader.close();
+            configFileData = stringBuilderObj.toString();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+        return configFileData;
+    }
+
+    /** Validate if given config exist in application paths map */
+    private boolean isValidPathConfig(String appName, String configName) {
+        return configUtilityMap.get(appName).isValidPath(configName);
+    }
+
+    /** Validate for Supported Applications */
+    private boolean isApplicationSupported(String applicationName) {
+        if (configUtilityMap.get(applicationName) != null) {
+            return true;
+        }
+        return false;
+    }
+
+    /** Validate given application config */
+    private boolean isValidApplicationConfig(String appConfig) {
+        return (mApplicationConfigMap.get(appConfig) != null);
+    }
+
+    /** Validate for Json String */
+    private boolean isJsonString(Object object) {
+        return !(object instanceof JSONObject || object instanceof JSONArray);
+    }
+
+    /** Validate for Json Object */
+    private boolean isJsonObject(Object object) {
+        return (object instanceof JSONObject);
+    }
+
+    /** Validate for Json Array */
+    private boolean isJsonArray(Object object) {
+        return (object instanceof JSONArray);
+    }
+
+    /** Throw Exception for Invalid Json Array */
+    private void throwJsonArrayException(String... configs) {
+        String message =
+                String.format(
+                        "Config file is invalid. Please validate %s config. It should be a JSON"
+                                + " Array.",
+                        Arrays.toString(configs));
+        throw new RuntimeException(message);
+    }
+
+    /** Throw Exception for Invalid Json String Object */
+    private void throwJsonStringException(String... configs) {
+        String message =
+                String.format(
+                        "Config file is invalid. Please validate %s config. It should be a String.",
+                        Arrays.toString(configs));
+        throw new RuntimeException(message);
+    }
+
+    /** Throw Exception for Invalid Json Object */
+    private void throwJsonObjectException(String... configs) {
+        String message =
+                String.format(
+                        "Config file is invalid. Please validate %s config. It should be a JSON"
+                                + " Object.",
+                        Arrays.toString(configs));
+        throw new RuntimeException(message);
+    }
+
+    /** Throw Exception for Unknown Object */
+    private void throwUnknownObjectException(String... configs) {
+        String message =
+                String.format(
+                        "Config file is invalid. Unknown config %s.", Arrays.toString(configs));
+        throw new RuntimeException(message);
+    }
+}
diff --git a/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoLockScreenConfigUtility.java b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoLockScreenConfigUtility.java
new file mode 100644
index 0000000..a313bdf
--- /dev/null
+++ b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoLockScreenConfigUtility.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2021 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.helpers;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/** Configuration for Lock Screen */
+public class AutoLockScreenConfigUtility implements IAutoConfigUtility {
+
+    private static final String LOG_TAG = AutoLockScreenConfigUtility.class.getSimpleName();
+
+    // Lock Screen Config
+    private static final String LOCK_SCREEN_PACKAGE = "com.android.systemui";
+
+    // Lock Screen Map
+    private Map<String, AutoConfiguration> mLockScreenConfigMap;
+
+    private static AutoLockScreenConfigUtility sAutoLockScreenConfigInstance = null;
+
+    private AutoLockScreenConfigUtility() {
+        // Initialize Lock Screen Config Map
+        mLockScreenConfigMap = new HashMap<String, AutoConfiguration>();
+    }
+
+    /** Get instance of Auto Lock Screen Utility */
+    public static IAutoConfigUtility getInstance() {
+        if (sAutoLockScreenConfigInstance == null) {
+            sAutoLockScreenConfigInstance = new AutoLockScreenConfigUtility();
+        }
+        return sAutoLockScreenConfigInstance;
+    }
+
+    /**
+     * Get Path for given menu
+     *
+     * @param menu Menu Name
+     */
+    public String[] getPath(String menu) {
+        throw new UnsupportedOperationException("Operation not supported for Lock Screen");
+    }
+
+    /**
+     * Add path for given Menu
+     *
+     * @param menu Menu Name
+     * @param path Path
+     */
+    public void addPath(String menu, String[] path) {
+        throw new UnsupportedOperationException("Operation not supported for Lock Screen");
+    }
+
+    /**
+     * Get available menu option
+     *
+     * @param menu Menu Name
+     */
+    public String[] getAvailableOptions(String menu) {
+        throw new UnsupportedOperationException("Operation not supported for Lock Screen");
+    }
+
+    /**
+     * Add available menu options
+     *
+     * @param menu Menu Name
+     * @param options Available Options
+     */
+    public void addAvailableOptions(String menu, String[] options) {
+        throw new UnsupportedOperationException("Operation not supported for Lock Screen");
+    }
+
+    /**
+     * Get App Grid Config Resource
+     *
+     * @param configName Configuration Name
+     * @param resourceName Resource Name
+     */
+    public AutoConfigResource getResourceConfiguration(String configName, String resourceName) {
+        if (mLockScreenConfigMap.get(configName) == null) {
+            // Unknown Configuration
+            return null;
+        }
+        return mLockScreenConfigMap.get(configName).getResource(resourceName);
+    }
+
+    /**
+     * Validate Home Configuration
+     *
+     * @param configName Configuration Name
+     */
+    public boolean isValidConfiguration(String configName) {
+        return (mLockScreenConfigMap.get(configName) != null);
+    }
+
+    /**
+     * Validate Home Configuration Resource
+     *
+     * @param configName Configuration Name
+     * @param resourceName Resource Name
+     */
+    public boolean isValidResource(String configName, String resourceName) {
+        if (mLockScreenConfigMap.get(configName) == null) {
+            return false;
+        }
+        return (mLockScreenConfigMap.get(configName).getResource(resourceName) != null);
+    }
+
+    /**
+     * Validate Menu Options
+     *
+     * @param menu Menu Name
+     */
+    public boolean isValidOption(String menu) {
+        throw new UnsupportedOperationException(
+                "Operation not supported for Lock Screen Application");
+    }
+
+    /**
+     * Validate Menu Path
+     *
+     * @param menu Menu Name
+     */
+    public boolean isValidPath(String menu) {
+        throw new UnsupportedOperationException("Operation not supported for Lock Screen");
+    }
+
+    /** Load default configuration for Lock Screen */
+    public void loadDefaultConfig(Map<String, String> mApplicationConfigMap) {
+        // Default Lock Screen Config
+        loadDefaultLockScreenConfig(mApplicationConfigMap);
+        // Default Lock Screen view Config
+        loadDefaultLockScreenViewConfig(mLockScreenConfigMap);
+    }
+
+    private void loadDefaultLockScreenConfig(Map<String, String> mApplicationConfigMap) {
+        // Add default App Grid package
+        mApplicationConfigMap.put(AutoConfigConstants.LOCK_SCREEN_PACKAGE, LOCK_SCREEN_PACKAGE);
+    }
+
+    private void loadDefaultLockScreenViewConfig(
+            Map<String, AutoConfiguration> mLockScreenConfigMap) {
+        AutoConfiguration lockScreenViewConfiguration = new AutoConfiguration();
+        lockScreenViewConfiguration.addResource(
+                AutoConfigConstants.PIN_PAD,
+                new AutoConfigResource(
+                        AutoConfigConstants.PIN_PAD, "keyguard_pin_view", LOCK_SCREEN_PACKAGE));
+        lockScreenViewConfiguration.addResource(
+                AutoConfigConstants.ENTER_KEY,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "key_enter", LOCK_SCREEN_PACKAGE));
+        mLockScreenConfigMap.put(AutoConfigConstants.LOCK_SCREEN_VIEW, lockScreenViewConfiguration);
+    }
+}
diff --git a/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoMediaCenterConfigUtility.java b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoMediaCenterConfigUtility.java
new file mode 100644
index 0000000..d7e1ad1
--- /dev/null
+++ b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoMediaCenterConfigUtility.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2021 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.helpers;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/** Configuration for Media Center Application */
+public class AutoMediaCenterConfigUtility implements IAutoConfigUtility {
+    private static final String LOG_TAG = AutoMediaCenterConfigUtility.class.getSimpleName();
+
+    // Media Center Config
+    private static final String MEDIA_CENTER_PACKAGE = "com.android.car.media";
+    private static final String MEDIA_ACTIVITY =
+            "com.android.bluetooth/"
+                    + "com.android.bluetooth.avrcpcontroller.BluetoothMediaBrowserService";
+
+    // Car Launcher For Reference Device
+    private static final String CAR_LAUNCHER_PACKAGE = "com.android.car.carlauncher";
+
+    // Config Map
+    private Map<String, AutoConfiguration> mMediaCenterConfigMap;
+
+    private static AutoMediaCenterConfigUtility sAutoMediaCenterConfigUtility = null;
+
+    private AutoMediaCenterConfigUtility() {
+        // Initialize Media Center Config Map
+        mMediaCenterConfigMap = new HashMap<String, AutoConfiguration>();
+    }
+
+    /** Get Auto Media Center Config Utility Instance */
+    public static IAutoConfigUtility getInstance() {
+        if (sAutoMediaCenterConfigUtility == null) {
+            sAutoMediaCenterConfigUtility = new AutoMediaCenterConfigUtility();
+        }
+        return sAutoMediaCenterConfigUtility;
+    }
+
+    /**
+     * Get Path for given menu
+     *
+     * @param menu Menu Name
+     */
+    public String[] getPath(String menu) {
+        throw new UnsupportedOperationException(
+                "Operation not supported for Media Center Application");
+    }
+
+    /**
+     * Add path for given Menu
+     *
+     * @param menu Menu Name
+     * @param path Path
+     */
+    public void addPath(String menu, String[] path) {
+        throw new UnsupportedOperationException(
+                "Operation not supported for Media Center Application");
+    }
+
+    /**
+     * Get available menu option
+     *
+     * @param menu Menu Name
+     */
+    public String[] getAvailableOptions(String menu) {
+        throw new UnsupportedOperationException(
+                "Operation not supported for Media Center Application");
+    }
+
+    /**
+     * Add available menu options
+     *
+     * @param menu Menu Name
+     * @param options Available Options
+     */
+    public void addAvailableOptions(String menu, String[] options) {
+        throw new UnsupportedOperationException(
+                "Operation not supported for Media Center Application");
+    }
+
+    /**
+     * Get Media Center Config Resource
+     *
+     * @param configName Configuration Name
+     * @param resourceName Resource Name
+     */
+    public AutoConfigResource getResourceConfiguration(String configName, String resourceName) {
+        if (mMediaCenterConfigMap.get(configName) == null) {
+            // Unknown Configuration
+            return null;
+        }
+        return mMediaCenterConfigMap.get(configName).getResource(resourceName);
+    }
+
+    /**
+     * Validate Media Center Configuration
+     *
+     * @param configName Configuration Name
+     */
+    public boolean isValidConfiguration(String configName) {
+        return (mMediaCenterConfigMap.get(configName) != null);
+    }
+
+    /**
+     * Validate Media Center Configuration Resource
+     *
+     * @param configName Configuration Name
+     * @param resourceName Resource Name
+     */
+    public boolean isValidResource(String configName, String resourceName) {
+        if (mMediaCenterConfigMap.get(configName) == null) {
+            return false;
+        }
+        return (mMediaCenterConfigMap.get(configName).getResource(resourceName) != null);
+    }
+
+    /**
+     * Validate Menu Options
+     *
+     * @param menu Menu Name
+     */
+    public boolean isValidOption(String menu) {
+        throw new UnsupportedOperationException(
+                "Operation not supported for Media Center Application");
+    }
+
+    /**
+     * Validate Menu Path
+     *
+     * @param menu Menu Name
+     */
+    public boolean isValidPath(String menu) {
+        throw new UnsupportedOperationException(
+                "Operation not supported for Media Center Application");
+    }
+
+    /** Load default config for Media Center Application ( Based on Reference Device ) */
+    public void loadDefaultConfig(Map<String, String> mApplicationConfigMap) {
+        // Default Media Center Application Config
+        loadDefaultMediaCenterAppConfig(mApplicationConfigMap);
+
+        // Default Media Center Screen Config
+        loadDefaultMediaCenterScreenConfig(mMediaCenterConfigMap);
+
+        // Default Media Center on Home Screen Config
+        loadDefaultMediaCenterOnHomeScreenConfig(mMediaCenterConfigMap);
+    }
+
+    private void loadDefaultMediaCenterAppConfig(Map<String, String> mApplicationConfigMap) {
+        // Add default Media Center package
+        mApplicationConfigMap.put(AutoConfigConstants.MEDIA_CENTER_PACKAGE, MEDIA_CENTER_PACKAGE);
+        // Add default Media Activity ( Default is for Bluetooth Audio )
+        mApplicationConfigMap.put(AutoConfigConstants.MEDIA_ACTIVITY, MEDIA_ACTIVITY);
+    }
+
+    private void loadDefaultMediaCenterScreenConfig(
+            Map<String, AutoConfiguration> mMediaCenterConfigMap) {
+        AutoConfiguration mediaCenterScreenConfiguration = new AutoConfiguration();
+        mediaCenterScreenConfiguration.addResource(
+                AutoConfigConstants.PLAY_PAUSE_BUTTON,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "play_pause_stop", MEDIA_CENTER_PACKAGE));
+        mediaCenterScreenConfiguration.addResource(
+                AutoConfigConstants.NEXT_BUTTON,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "skip_next", MEDIA_CENTER_PACKAGE));
+        mediaCenterScreenConfiguration.addResource(
+                AutoConfigConstants.PREVIOUS_BUTTON,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "skip_prev", MEDIA_CENTER_PACKAGE));
+        mediaCenterScreenConfiguration.addResource(
+                AutoConfigConstants.SHUFFLE_BUTTON,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "overflow_on", MEDIA_CENTER_PACKAGE));
+        mediaCenterScreenConfiguration.addResource(
+                AutoConfigConstants.PLAY_QUEUE_BUTTON,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "play_queue", MEDIA_CENTER_PACKAGE));
+        mediaCenterScreenConfiguration.addResource(
+                AutoConfigConstants.MINIMIZED_MEDIA_CONTROLS,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID,
+                        "minimized_playback_controls",
+                        MEDIA_CENTER_PACKAGE));
+        mediaCenterScreenConfiguration.addResource(
+                AutoConfigConstants.TRACK_NAME,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "title", MEDIA_CENTER_PACKAGE));
+        mediaCenterScreenConfiguration.addResource(
+                AutoConfigConstants.TRACK_NAME_MINIMIZED_CONTROL,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID,
+                        "minimized_control_bar_title",
+                        MEDIA_CENTER_PACKAGE));
+        mediaCenterScreenConfiguration.addResource(
+                AutoConfigConstants.BACK_BUTTON,
+                new AutoConfigResource(AutoConfigConstants.DESCRIPTION, "Back"));
+        mMediaCenterConfigMap.put(
+                AutoConfigConstants.MEDIA_CENTER_SCREEN, mediaCenterScreenConfiguration);
+    }
+
+    private void loadDefaultMediaCenterOnHomeScreenConfig(
+            Map<String, AutoConfiguration> mMediaCenterConfigMap) {
+        AutoConfiguration mediaCenterOnHomeScreenConfiguration = new AutoConfiguration();
+        mediaCenterOnHomeScreenConfiguration.addResource(
+                AutoConfigConstants.PLAY_PAUSE_BUTTON,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "play_pause_stop", CAR_LAUNCHER_PACKAGE));
+        mediaCenterOnHomeScreenConfiguration.addResource(
+                AutoConfigConstants.PREVIOUS_BUTTON,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "skip_prev", CAR_LAUNCHER_PACKAGE));
+        mediaCenterOnHomeScreenConfiguration.addResource(
+                AutoConfigConstants.NEXT_BUTTON,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "skip_next", CAR_LAUNCHER_PACKAGE));
+        mediaCenterOnHomeScreenConfiguration.addResource(
+                AutoConfigConstants.TRACK_NAME,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "title", CAR_LAUNCHER_PACKAGE));
+        mMediaCenterConfigMap.put(
+                AutoConfigConstants.MEDIA_CENTER_ON_HOME_SCREEN,
+                mediaCenterOnHomeScreenConfiguration);
+    }
+}
diff --git a/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoNotificationsConfigUtility.java b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoNotificationsConfigUtility.java
new file mode 100644
index 0000000..c04c763
--- /dev/null
+++ b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoNotificationsConfigUtility.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2021 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.helpers;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/** Configuration for Notifications Application */
+public class AutoNotificationsConfigUtility implements IAutoConfigUtility {
+    private static final String LOG_TAG = AutoNotificationsConfigUtility.class.getSimpleName();
+
+    // Notifications Config
+    private static final String SYSTEM_UI_PACKAGE = "com.android.systemui";
+    private static final String RECYCLER_VIEW_CLASS = "androidx.recyclerview.widget.RecyclerView";
+    private static final String OPEN_NOTIFICATIONS_COMMAND = "service call statusbar 1";
+
+    // Config Map
+    private Map<String, AutoConfiguration> mNotificationConfigMap;
+
+    private static AutoNotificationsConfigUtility sAutoNotificationsConfigInstance = null;
+
+    private AutoNotificationsConfigUtility() {
+        // Initialize Notifications Config Map
+        mNotificationConfigMap = new HashMap<String, AutoConfiguration>();
+    }
+
+    /** Get instance of Auto Notifications Utility */
+    public static IAutoConfigUtility getInstance() {
+        if (sAutoNotificationsConfigInstance == null) {
+            sAutoNotificationsConfigInstance = new AutoNotificationsConfigUtility();
+        }
+        return sAutoNotificationsConfigInstance;
+    }
+
+    /**
+     * Get Path for given menu
+     *
+     * @param menu Menu Name
+     */
+    public String[] getPath(String menu) {
+        throw new UnsupportedOperationException(
+                "Operation not supported for Notifications Application");
+    }
+
+    /**
+     * Add path for given Menu
+     *
+     * @param menu Menu Name
+     * @param path Path
+     */
+    public void addPath(String menu, String[] path) {
+        throw new UnsupportedOperationException(
+                "Operation not supported for Notifications Application");
+    }
+
+    /**
+     * Get available menu option
+     *
+     * @param menu Menu Name
+     */
+    public String[] getAvailableOptions(String menu) {
+        throw new UnsupportedOperationException(
+                "Operation not supported for Notifications Application");
+    }
+
+    /**
+     * Add available menu options
+     *
+     * @param menu Menu Name
+     * @param options Available Options
+     */
+    public void addAvailableOptions(String menu, String[] options) {
+        throw new UnsupportedOperationException(
+                "Operation not supported for Notifications Application");
+    }
+
+    /**
+     * Get Notifications Config Resource
+     *
+     * @param configName Configuration Name
+     * @param resourceName Resource Name
+     */
+    public AutoConfigResource getResourceConfiguration(String configName, String resourceName) {
+        if (mNotificationConfigMap.get(configName) == null) {
+            // Unknown Configuration
+            return null;
+        }
+        return mNotificationConfigMap.get(configName).getResource(resourceName);
+    }
+
+    /**
+     * Validate Notifications Configuration
+     *
+     * @param configName Configuration Name
+     */
+    public boolean isValidConfiguration(String configName) {
+        return (mNotificationConfigMap.get(configName) != null);
+    }
+
+    /**
+     * Validate Notifications Configuration Resource
+     *
+     * @param configName Configuration Name
+     * @param resourceName Resource Name
+     */
+    public boolean isValidResource(String configName, String resourceName) {
+        if (mNotificationConfigMap.get(configName) == null) {
+            return false;
+        }
+        return (mNotificationConfigMap.get(configName).getResource(resourceName) != null);
+    }
+
+    /**
+     * Validate Menu Options
+     *
+     * @param menu Menu Name
+     */
+    public boolean isValidOption(String menu) {
+        throw new UnsupportedOperationException(
+                "Operation not supported for Notifications Application");
+    }
+
+    /**
+     * Validate Menu Path
+     *
+     * @param menu Menu Name
+     */
+    public boolean isValidPath(String menu) {
+        throw new UnsupportedOperationException(
+                "Operation not supported for Notifications Application");
+    }
+
+    /** Load default configuration for Notification application */
+    public void loadDefaultConfig(Map<String, String> mApplicationConfigMap) {
+        // Default Notification Application Config
+        loadDefaultNotificationAppConfig(mApplicationConfigMap);
+
+        // Default Expanded Notifications Screen Config
+        loadDefaultExpandedNotificationsConfig(mNotificationConfigMap);
+    }
+
+    private void loadDefaultNotificationAppConfig(Map<String, String> mApplicationConfigMap) {
+        // Add default command to open Notifications
+        mApplicationConfigMap.put(
+                AutoConfigConstants.OPEN_NOTIFICATIONS_COMMAND, OPEN_NOTIFICATIONS_COMMAND);
+        // Add default recyler view class
+        mApplicationConfigMap.put(AutoConfigConstants.RECYCLER_VIEW_CLASS, RECYCLER_VIEW_CLASS);
+    }
+
+    private void loadDefaultExpandedNotificationsConfig(
+            Map<String, AutoConfiguration> mNotificationConfigMap) {
+        AutoConfiguration expandedNotificationsConfig = new AutoConfiguration();
+        expandedNotificationsConfig.addResource(
+                AutoConfigConstants.NOTIFICATION_LIST,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "notifications", SYSTEM_UI_PACKAGE));
+        expandedNotificationsConfig.addResource(
+                AutoConfigConstants.NOTIFICATION_VIEW,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "notification_view", SYSTEM_UI_PACKAGE));
+        expandedNotificationsConfig.addResource(
+                AutoConfigConstants.CLEAR_ALL_BUTTON,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "clear_all_button", SYSTEM_UI_PACKAGE));
+        expandedNotificationsConfig.addResource(
+                AutoConfigConstants.STATUS_BAR,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "car_top_bar", SYSTEM_UI_PACKAGE));
+        expandedNotificationsConfig.addResource(
+                AutoConfigConstants.APP_ICON,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "app_icon", SYSTEM_UI_PACKAGE));
+        expandedNotificationsConfig.addResource(
+                AutoConfigConstants.APP_NAME,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "header_text", SYSTEM_UI_PACKAGE));
+        expandedNotificationsConfig.addResource(
+                AutoConfigConstants.NOTIFICATION_TITLE,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID,
+                        "notification_body_title",
+                        SYSTEM_UI_PACKAGE));
+        expandedNotificationsConfig.addResource(
+                AutoConfigConstants.NOTIFICATION_BODY,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID,
+                        "notification_body_content",
+                        SYSTEM_UI_PACKAGE));
+        expandedNotificationsConfig.addResource(
+                AutoConfigConstants.CARD_VIEW,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "card_view", SYSTEM_UI_PACKAGE));
+        mNotificationConfigMap.put(
+                AutoConfigConstants.EXPANDED_NOTIFICATIONS_SCREEN, expandedNotificationsConfig);
+    }
+}
diff --git a/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoSettingsConfigUtility.java b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoSettingsConfigUtility.java
new file mode 100644
index 0000000..e54c934
--- /dev/null
+++ b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoSettingsConfigUtility.java
@@ -0,0 +1,590 @@
+/*
+ * Copyright (C) 2021 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.helpers;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/** Configuration for Settings Application */
+public class AutoSettingsConfigUtility implements IAutoConfigUtility {
+    private static final String LOG_TAG = AutoSettingsConfigUtility.class.getSimpleName();
+
+    // Settings Config
+    private static final String SETTINGS_TITLE_TEXT = "Settings";
+    private static final String SETTING_APP_PACKAGE = "com.android.car.settings";
+    private static final String SETTING_RRO_PACKAGE = "com.android.car.settings.googlecarui.rro";
+    private static final String SETTING_INTELLIGENCE_PACKAGE = "com.android.settings.intelligence";
+    private static final String PERMISSIONS_PACKAGE = "com.android.permissioncontroller";
+    private static final String OPEN_SETTINGS_COMMAND = "am start -a android.settings.SETTINGS";
+    private static final String OPEN_QUICK_SETTINGS_COMMAND =
+            "am start -n "
+                    + "com.android.car.settings/"
+                    + "com.android.car.settings.common.CarSettingActivities$QuickSettingActivity";
+
+    // Config Maps
+    private Map<String, String[]> mSettingsOptionsMap;
+    private Map<String, String[]> mSettingsPathMap;
+    private Map<String, AutoConfiguration> mSettingsConfigMap;
+
+    private static AutoSettingsConfigUtility sAutoSettingsConfigInstance = null;
+
+    private AutoSettingsConfigUtility() {
+        // Initialize Settings Config Maps
+        mSettingsOptionsMap = new HashMap<String, String[]>();
+        mSettingsPathMap = new HashMap<String, String[]>();
+        mSettingsConfigMap = new HashMap<String, AutoConfiguration>();
+    }
+
+    /** Get instance of Auto Settings Utility */
+    public static IAutoConfigUtility getInstance() {
+        if (sAutoSettingsConfigInstance == null) {
+            sAutoSettingsConfigInstance = new AutoSettingsConfigUtility();
+        }
+        return sAutoSettingsConfigInstance;
+    }
+
+    /**
+     * Get Path for given Setting
+     *
+     * @param menu Setting Name
+     */
+    public String[] getPath(String menu) {
+        String[] settingPath = mSettingsPathMap.get(menu);
+        if (settingPath == null) {
+            settingPath = new String[] {menu};
+        }
+        return settingPath;
+    }
+
+    /**
+     * Add path for given Setting
+     *
+     * @param menu Setting Name
+     * @param path Path
+     */
+    public void addPath(String menu, String[] path) {
+        mSettingsPathMap.put(menu, path);
+    }
+
+    /**
+     * Get available menu options for given Setting
+     *
+     * @param menu Setting Name
+     */
+    public String[] getAvailableOptions(String menu) {
+        return mSettingsOptionsMap.get(menu);
+    }
+
+    /**
+     * Add available menu options for given Setting
+     *
+     * @param menu Setting Name
+     * @param options Available Options
+     */
+    public void addAvailableOptions(String menu, String[] options) {
+        mSettingsOptionsMap.put(menu, options);
+    }
+
+    /**
+     * Get Setting Config Resource
+     *
+     * @param configName Configuration Name
+     * @param resourceName Resource Name
+     */
+    public AutoConfigResource getResourceConfiguration(String configName, String resourceName) {
+        if (mSettingsConfigMap.get(configName) == null) {
+            // Unknown Configuration
+            return null;
+        }
+        return mSettingsConfigMap.get(configName).getResource(resourceName);
+    }
+
+    /**
+     * Validate Setting Configuration
+     *
+     * @param configName Configuration Name
+     */
+    public boolean isValidConfiguration(String configName) {
+        return (mSettingsConfigMap.get(configName) != null);
+    }
+
+    /**
+     * Validate Setting Configuration Resource
+     *
+     * @param configName Configuration Name
+     * @param resourceName Resource Name
+     */
+    public boolean isValidResource(String configName, String resourceName) {
+        if (mSettingsConfigMap.get(configName) == null) {
+            return false;
+        }
+        return (mSettingsConfigMap.get(configName).getResource(resourceName) != null);
+    }
+
+    /**
+     * Validate Setting Options
+     *
+     * @param menu Setting Name
+     */
+    public boolean isValidOption(String menu) {
+        return (mSettingsOptionsMap.get(menu) != null);
+    }
+
+    /**
+     * Validate Setting Path
+     *
+     * @param menu Setting Name
+     */
+    public boolean isValidPath(String menu) {
+        return (mSettingsPathMap.get(menu) != null);
+    }
+
+    /** Load default configuration for Settings application */
+    public void loadDefaultConfig(Map<String, String> mApplicationConfigMap) {
+        // Default Settings Application Config
+        loadDefaultSettingsAppConfig(mApplicationConfigMap);
+
+        // Default Setting Options
+        loadDefaultSettingOptions(mSettingsOptionsMap);
+
+        // Default Setting Paths
+        loadDefaultSettingPaths(mSettingsPathMap);
+
+        // Default Setting Resource Config
+        loadDefaultSettingResourceConfig(mSettingsConfigMap);
+    }
+
+    private void loadDefaultSettingsAppConfig(Map<String, String> mApplicationConfigMap) {
+        // Add default settings title text
+        mApplicationConfigMap.put(AutoConfigConstants.SETTINGS_TITLE_TEXT, SETTINGS_TITLE_TEXT);
+        // Add default settings package
+        mApplicationConfigMap.put(AutoConfigConstants.SETTINGS_PACKAGE, SETTING_APP_PACKAGE);
+        // Add default settings rro package
+        mApplicationConfigMap.put(AutoConfigConstants.SETTINGS_RRO_PACKAGE, SETTING_RRO_PACKAGE);
+        // Add default open settings (full settings) command
+        mApplicationConfigMap.put(AutoConfigConstants.OPEN_SETTINGS_COMMAND, OPEN_SETTINGS_COMMAND);
+        // Add default open quick settings command
+        mApplicationConfigMap.put(
+                AutoConfigConstants.OPEN_QUICK_SETTINGS_COMMAND, OPEN_QUICK_SETTINGS_COMMAND);
+        // Add default settings spli screen UI config
+        mApplicationConfigMap.put(AutoConfigConstants.SPLIT_SCREEN_UI, "TRUE");
+    }
+
+    private void loadDefaultSettingOptions(Map<String, String[]> mSettingsOptionsMap) {
+        mSettingsOptionsMap.put(
+                AutoConfigConstants.DISPLAY_SETTINGS, new String[] {"Brightness level"});
+        mSettingsOptionsMap.put(
+                AutoConfigConstants.SOUND_SETTINGS, new String[] {"Media volume", "Alarm volume"});
+        mSettingsOptionsMap.put(AutoConfigConstants.NETWORK_AND_INTERNET_SETTINGS, new String[] {});
+        mSettingsOptionsMap.put(AutoConfigConstants.BLUETOOTH_SETTINGS, new String[] {});
+        mSettingsOptionsMap.put(AutoConfigConstants.APPS_SETTINGS, new String[] {});
+        mSettingsOptionsMap.put(
+                AutoConfigConstants.DATE_AND_TIME_SETTINGS,
+                new String[] {"Set time automatically", "Set time zone automatically"});
+        mSettingsOptionsMap.put(
+                AutoConfigConstants.PROFILE_ACCOUNT_SETTINGS, new String[] {"Add a profile"});
+        mSettingsOptionsMap.put(
+                AutoConfigConstants.SYSTEM_SETTINGS, new String[] {"System update"});
+        mSettingsOptionsMap.put(AutoConfigConstants.SECURITY_SETTINGS, new String[] {});
+    }
+
+    private void loadDefaultSettingPaths(Map<String, String[]> mSettingsPathMap) {
+        mSettingsPathMap.put(AutoConfigConstants.DISPLAY_SETTINGS, new String[] {"Display"});
+        mSettingsPathMap.put(AutoConfigConstants.SOUND_SETTINGS, new String[] {"Sound"});
+        mSettingsPathMap.put(
+                AutoConfigConstants.NETWORK_AND_INTERNET_SETTINGS,
+                new String[] {"Network & internet"});
+        mSettingsPathMap.put(AutoConfigConstants.BLUETOOTH_SETTINGS, new String[] {"Bluetooth"});
+        mSettingsPathMap.put(AutoConfigConstants.APPS_SETTINGS, new String[] {"Apps"});
+        mSettingsPathMap.put(
+                AutoConfigConstants.DATE_AND_TIME_SETTINGS, new String[] {"System", "Date & time"});
+        mSettingsPathMap.put(
+                AutoConfigConstants.PROFILE_ACCOUNT_SETTINGS, new String[] {"Profiles & accounts"});
+        mSettingsPathMap.put(AutoConfigConstants.SYSTEM_SETTINGS, new String[] {"System"});
+        mSettingsPathMap.put(AutoConfigConstants.SECURITY_SETTINGS, new String[] {"Security"});
+    }
+
+    private void loadDefaultSettingResourceConfig(
+            Map<String, AutoConfiguration> mSettingsConfigMap) {
+        // Full Settings Config
+        loadDefaultFullSettingsConfig(mSettingsConfigMap);
+
+        // Quick Settings Config
+        loadDefaultQuickSettingsConfig(mSettingsConfigMap);
+
+        // Display Settings Config
+        loadDefaultDisplaySettingsConfig(mSettingsConfigMap);
+
+        // Sound Settings Config
+        loadDefaultSoundSettingsConfig(mSettingsConfigMap);
+
+        // Network And Internet Settings Config
+        loadDefaultNetworkSettingsConfig(mSettingsConfigMap);
+
+        // Bluetooth Settings Config
+        loadDefaultBluetoothSettingsConfig(mSettingsConfigMap);
+
+        // App and Notifications Settings Config
+        loadDefaultAppAndNotificationsSettingsConfig(mSettingsConfigMap);
+
+        // Date and Time Settings Config
+        loadDefaultDateAndTimeSettingsConfig(mSettingsConfigMap);
+
+        // System Settings Config
+        loadDefaultSystemSettingsConfig(mSettingsConfigMap);
+
+        // Account Settings Config
+        loadDefaultAccountSettingsConfig(mSettingsConfigMap);
+
+        // Security Settings Config
+        loadDefaultSecuritySettingsConfig(mSettingsConfigMap);
+    }
+
+    private void loadDefaultFullSettingsConfig(Map<String, AutoConfiguration> mSettingsConfigMap) {
+        AutoConfiguration fullSettingsConfiguration = new AutoConfiguration();
+        fullSettingsConfiguration.addResource(
+                AutoConfigConstants.PAGE_TITLE,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID,
+                        "car_ui_toolbar_title",
+                        SETTING_APP_PACKAGE));
+        fullSettingsConfiguration.addResource(
+                AutoConfigConstants.SEARCH,
+                new AutoConfigResource(AutoConfigConstants.DESCRIPTION, "Search"));
+        fullSettingsConfiguration.addResource(
+                AutoConfigConstants.SEARCH_BOX,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID,
+                        "car_ui_toolbar_search_bar",
+                        SETTING_INTELLIGENCE_PACKAGE));
+        fullSettingsConfiguration.addResource(
+                AutoConfigConstants.SEARCH_RESULTS,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID,
+                        "recycler_view",
+                        SETTING_INTELLIGENCE_PACKAGE));
+        fullSettingsConfiguration.addResource(
+                AutoConfigConstants.UP_BUTTON,
+                new AutoConfigResource(AutoConfigConstants.DESCRIPTION, "Scroll up"));
+        fullSettingsConfiguration.addResource(
+                AutoConfigConstants.DOWN_BUTTON,
+                new AutoConfigResource(AutoConfigConstants.DESCRIPTION, "Scroll down"));
+        mSettingsConfigMap.put(AutoConfigConstants.FULL_SETTINGS, fullSettingsConfiguration);
+    }
+
+    private void loadDefaultQuickSettingsConfig(Map<String, AutoConfiguration> mSettingsConfigMap) {
+        AutoConfiguration quickSettingsConfiguration = new AutoConfiguration();
+        quickSettingsConfiguration.addResource(
+                AutoConfigConstants.OPEN_MORE_SETTINGS,
+                new AutoConfigResource(
+                        AutoConfigConstants.DESCRIPTION, "More", SETTING_APP_PACKAGE));
+        quickSettingsConfiguration.addResource(
+                AutoConfigConstants.NIGHT_MODE,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Night mode"));
+        mSettingsConfigMap.put(AutoConfigConstants.QUICK_SETTINGS, quickSettingsConfiguration);
+    }
+
+    private void loadDefaultDisplaySettingsConfig(
+            Map<String, AutoConfiguration> mSettingsConfigMap) {
+        AutoConfiguration displaySettingsConfiguration = new AutoConfiguration();
+        displaySettingsConfiguration.addResource(
+                AutoConfigConstants.BRIGHTNESS_LEVEL,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "seekbar", SETTING_APP_PACKAGE));
+        mSettingsConfigMap.put(AutoConfigConstants.DISPLAY_SETTINGS, displaySettingsConfiguration);
+    }
+
+    private void loadDefaultSoundSettingsConfig(Map<String, AutoConfiguration> mSettingsConfigMap) {
+        AutoConfiguration soundSettingsConfiguration = new AutoConfiguration();
+        soundSettingsConfiguration.addResource(
+                AutoConfigConstants.SAVE_BUTTON,
+                new AutoConfigResource(AutoConfigConstants.DESCRIPTION, "Save"));
+        mSettingsConfigMap.put(AutoConfigConstants.SOUND_SETTINGS, soundSettingsConfiguration);
+    }
+
+    private void loadDefaultNetworkSettingsConfig(
+            Map<String, AutoConfiguration> mSettingsConfigMap) {
+        AutoConfiguration networkSettingsConfiguration = new AutoConfiguration();
+        networkSettingsConfiguration.addResource(
+                AutoConfigConstants.TOGGLE_WIFI,
+                new AutoConfigResource(AutoConfigConstants.DESCRIPTION, "Wi‑Fi toggle switch"));
+        networkSettingsConfiguration.addResource(
+                AutoConfigConstants.TOGGLE_HOTSPOT,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID,
+                        "car_ui_secondary_action_concrete",
+                        SETTING_APP_PACKAGE));
+        mSettingsConfigMap.put(
+                AutoConfigConstants.NETWORK_AND_INTERNET_SETTINGS, networkSettingsConfiguration);
+    }
+
+    private void loadDefaultBluetoothSettingsConfig(
+            Map<String, AutoConfiguration> mSettingsConfigMap) {
+        AutoConfiguration bluetoothSettingsConfiguration = new AutoConfiguration();
+        bluetoothSettingsConfiguration.addResource(
+                AutoConfigConstants.TOGGLE_BLUETOOTH,
+                new AutoConfigResource(AutoConfigConstants.DESCRIPTION, "Bluetooth toggle switch"));
+        mSettingsConfigMap.put(
+                AutoConfigConstants.BLUETOOTH_SETTINGS, bluetoothSettingsConfiguration);
+    }
+
+    private void loadDefaultAppAndNotificationsSettingsConfig(
+            Map<String, AutoConfiguration> mSettingsConfigMap) {
+        AutoConfiguration appsAndNotificationsSettingsConfiguration = new AutoConfiguration();
+        appsAndNotificationsSettingsConfiguration.addResource(
+                AutoConfigConstants.PERMISSIONS_PAGE_TITLE,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID,
+                        "car_ui_toolbar_title",
+                        PERMISSIONS_PACKAGE));
+        appsAndNotificationsSettingsConfiguration.addResource(
+                AutoConfigConstants.VIEW_ALL,
+                new AutoConfigResource(AutoConfigConstants.TEXT_CONTAINS, "View all"));
+        appsAndNotificationsSettingsConfiguration.addResource(
+                AutoConfigConstants.ENABLE_DISABLE_BUTTON,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "button1Text", SETTING_APP_PACKAGE));
+        appsAndNotificationsSettingsConfiguration.addResource(
+                AutoConfigConstants.DISABLE_BUTTON_TEXT,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Disable"));
+        appsAndNotificationsSettingsConfiguration.addResource(
+                AutoConfigConstants.ENABLE_BUTTON_TEXT,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Enable"));
+        appsAndNotificationsSettingsConfiguration.addResource(
+                AutoConfigConstants.DISABLE_APP_BUTTON,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "DISABLE APP"));
+        appsAndNotificationsSettingsConfiguration.addResource(
+                AutoConfigConstants.FORCE_STOP_BUTTON,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Force stop"));
+        appsAndNotificationsSettingsConfiguration.addResource(
+                AutoConfigConstants.OK_BUTTON,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "OK"));
+        appsAndNotificationsSettingsConfiguration.addResource(
+                AutoConfigConstants.PERMISSIONS_MENU,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Permissions?"));
+        appsAndNotificationsSettingsConfiguration.addResource(
+                AutoConfigConstants.ALLOW_BUTTON,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Allow"));
+        appsAndNotificationsSettingsConfiguration.addResource(
+                AutoConfigConstants.DONT_ALLOW_BUTTON,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Don’t allow"));
+        appsAndNotificationsSettingsConfiguration.addResource(
+                AutoConfigConstants.DONT_ALLOW_ANYWAY_BUTTON,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Don’t allow anyway"));
+        mSettingsConfigMap.put(
+                AutoConfigConstants.APPS_SETTINGS, appsAndNotificationsSettingsConfiguration);
+    }
+
+    private void loadDefaultDateAndTimeSettingsConfig(
+            Map<String, AutoConfiguration> mSettingsConfigMap) {
+        AutoConfiguration dateAndTimeSettingsConfiguration = new AutoConfiguration();
+        dateAndTimeSettingsConfiguration.addResource(
+                AutoConfigConstants.SET_TIME_AUTOMATICALLY,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Set time automatically"));
+        dateAndTimeSettingsConfiguration.addResource(
+                AutoConfigConstants.SET_TIME_ZONE_AUTOMATICALLY,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Set time zone automatically"));
+        dateAndTimeSettingsConfiguration.addResource(
+                AutoConfigConstants.SET_DATE,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Set date"));
+        dateAndTimeSettingsConfiguration.addResource(
+                AutoConfigConstants.SET_TIME,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Set time"));
+        dateAndTimeSettingsConfiguration.addResource(
+                AutoConfigConstants.SELECT_TIME_ZONE,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Select time zone"));
+        dateAndTimeSettingsConfiguration.addResource(
+                AutoConfigConstants.USE_24_HOUR_FORMAT,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Use 24-hour format"));
+        dateAndTimeSettingsConfiguration.addResource(
+                AutoConfigConstants.OK_BUTTON,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID,
+                        "toolbar_menu_item_0",
+                        SETTING_APP_PACKAGE));
+        dateAndTimeSettingsConfiguration.addResource(
+                AutoConfigConstants.NUMBER_PICKER_WIDGET,
+                new AutoConfigResource(AutoConfigConstants.CLASS, "android.widget.NumberPicker"));
+        dateAndTimeSettingsConfiguration.addResource(
+                AutoConfigConstants.EDIT_TEXT_WIDGET,
+                new AutoConfigResource(AutoConfigConstants.CLASS, "android.widget.EditText"));
+        mSettingsConfigMap.put(
+                AutoConfigConstants.DATE_AND_TIME_SETTINGS, dateAndTimeSettingsConfiguration);
+    }
+
+    private void loadDefaultSystemSettingsConfig(
+            Map<String, AutoConfiguration> mSettingsConfigMap) {
+        AutoConfiguration systemSettingsConfiguration = new AutoConfiguration();
+        systemSettingsConfiguration.addResource(
+                AutoConfigConstants.ABOUT_MENU,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "About"));
+        systemSettingsConfiguration.addResource(
+                AutoConfigConstants.RESET_OPTIONS_MENU,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Reset options"));
+        systemSettingsConfiguration.addResource(
+                AutoConfigConstants.LANGUAGES_AND_INPUT_MENU,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Languages & input"));
+        systemSettingsConfiguration.addResource(
+                AutoConfigConstants.DEVICE_MODEL,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Model & hardware"));
+        systemSettingsConfiguration.addResource(
+                AutoConfigConstants.ANDROID_VERSION,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Android version"));
+        systemSettingsConfiguration.addResource(
+                AutoConfigConstants.ANDROID_SECURITY_PATCH_LEVEL,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Android security patch level"));
+        systemSettingsConfiguration.addResource(
+                AutoConfigConstants.KERNEL_VERSION,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Kernel Version"));
+        systemSettingsConfiguration.addResource(
+                AutoConfigConstants.BUILD_NUMBER,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Build number"));
+        systemSettingsConfiguration.addResource(
+                AutoConfigConstants.RECYCLER_VIEW_WIDGET,
+                new AutoConfigResource(
+                        AutoConfigConstants.CLASS, "androidx.recyclerview.widget.RecyclerView"));
+        systemSettingsConfiguration.addResource(
+                AutoConfigConstants.RESET_NETWORK,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Reset network"));
+        systemSettingsConfiguration.addResource(
+                AutoConfigConstants.RESET_SETTINGS,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "RESET SETTINGS"));
+        systemSettingsConfiguration.addResource(
+                AutoConfigConstants.RESET_APP_PREFERENCES,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Reset app preferences"));
+        systemSettingsConfiguration.addResource(
+                AutoConfigConstants.RESET_APPS,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "RESET APPS"));
+        systemSettingsConfiguration.addResource(
+                AutoConfigConstants.LANGUAGES_MENU,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Languages"));
+        // Selected Language Spanish by default
+        systemSettingsConfiguration.addResource(
+                AutoConfigConstants.LANGUAGES_MENU_IN_SELECTED_LANGUAGE,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Idiomas"));
+        mSettingsConfigMap.put(AutoConfigConstants.SYSTEM_SETTINGS, systemSettingsConfiguration);
+    }
+
+    private void loadDefaultAccountSettingsConfig(
+            Map<String, AutoConfiguration> mSettingsConfigMap) {
+        AutoConfiguration accountSettingsConfiguration = new AutoConfiguration();
+        accountSettingsConfiguration.addResource(
+                AutoConfigConstants.ADD_ACCOUNT,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "ADD ACCOUNT"));
+        accountSettingsConfiguration.addResource(
+                AutoConfigConstants.ADD_GOOGLE_ACCOUNT,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Google"));
+        accountSettingsConfiguration.addResource(
+                AutoConfigConstants.SIGN_IN_ON_CAR_SCREEN,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Sign in on car screen"));
+        accountSettingsConfiguration.addResource(
+                AutoConfigConstants.GOOGLE_SIGN_IN_SCREEN,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Sign in to your Google Account"));
+        accountSettingsConfiguration.addResource(
+                AutoConfigConstants.ENTER_EMAIL,
+                new AutoConfigResource(AutoConfigConstants.CLASS, "android.widget.EditText"));
+        accountSettingsConfiguration.addResource(
+                AutoConfigConstants.ENTER_PASSWORD,
+                new AutoConfigResource(AutoConfigConstants.CLASS, "android.widget.EditText"));
+        accountSettingsConfiguration.addResource(
+                AutoConfigConstants.NEXT_BUTTON,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Next"));
+        accountSettingsConfiguration.addResource(
+                AutoConfigConstants.DONE_BUTTON,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Done"));
+        accountSettingsConfiguration.addResource(
+                AutoConfigConstants.REMOVE_BUTTON,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Remove"));
+        accountSettingsConfiguration.addResource(
+                AutoConfigConstants.REMOVE_ACCOUNT_BUTTON,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Remove Account"));
+        accountSettingsConfiguration.addResource(
+                AutoConfigConstants.MANAGE_OTHER_PROFILES,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Manage other profiles"));
+        accountSettingsConfiguration.addResource(
+                AutoConfigConstants.ADD_PROFILE,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Add.*profile"));
+        accountSettingsConfiguration.addResource(
+                AutoConfigConstants.OK, new AutoConfigResource(AutoConfigConstants.TEXT, "OK"));
+        accountSettingsConfiguration.addResource(
+                AutoConfigConstants.DELETE_SELF,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Delete this profile"));
+        accountSettingsConfiguration.addResource(
+                AutoConfigConstants.DELETE,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Delete"));
+        accountSettingsConfiguration.addResource(
+                AutoConfigConstants.MAKE_ADMIN,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Make Admin"));
+        accountSettingsConfiguration.addResource(
+                AutoConfigConstants.MAKE_ADMIN_CONFIRM,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Yes, make admin"));
+        accountSettingsConfiguration.addResource(
+                AutoConfigConstants.TIME_PATTERN,
+                new AutoConfigResource(
+                        AutoConfigConstants.TEXT, "(1[012]|[1-9]):[0-5][0-9](\\s)?.*"));
+        mSettingsConfigMap.put(
+                AutoConfigConstants.PROFILE_ACCOUNT_SETTINGS, accountSettingsConfiguration);
+    }
+
+    private void loadDefaultSecuritySettingsConfig(
+            Map<String, AutoConfiguration> mSettingsConfigMap) {
+        AutoConfiguration securitySettingsConfiguration = new AutoConfiguration();
+        securitySettingsConfiguration.addResource(
+                AutoConfigConstants.TITLE,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID,
+                        "car_ui_toolbar_title",
+                        SETTING_APP_PACKAGE));
+        securitySettingsConfiguration.addResource(
+                AutoConfigConstants.CHOOSE_LOCK_TYPE,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Choose a lock type"));
+        securitySettingsConfiguration.addResource(
+                AutoConfigConstants.PROFILE_LOCK,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Profile lock"));
+        securitySettingsConfiguration.addResource(
+                AutoConfigConstants.LOCK_TYPE_PASSWORD,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Password"));
+        securitySettingsConfiguration.addResource(
+                AutoConfigConstants.LOCK_TYPE_PIN,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "PIN"));
+        securitySettingsConfiguration.addResource(
+                AutoConfigConstants.LOCK_TYPE_NONE,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "None"));
+        securitySettingsConfiguration.addResource(
+                AutoConfigConstants.CONTINUE_BUTTON,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Continue"));
+        securitySettingsConfiguration.addResource(
+                AutoConfigConstants.CONFIRM_BUTTON,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Confirm"));
+        securitySettingsConfiguration.addResource(
+                AutoConfigConstants.ENTER_PASSWORD,
+                new AutoConfigResource(AutoConfigConstants.CLASS, "android.widget.EditText"));
+        securitySettingsConfiguration.addResource(
+                AutoConfigConstants.PIN_PAD,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "pin_pad", SETTING_APP_PACKAGE));
+        securitySettingsConfiguration.addResource(
+                AutoConfigConstants.ENTER_PIN_BUTTON,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "key_enter", SETTING_APP_PACKAGE));
+        securitySettingsConfiguration.addResource(
+                AutoConfigConstants.REMOVE_BUTTON,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Remove"));
+        mSettingsConfigMap.put(
+                AutoConfigConstants.SECURITY_SETTINGS, securitySettingsConfiguration);
+    }
+}
diff --git a/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoUtility.java b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoUtility.java
new file mode 100644
index 0000000..1fc66bc
--- /dev/null
+++ b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoUtility.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2021 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.helpers;
+
+import android.app.Instrumentation;
+import android.os.SystemClock;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import androidx.test.InstrumentationRegistry;
+
+import java.util.regex.Pattern;
+
+public class AutoUtility {
+    private static final String CARLAUNCHER_PACKAGE = "com.android.car.carlauncher";
+    private static final String DO_NOT_SHOW_AGAIN = "DO NOT SHOW AGAIN";
+
+    private static final BySelector DISMISS_INITIAL_USER_NOTICE_SELECTOR =
+            By.clickable(true).text(Pattern.compile(DO_NOT_SHOW_AGAIN, Pattern.CASE_INSENSITIVE));
+
+    private static final int UI_WAIT_TIME = 5000;
+    private static final int UI_WAIT_TIME_TWENTY_SEC = 20000;
+
+    private static Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation();
+    private static UiDevice mDevice = UiDevice.getInstance(mInstrumentation);
+
+    public static void exitSuw() {
+        mDevice.pressHome();
+        dismissInitialUserNoticeUiServiceDialog();
+        SystemClock.sleep(UI_WAIT_TIME_TWENTY_SEC);
+    }
+
+    private static void dismissInitialUserNoticeUiServiceDialog() {
+        mDevice.pressHome();
+        UiObject2 object =
+                mDevice.wait(Until.findObject(DISMISS_INITIAL_USER_NOTICE_SELECTOR), UI_WAIT_TIME);
+        if (object != null) {
+            object.click();
+        }
+    }
+}
diff --git a/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/interfaces/IAutoConfigUtility.java b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/interfaces/IAutoConfigUtility.java
new file mode 100644
index 0000000..d358c01
--- /dev/null
+++ b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/interfaces/IAutoConfigUtility.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2021 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.helpers;
+
+import java.util.Map;
+
+public interface IAutoConfigUtility {
+    /** This method is used loading default configuration. */
+    void loadDefaultConfig(Map<String, String> mApplicationConfigMap);
+
+    /** This method is used to get the resource configuration. */
+    AutoConfigResource getResourceConfiguration(String configName, String resourceName);
+
+    /** This method is used to get an array of available options for given menu. */
+    String[] getAvailableOptions(String menu);
+
+    /** This method is used to add an array of available options for given menu. */
+    void addAvailableOptions(String menu, String[] options);
+
+    /** This method is used to get path for given menu. */
+    String[] getPath(String menu);
+
+    /** This method is used to add path for given menu. */
+    void addPath(String menu, String[] options);
+
+    /** This method is used validate configuration. */
+    boolean isValidConfiguration(String configName);
+
+    /** This method is used validate configuration resource. */
+    boolean isValidResource(String configName, String resourceName);
+
+    /** This method is used validate options. */
+    boolean isValidOption(String menu);
+
+    /** This method is used validate path. */
+    boolean isValidPath(String menu);
+}
diff --git a/libraries/car-helpers/multiuser-helper/src/android/platform/helpers/MultiUserHelper.java b/libraries/car-helpers/multiuser-helper/src/android/platform/helpers/MultiUserHelper.java
index dd55a61..16ccc71 100644
--- a/libraries/car-helpers/multiuser-helper/src/android/platform/helpers/MultiUserHelper.java
+++ b/libraries/car-helpers/multiuser-helper/src/android/platform/helpers/MultiUserHelper.java
@@ -21,6 +21,7 @@
 import android.car.user.CarUserManager;
 import android.car.user.CarUserManager.UserLifecycleListener;
 import android.car.user.UserSwitchResult;
+import android.car.util.concurrent.AsyncFuture;
 import android.content.Context;
 import android.content.pm.UserInfo;
 import android.os.SystemClock;
@@ -29,8 +30,6 @@
 
 import androidx.test.InstrumentationRegistry;
 
-import com.android.internal.infra.AndroidFuture;
-
 import java.io.IOException;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
@@ -114,7 +113,7 @@
                     }
                 };
         mCarUserManager.addListener(Runnable::run, userSwitchListener);
-        AndroidFuture<UserSwitchResult> future = mCarUserManager.switchUser(id);
+        AsyncFuture<UserSwitchResult> future = mCarUserManager.switchUser(id);
         UserSwitchResult result = null;
         try {
             result = future.get(USER_SWITCH_TIMEOUT_SECOND, TimeUnit.SECONDS);
@@ -174,7 +173,7 @@
     @Nullable
     public UserInfo getUserByName(String name) {
         return mUserManager
-                .getUsers(/* excludeDying= */ true)
+                .getAliveUsers()
                 .stream()
                 .filter(user -> user.name.equals(name))
                 .findFirst()
diff --git a/libraries/collectors-helper/lyric/Android.bp b/libraries/collectors-helper/lyric/Android.bp
new file mode 100644
index 0000000..811caca
--- /dev/null
+++ b/libraries/collectors-helper/lyric/Android.bp
@@ -0,0 +1,36 @@
+// Copyright (C) 2021 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.
+
+// Used for collecting Lyric specific metrics..
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+    name: "lyric-metric-helper",
+    defaults: ["tradefed_errorprone_defaults"],
+
+    srcs: [
+        "src/**/*.java",
+    ],
+
+    static_libs: [
+        "androidx.test.runner",
+        "androidx.test.uiautomator",
+        "collector-helper-utilities",
+        "guava",
+    ],
+
+    sdk_version: "current",
+}
diff --git a/libraries/collectors-helper/lyric/src/com/android/helpers/LyricCpuUtilizationHelper.java b/libraries/collectors-helper/lyric/src/com/android/helpers/LyricCpuUtilizationHelper.java
new file mode 100644
index 0000000..94cb666
--- /dev/null
+++ b/libraries/collectors-helper/lyric/src/com/android/helpers/LyricCpuUtilizationHelper.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2021 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 android.util.Log;
+import androidx.annotation.VisibleForTesting;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.uiautomator.UiDevice;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * This is a collector helper that collects the dumpsys output for specified services and puts them
+ * into files.
+ */
+public class LyricCpuUtilizationHelper implements ICollectorHelper<Double> {
+
+    private static final String TAG = LyricCpuUtilizationHelper.class.getSimpleName();
+
+    private static final String DUMPSYS_CMD = "dumpsys media.camera";
+
+    private static final Pattern METRIC_REGEX_PATTERN =
+            Pattern.compile("((?:-|)\\d+\\.?\\d*)([a-zA-Z]*)");
+
+    private static final String METRIC_REGEX = "((?:-|)\\d+\\.?\\d*[a-zA-Z]*)";
+
+    private static final Pattern CPU_USAGE_PATTERN =
+            Pattern.compile(
+                    String.format(
+                            "\\[(?:>|\\s)] p\\d+ (.*) after (\\d+) invocations\n"
+                                + "  System CPU: %1$s \\(Max: %1$s Min: %1$s\\)\n"
+                                + "  User CPU: %1$s \\(Max: %1$s Min: %1$s\\)\n"
+                                + "  Wall Time: %1$s \\(Max: %1$s Min: %1$s\\)\n"
+                                + "  Processing frequency %1$s\\(Hz\\), Duty cycle: %1$s%%\n"
+                                + "  Average Graph Runner Scheduling Delay is %1$s \\(Max: %1$s"
+                                + " Min: %1$s\\)  with %1$s of it due to waiting for a particular"
+                                + " thread/reserialization\n"
+                                + "  Spent %1$s%% of each ProcessInput \\(Mean duration: %1$s Max:"
+                                + " %1$s Min: %1$s\\) waiting for something \\(or on"
+                                + " overhead\\).\n"
+                                + "  On average per process call, there were %1$s \\(Max: %1$s"
+                                + " Min: %1$s\\)  reserializations into another queue and %1$s"
+                                + " \\(Max: %1$s Min: %1$s\\)  wakes and %1$s involuntary and %1$s"
+                                + " voluntary context switches \\(\\+GraphRunner overhead\\).",
+                            METRIC_REGEX));
+
+    private static final String METRIC_KEY = "cpu_util_%s_%s";
+
+    private UiDevice mUiDevice;
+
+    @Override
+    public boolean startCollecting() {
+        mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        return true;
+    }
+
+    @Override
+    public Map<String, Double> getMetrics() {
+        try {
+            String res = mUiDevice.executeShellCommand(DUMPSYS_CMD);
+            return processOutput(res);
+        } catch (IOException e) {
+            Log.e(TAG, "Failed to collect Lyric CPU metrics.");
+        }
+        return new HashMap<>();
+    }
+
+    @Override
+    public boolean stopCollecting() {
+        return true;
+    }
+
+    @VisibleForTesting
+    static Map<String, Double> processOutput(String output) {
+        Map<String, Double> metrics = new HashMap<>();
+        Matcher matcher = CPU_USAGE_PATTERN.matcher(output);
+        while (matcher.find()) {
+            metrics.putAll(processMatch(matcher));
+        }
+        return metrics;
+    }
+
+    private static Map<String, Double> processMatch(Matcher matcher) {
+        Map<String, Double> metrics = new HashMap<>();
+        String node = matcher.group(1).replace(":", "-");
+        metrics.put(
+                String.format(METRIC_KEY, node, "number_of_invocations"),
+                Double.parseDouble(matcher.group(2)));
+        metrics.put(String.format(METRIC_KEY, node, "system_time"), parseTime(matcher.group(3)));
+        metrics.put(
+                String.format(METRIC_KEY, node, "system_time_max"), parseTime(matcher.group(4)));
+        metrics.put(
+                String.format(METRIC_KEY, node, "system_time_min"), parseTime(matcher.group(5)));
+        metrics.put(String.format(METRIC_KEY, node, "user_time"), parseTime(matcher.group(6)));
+        metrics.put(String.format(METRIC_KEY, node, "user_time_max"), parseTime(matcher.group(7)));
+        metrics.put(String.format(METRIC_KEY, node, "user_time_min"), parseTime(matcher.group(8)));
+        metrics.put(String.format(METRIC_KEY, node, "wall_time"), parseTime(matcher.group(9)));
+        metrics.put(String.format(METRIC_KEY, node, "wall_time_max"), parseTime(matcher.group(10)));
+        metrics.put(String.format(METRIC_KEY, node, "wall_time_min"), parseTime(matcher.group(11)));
+
+        return metrics;
+    }
+
+    /** Takes a time string and returns the value in milliseconds. */
+    private static Double parseTime(String timeString) {
+        Matcher matcher = METRIC_REGEX_PATTERN.matcher(timeString);
+        if (!matcher.find()) {
+            throw new IllegalArgumentException("Time string does not match the expected format.");
+        }
+        double value = Double.parseDouble(matcher.group(1));
+        String unit = matcher.group(2);
+        switch (unit) {
+            case "us":
+                value /= 1000;
+                break;
+            case "s":
+                value *= 1000;
+                break;
+            default:
+                break;
+        }
+        return value;
+    }
+}
diff --git a/libraries/collectors-helper/lyric/test/Android.bp b/libraries/collectors-helper/lyric/test/Android.bp
new file mode 100644
index 0000000..253cdbb
--- /dev/null
+++ b/libraries/collectors-helper/lyric/test/Android.bp
@@ -0,0 +1,34 @@
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+    name: "lyric-helper-test",
+    defaults: ["tradefed_errorprone_defaults"],
+
+    srcs: ["src/**/*.java"],
+
+    static_libs: [
+        "androidx.test.runner",
+        "junit",
+        "mockito-target",
+        "lyric-metric-helper",
+        "truth-prebuilt",
+    ],
+
+    sdk_version: "current",
+}
diff --git a/libraries/collectors-helper/lyric/test/src/com/android/helpers/LyricCpuUtilizationHelperTest.java b/libraries/collectors-helper/lyric/test/src/com/android/helpers/LyricCpuUtilizationHelperTest.java
new file mode 100644
index 0000000..b246331
--- /dev/null
+++ b/libraries/collectors-helper/lyric/test/src/com/android/helpers/LyricCpuUtilizationHelperTest.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2021 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.google.common.truth.Truth.assertThat;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Map;
+
+/**
+ * Android unit test for {@link LyricCpuUtilizationHelper}
+ *
+ * <p>To run: atest CollectorsHelperTest:com.android.helpers.tests.LyricCpuUtilizationHelper
+ */
+@RunWith(AndroidJUnit4.class)
+public class LyricCpuUtilizationHelperTest {
+
+    private static final String METRIC_KEY = "cpu_util_%s_%s";
+
+    private static final Double METRIC_VALUE_THRESHOLD = 0.00001;
+
+    @Test
+    public void testProcessOutput() {
+
+        String testString =
+                "[>] p2 retiming:empty_group after 2994 invocations\n"
+                    + "  System CPU: 222.601us (Max: 1.819ms Min: 0)\n"
+                    + "  User CPU: 698.4612us (Max: 2.023ms Min: 0)\n"
+                    + "  Wall Time: 50.36290575ms (Max: 61.2205ms Min: 50.035889ms)\n"
+                    + "  Processing frequency 19.7906(Hz), Duty cycle: 99.671%\n"
+                    + "  Average Graph Runner Scheduling Delay is 132.352us (Max: 10.517701ms Min:"
+                    + " 20.956us)  with 0 of it due to waiting for a particular"
+                    + " thread/reserialization\n"
+                    + "  Spent 98.1711% of each ProcessInput (Mean duration: 50.36290575ms Max:"
+                    + " 61.2205ms Min: 50.035889ms) waiting for something (or on overhead).\n"
+                    + "  On average per process call, there were 0 (Max: 0 Min: 0) "
+                    + " reserializations into another queue and 0.999664 (Max: 1 Min: 0)  wakes"
+                    + " and 0.830996 involuntary and 12.9198 voluntary context switches"
+                    + " (+GraphRunner overhead).";
+
+        Map<String, Double> metrics = LyricCpuUtilizationHelper.processOutput(testString);
+
+        String node = "retiming-empty_group";
+        assertThat(metrics.get(String.format(METRIC_KEY, node, "number_of_invocations")))
+                .isWithin(METRIC_VALUE_THRESHOLD)
+                .of(2994);
+        assertThat(metrics.get(String.format(METRIC_KEY, node, "user_time")))
+                .isWithin(METRIC_VALUE_THRESHOLD)
+                .of(0.6984612);
+        assertThat(metrics.get(String.format(METRIC_KEY, node, "user_time")))
+                .isWithin(METRIC_VALUE_THRESHOLD)
+                .of(0.6984612);
+        assertThat(metrics.get(String.format(METRIC_KEY, node, "user_time_max")))
+                .isWithin(METRIC_VALUE_THRESHOLD)
+                .of(2.023);
+        assertThat(metrics.get(String.format(METRIC_KEY, node, "user_time_min")))
+                .isWithin(METRIC_VALUE_THRESHOLD)
+                .of(0);
+        assertThat(metrics.get(String.format(METRIC_KEY, node, "system_time")))
+                .isWithin(METRIC_VALUE_THRESHOLD)
+                .of(0.222601);
+        assertThat(metrics.get(String.format(METRIC_KEY, node, "system_time_max")))
+                .isWithin(METRIC_VALUE_THRESHOLD)
+                .of(1.819);
+        assertThat(metrics.get(String.format(METRIC_KEY, node, "system_time_min")))
+                .isWithin(METRIC_VALUE_THRESHOLD)
+                .of(0);
+        assertThat(metrics.get(String.format(METRIC_KEY, node, "wall_time")))
+                .isWithin(METRIC_VALUE_THRESHOLD)
+                .of(50.36290575);
+        assertThat(metrics.get(String.format(METRIC_KEY, node, "wall_time_max")))
+                .isWithin(METRIC_VALUE_THRESHOLD)
+                .of(61.2205);
+        assertThat(metrics.get(String.format(METRIC_KEY, node, "wall_time_min")))
+                .isWithin(METRIC_VALUE_THRESHOLD)
+                .of(50.035889);
+    }
+}
diff --git a/libraries/collectors-helper/memory/src/com/android/helpers/MemhogHelper.java b/libraries/collectors-helper/memory/src/com/android/helpers/MemhogHelper.java
new file mode 100644
index 0000000..32c55af
--- /dev/null
+++ b/libraries/collectors-helper/memory/src/com/android/helpers/MemhogHelper.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2021 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 android.os.SystemClock;
+import android.util.Log;
+
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import java.io.IOException;
+import java.util.Locale;
+
+/** MemhogHelper is a helper for starting and stopping a memhog process. */
+public class MemhogHelper {
+    private static final String TAG = MemhogHelper.class.getSimpleName();
+    // Command to make memhog executable.
+    private static final String CHMOD_CMD = "chmod 755 %s";
+    // Location of memhog on the device, defined in heavyweight-memhog.gcl.
+    private static final String MEMHOG_FILE_PATH = "/data/local/tmp/memhog";
+    private static final String MEMHOG_PROC_ID_CMD = "pidof memhog";
+    private static final String MEMHOG_START_CMD =
+            "%s -m %d -s -1 -M -r 1 </dev/null &>/dev/null &";
+    private static final String MEMHOG_STOP_CMD = "pkill memhog";
+
+    private static final int MEMHOG_START_RETRY_COUNT = 3;
+    private static final int MEMHOG_START_RETRY_TIME_MS = 1000;
+
+    private static final int MEMHOG_STOP_RETRY_COUNT = 5;
+    private static final int MEMHOG_STOP_RETRY_TIME_MS = 1000;
+
+    private UiDevice mUiDevice;
+
+    // Starts memhog with the specified amount of memory in bytes.
+    public boolean startMemhog(long memorySizeBytes) {
+        mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        Log.i(TAG, "Starting memhog.");
+        new Thread() {
+            @Override
+            public void run() {
+                UiDevice uiDevice =
+                        UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+                try {
+                    // Makes memhog executable, then runs it.
+                    uiDevice.executeShellCommand(String.format(CHMOD_CMD, MEMHOG_FILE_PATH));
+                    uiDevice.executeShellCommand(
+                            String.format(
+                                    Locale.US,
+                                    MEMHOG_START_CMD,
+                                    MEMHOG_FILE_PATH,
+                                    memorySizeBytes));
+                } catch (IOException e) {
+                    Log.e(TAG, "Failed to start memhog: " + e.getMessage());
+                }
+            }
+        }.start();
+
+        // Waits for the memhog process to be found.
+        int waitCount = 0;
+        while (!isMemhogRunning()) {
+            if (waitCount < MEMHOG_START_RETRY_COUNT) {
+                SystemClock.sleep(MEMHOG_START_RETRY_TIME_MS);
+                waitCount++;
+                continue;
+            }
+            Log.e(TAG, "Memhog process not found; failed to start memhog.");
+            return false;
+        }
+        Log.i(TAG, "Successfully started memhog.");
+        return true;
+    }
+
+    // Stops the memhog process with pkill.
+    public boolean stopMemhog() {
+        if (!isMemhogRunning()) {
+            Log.e(TAG, "stopMemhog() was called, but memhog is not running.");
+            return false;
+        }
+        try {
+            mUiDevice.executeShellCommand(MEMHOG_STOP_CMD);
+        } catch (IOException e) {
+            Log.e(TAG, "Failed to stop memhog: " + e);
+            return false;
+        }
+
+        // Waits for the memhog process to disappear.
+        int waitCount = 0;
+        while (isMemhogRunning()) {
+            if (waitCount < MEMHOG_STOP_RETRY_COUNT) {
+                SystemClock.sleep(MEMHOG_STOP_RETRY_TIME_MS);
+                waitCount++;
+                continue;
+            }
+            Log.e(TAG, "Memhog process still present; failed to stop memhog.");
+            return false;
+        }
+        Log.i(TAG, "Successfully stopped memhog.");
+        return true;
+    }
+
+    // Utility method for checking if memhog is running.
+    private boolean isMemhogRunning() {
+        try {
+            String memhogProcId = mUiDevice.executeShellCommand(MEMHOG_PROC_ID_CMD);
+            Log.i(TAG, String.format("Memhog pid - %s", memhogProcId));
+            if (memhogProcId.isEmpty()) {
+                return false;
+            }
+        } catch (IOException e) {
+            Log.e(TAG, "Unable to check memhog status: " + e.getMessage());
+            return false;
+        }
+        return true;
+    }
+}
diff --git a/libraries/collectors-helper/perfetto/test/Android.bp b/libraries/collectors-helper/perfetto/test/Android.bp
index c045c53..07f1a5e 100644
--- a/libraries/collectors-helper/perfetto/test/Android.bp
+++ b/libraries/collectors-helper/perfetto/test/Android.bp
@@ -24,7 +24,7 @@
 
     static_libs: [
         "androidx.test.runner",
-        "android-support-test",
+        "androidx.test.rules",
         "perfetto-helper",
         "junit",
 
diff --git a/libraries/collectors-helper/simpleperf/src/com/android/helpers/SimpleperfHelper.java b/libraries/collectors-helper/simpleperf/src/com/android/helpers/SimpleperfHelper.java
index a7a5b44..679172f 100644
--- a/libraries/collectors-helper/simpleperf/src/com/android/helpers/SimpleperfHelper.java
+++ b/libraries/collectors-helper/simpleperf/src/com/android/helpers/SimpleperfHelper.java
@@ -36,8 +36,7 @@
     private static final String LOG_TAG = SimpleperfHelper.class.getSimpleName();
     private static final String SIMPLEPERF_TMP_FILE_PATH = "/data/local/tmp/perf.data";
 
-    private static final String SIMPLEPERF_START_CMD =
-            "simpleperf record -o %s -g --post-unwind=yes -f 500 -a --exclude-perf";
+    private static final String SIMPLEPERF_START_CMD = "simpleperf %s -o %s %s";
     private static final String SIMPLEPERF_STOP_CMD = "pkill -INT simpleperf";
     private static final String SIMPLEPERF_PROC_ID_CMD = "pidof simpleperf";
     private static final String REMOVE_CMD = "rm %s";
@@ -50,7 +49,7 @@
 
     private UiDevice mUiDevice;
 
-    public boolean startCollecting() {
+    public boolean startCollecting(String subcommand, String arguments) {
         mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
         try {
             // Cleanup any running simpleperf sessions.
@@ -66,11 +65,20 @@
             new Thread() {
                 @Override
                 public void run() {
+                    String startCommand =
+                            String.format(
+                                    SIMPLEPERF_START_CMD,
+                                    subcommand,
+                                    SIMPLEPERF_TMP_FILE_PATH,
+                                    arguments);
+                    Log.i(LOG_TAG, String.format("Start command: %s", startCommand));
                     UiDevice uiDevice =
                             UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
                     try {
-                        uiDevice.executeShellCommand(
-                                String.format(SIMPLEPERF_START_CMD, SIMPLEPERF_TMP_FILE_PATH));
+                        String startOutput = uiDevice.executeShellCommand(startCommand);
+                        Log.i(
+                                LOG_TAG,
+                                String.format("Simpleperf start command output - %s", startOutput));
                     } catch (IOException e) {
                         Log.e(LOG_TAG, "Failed to start simpleperf.");
                     }
diff --git a/libraries/collectors-helper/simpleperf/test/Android.bp b/libraries/collectors-helper/simpleperf/test/Android.bp
index 13031fb..8afc153 100644
--- a/libraries/collectors-helper/simpleperf/test/Android.bp
+++ b/libraries/collectors-helper/simpleperf/test/Android.bp
@@ -24,7 +24,7 @@
 
     static_libs: [
         "androidx.test.runner",
-        "android-support-test",
+        "androidx.test.rules",
         "simpleperf-helper",
         "junit",
 
diff --git a/libraries/collectors-helper/simpleperf/test/src/com/android/helpers/tests/SimpleperfHelperTest.java b/libraries/collectors-helper/simpleperf/test/src/com/android/helpers/tests/SimpleperfHelperTest.java
index c7890e1..a5089c5 100644
--- a/libraries/collectors-helper/simpleperf/test/src/com/android/helpers/tests/SimpleperfHelperTest.java
+++ b/libraries/collectors-helper/simpleperf/test/src/com/android/helpers/tests/SimpleperfHelperTest.java
@@ -41,6 +41,8 @@
 
     private static final String REMOVE_CMD = "rm %s";
     private static final String FILE_SIZE_IN_BYTES = "wc -c %s";
+    private static final String DEFAULT_SUBCOMMAND = "record";
+    private static final String DEFAULT_ARGUMENTS = "-g --post-unwind=yes -f 500 -a --exclude-perf";
 
     private SimpleperfHelper simpleperfHelper;
 
@@ -59,20 +61,20 @@
     /** Test simpleperf collection starts collecting properly. */
     @Test
     public void testSimpleperfStartSuccess() throws Exception {
-        assertTrue(simpleperfHelper.startCollecting());
+        assertTrue(simpleperfHelper.startCollecting(DEFAULT_SUBCOMMAND, DEFAULT_ARGUMENTS));
     }
 
     /** Test if the path name is prefixed with /. */
     @Test
     public void testSimpleperfValidOutputPath() throws Exception {
-        assertTrue(simpleperfHelper.startCollecting());
+        assertTrue(simpleperfHelper.startCollecting(DEFAULT_SUBCOMMAND, DEFAULT_ARGUMENTS));
         assertTrue(simpleperfHelper.stopCollecting("data/local/tmp/perf.data"));
     }
 
     /** Test the invalid output path. */
     @Test
     public void testSimpleperfInvalidOutputPath() throws Exception {
-        assertTrue(simpleperfHelper.startCollecting());
+        assertTrue(simpleperfHelper.startCollecting(DEFAULT_SUBCOMMAND, DEFAULT_ARGUMENTS));
         // Don't have permission to create new folder under /data
         assertFalse(simpleperfHelper.stopCollecting("/data/dummy/xyz/perf.data"));
     }
@@ -80,7 +82,7 @@
     /** Test simpleperf collection returns true and output file size greater than zero */
     @Test
     public void testSimpleperfSuccess() throws Exception {
-        assertTrue(simpleperfHelper.startCollecting());
+        assertTrue(simpleperfHelper.startCollecting(DEFAULT_SUBCOMMAND, DEFAULT_ARGUMENTS));
         Thread.sleep(1000);
         assertTrue(simpleperfHelper.stopCollecting("/data/local/tmp/perf.data"));
         Thread.sleep(1000);
diff --git a/libraries/collectors-helper/statsd/Android.bp b/libraries/collectors-helper/statsd/Android.bp
index e4a90bf..e6dd6d7 100644
--- a/libraries/collectors-helper/statsd/Android.bp
+++ b/libraries/collectors-helper/statsd/Android.bp
@@ -30,8 +30,8 @@
         "androidx.test.uiautomator",
         "collector-helper-utilities",
         "guava",
-        "platformprotoslite",
-        "statsdprotolite",
+        "platformprotosnano",
+        "statsdprotonano",
     ],
 
     platform_apis: true,
diff --git a/libraries/collectors-helper/statsd/src/com/android/helpers/AppStartupHelper.java b/libraries/collectors-helper/statsd/src/com/android/helpers/AppStartupHelper.java
index 3398b00..3e846b7 100644
--- a/libraries/collectors-helper/statsd/src/com/android/helpers/AppStartupHelper.java
+++ b/libraries/collectors-helper/statsd/src/com/android/helpers/AppStartupHelper.java
@@ -18,11 +18,8 @@
 
 import android.util.Log;
 
-import com.android.os.AtomsProto.AppStartFullyDrawn;
-import com.android.os.AtomsProto.AppStartOccurred;
-import com.android.os.AtomsProto.Atom;
-import com.android.os.AtomsProto.ProcessStartTime;
-import com.android.os.StatsLog.EventMetricData;
+import com.android.os.nano.AtomsProto;
+import com.android.os.nano.StatsLog;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -55,6 +52,7 @@
     private static final String PROCESS_START = "process_start";
     private static final String PROCESS_START_DELAY = "process_start_delay";
     private static final String TRANSITION_DELAY_MILLIS = "transition_delay_millis";
+    private static final String SOURCE_EVENT_DELAY_MILLIS = "source_event_delay_millis";
     private boolean isProcStartDetailsDisabled;
 
     private StatsdHelper mStatsdHelper = new StatsdHelper();
@@ -66,10 +64,10 @@
     public boolean startCollecting() {
         Log.i(LOG_TAG, "Adding app startup configs to statsd.");
         List<Integer> atomIdList = new ArrayList<>();
-        atomIdList.add(Atom.APP_START_OCCURRED_FIELD_NUMBER);
-        atomIdList.add(Atom.APP_START_FULLY_DRAWN_FIELD_NUMBER);
+        atomIdList.add(AtomsProto.Atom.APP_START_OCCURRED_FIELD_NUMBER);
+        atomIdList.add(AtomsProto.Atom.APP_START_FULLY_DRAWN_FIELD_NUMBER);
         if (!isProcStartDetailsDisabled) {
-            atomIdList.add(Atom.PROCESS_START_TIME_FIELD_NUMBER);
+            atomIdList.add(AtomsProto.Atom.PROCESS_START_TIME_FIELD_NUMBER);
         }
         return mStatsdHelper.addEventConfig(atomIdList);
     }
@@ -79,21 +77,27 @@
      */
     @Override
     public Map<String, StringBuilder> getMetrics() {
-        List<EventMetricData> eventMetricData = mStatsdHelper.getEventMetrics();
+        List<StatsLog.EventMetricData> eventMetricData = mStatsdHelper.getEventMetrics();
         Map<String, StringBuilder> appStartResultMap = new HashMap<>();
         Map<String, Integer> appStartCountMap = new HashMap<>();
         Map<String, Integer> tempResultCountMap = new HashMap<>();
-        for (EventMetricData dataItem : eventMetricData) {
-            Atom atom = dataItem.getAtom();
+        for (StatsLog.EventMetricData dataItem : eventMetricData) {
+            AtomsProto.Atom atom = dataItem.atom;
             if (atom.hasAppStartOccurred()) {
-                AppStartOccurred appStartAtom = atom.getAppStartOccurred();
-                String pkgName = appStartAtom.getPkgName();
-                String transitionType = appStartAtom.getType().toString();
-                int windowsDrawnMillis = appStartAtom.getWindowsDrawnDelayMillis();
-                int transitionDelayMillis = appStartAtom.getTransitionDelayMillis();
-                Log.i(LOG_TAG, String.format("Pkg Name: %s, Transition Type: %s, "
-                        + "WindowDrawnDelayMillis: %s, TransitionDelayMillis: %s",
-                        pkgName, transitionType, windowsDrawnMillis, transitionDelayMillis));
+                AtomsProto.AppStartOccurred appStartAtom = atom.getAppStartOccurred();
+                String pkgName = appStartAtom.pkgName;
+                String transitionType = String.valueOf(appStartAtom.type);
+                int windowsDrawnMillis = appStartAtom.windowsDrawnDelayMillis;
+                int transitionDelayMillis = appStartAtom.transitionDelayMillis;
+                Log.i(
+                        LOG_TAG,
+                        String.format(
+                                "Pkg Name: %s, Transition Type: %s, "
+                                        + "WindowDrawnDelayMillis: %s, TransitionDelayMillis: %s",
+                                pkgName,
+                                transitionType,
+                                windowsDrawnMillis,
+                                transitionDelayMillis));
 
                 String metricTypeKey = "";
                 String metricTransitionKey = "";
@@ -102,17 +106,17 @@
                 // To track total number of startups per type.
                 String totalCountKey = "";
                 String typeKey = "";
-                switch (appStartAtom.getType()) {
-                    case COLD:
+                switch (appStartAtom.type) {
+                    case AtomsProto.AppStartOccurred.COLD:
                         typeKey = COLD_STARTUP;
                         break;
-                    case WARM:
+                    case AtomsProto.AppStartOccurred.WARM:
                         typeKey = WARM_STARTUP;
                         break;
-                    case HOT:
+                    case AtomsProto.AppStartOccurred.HOT:
                         typeKey = HOT_STARTUP;
                         break;
-                    case UNKNOWN:
+                    case AtomsProto.AppStartOccurred.UNKNOWN:
                         break;
                 }
                 if (!typeKey.isEmpty()) {
@@ -131,26 +135,36 @@
                     MetricUtility.addMetric(metricTransitionKey, transitionDelayMillis,
                             appStartResultMap);
                 }
+                if (appStartAtom.sourceEventDelayMillis != 0) {
+                    int sourceEventDelayMillis = appStartAtom.sourceEventDelayMillis;
+                    Log.i(LOG_TAG, String.format("Pkg Name: %s, SourceEventDelayMillis: %d",
+                            pkgName, sourceEventDelayMillis));
+
+                    String metricEventDelayKey = MetricUtility.constructKey(
+                            SOURCE_EVENT_DELAY_MILLIS, pkgName);
+                    MetricUtility.addMetric(metricEventDelayKey, sourceEventDelayMillis,
+                            appStartResultMap);
+                }
             }
             if (atom.hasAppStartFullyDrawn()) {
-                AppStartFullyDrawn appFullyDrawnAtom = atom.getAppStartFullyDrawn();
-                String pkgName = appFullyDrawnAtom.getPkgName();
-                String transitionType = appFullyDrawnAtom.getType().toString();
-                long startupTimeMillis = appFullyDrawnAtom.getAppStartupTimeMillis();
+                AtomsProto.AppStartFullyDrawn appFullyDrawnAtom = atom.getAppStartFullyDrawn();
+                String pkgName = appFullyDrawnAtom.pkgName;
+                String transitionType = String.valueOf(appFullyDrawnAtom.type);
+                long startupTimeMillis = appFullyDrawnAtom.appStartupTimeMillis;
                 Log.i(LOG_TAG, String.format("Pkg Name: %s, Transition Type: %s, "
                         + "AppStartupTimeMillis: %d", pkgName, transitionType, startupTimeMillis));
 
                 String metricKey = "";
-                switch (appFullyDrawnAtom.getType()) {
-                    case UNKNOWN:
+                switch (appFullyDrawnAtom.type) {
+                    case AtomsProto.AppStartFullyDrawn.UNKNOWN:
                         metricKey = MetricUtility.constructKey(
                                 STARTUP_FULLY_DRAWN_UNKNOWN, pkgName);
                         break;
-                    case WITH_BUNDLE:
+                    case AtomsProto.AppStartFullyDrawn.WITH_BUNDLE:
                         metricKey = MetricUtility.constructKey(
                                 STARTUP_FULLY_DRAWN_WITH_BUNDLE, pkgName);
                         break;
-                    case WITHOUT_BUNDLE:
+                    case AtomsProto.AppStartFullyDrawn.WITHOUT_BUNDLE:
                         metricKey = MetricUtility.constructKey(
                                 STARTUP_FULLY_DRAWN_WITHOUT_BUNDLE, pkgName);
                         break;
@@ -161,35 +175,39 @@
             }
             // ProcessStartTime reports startup time for both foreground and background process.
             if (atom.hasProcessStartTime()) {
-                ProcessStartTime processStartTimeAtom = atom.getProcessStartTime();
-                String processName = processStartTimeAtom.getProcessName();
+                AtomsProto.ProcessStartTime processStartTimeAtom = atom.getProcessStartTime();
+                String processName = processStartTimeAtom.processName;
                 // Number of milliseconds it takes to finish start of the process.
-                long processStartDelayMillis = processStartTimeAtom.getProcessStartDelayMillis();
+                long processStartDelayMillis = processStartTimeAtom.processStartDelayMillis;
                 // Treating activity hosting type as foreground and everything else as background.
-                String hostingType = processStartTimeAtom.getHostingType().equals("activity")
-                        ? "fg" : "bg";
-                Log.i(LOG_TAG, String.format("Process Name: %s, Start Type: %s, Hosting Type: %s,"
-                        + " ProcessStartDelayMillis: %d", processName,
-                        processStartTimeAtom.getType().toString(),
-                        hostingType, processStartDelayMillis));
-
+                String hostingType =
+                        processStartTimeAtom.hostingType.contains("activity") ? "fg" : "bg";
+                Log.i(
+                        LOG_TAG,
+                        String.format(
+                                "Process Name: %s, Start Type: %s, Hosting Type: %s,"
+                                        + " ProcessStartDelayMillis: %d",
+                                processName,
+                                processStartTimeAtom.type,
+                                hostingType,
+                                processStartDelayMillis));
                 String metricKey = "";
                 // To track number of startups per type per package.
                 String metricCountKey = "";
                 // To track total number of startups per type.
                 String totalCountKey = "";
                 String typeKey = "";
-                switch (processStartTimeAtom.getType()) {
-                    case COLD:
+                switch (processStartTimeAtom.type) {
+                    case AtomsProto.ProcessStartTime.COLD:
                         typeKey = COLD_STARTUP;
                         break;
-                    case WARM:
+                    case AtomsProto.ProcessStartTime.WARM:
                         typeKey = WARM_STARTUP;
                         break;
-                    case HOT:
+                    case AtomsProto.ProcessStartTime.HOT:
                         typeKey = HOT_STARTUP;
                         break;
-                    case UNKNOWN:
+                    case AtomsProto.ProcessStartTime.UNKNOWN:
                         break;
                 }
                 if (!typeKey.isEmpty()) {
diff --git a/libraries/collectors-helper/statsd/src/com/android/helpers/CpuUsageHelper.java b/libraries/collectors-helper/statsd/src/com/android/helpers/CpuUsageHelper.java
index 26fdbd6..b2a613f 100644
--- a/libraries/collectors-helper/statsd/src/com/android/helpers/CpuUsageHelper.java
+++ b/libraries/collectors-helper/statsd/src/com/android/helpers/CpuUsageHelper.java
@@ -18,9 +18,8 @@
 
 import android.util.Log;
 
-import com.android.os.AtomsProto.Atom;
-import com.android.os.StatsLog.GaugeBucketInfo;
-import com.android.os.StatsLog.GaugeMetricData;
+import com.android.os.nano.AtomsProto;
+import com.android.os.nano.StatsLog;
 
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ListMultimap;
@@ -42,21 +41,15 @@
 
     private static final String LOG_TAG = CpuUsageHelper.class.getSimpleName();
     private static final String CPU_USAGE_PKG_UID = "cpu_usage_pkg_or_uid";
-    private static final String CPU_USAGE_FREQ = "cpu_usage_freq";
     private static final String TOTAL_CPU_USAGE = "total_cpu_usage";
-    private static final String TOTAL_CPU_USAGE_FREQ = "total_cpu_usage_freq";
-    private static final String CLUSTER_ID = "cluster";
-    private static final String FREQ_INDEX = "freq_index";
     private static final String USER_TIME = "user_time";
     private static final String SYSTEM_TIME = "system_time";
     private static final String TOTAL_CPU_TIME = "total_cpu_time";
     private static final String CPU_UTILIZATION = "cpu_utilization_average_per_core_percent";
 
     private StatsdHelper mStatsdHelper = new StatsdHelper();
-    private boolean isPerFreqDisabled;
     private boolean isPerPkgDisabled;
     private boolean isTotalPkgDisabled;
-    private boolean isTotalFreqDisabled;
     private boolean isCpuUtilizationEnabled;
     private long mStartTime;
     private long mEndTime;
@@ -67,8 +60,7 @@
         Log.i(LOG_TAG, "Adding CpuUsage config to statsd.");
         List<Integer> atomIdList = new ArrayList<>();
         // Add the atoms to be tracked.
-        atomIdList.add(Atom.CPU_TIME_PER_UID_FIELD_NUMBER);
-        atomIdList.add(Atom.CPU_TIME_PER_FREQ_FIELD_NUMBER);
+        atomIdList.add(AtomsProto.Atom.CPU_TIME_PER_UID_FIELD_NUMBER);
 
         if (isCpuUtilizationEnabled) {
             mStartTime = System.currentTimeMillis();
@@ -81,7 +73,7 @@
     public Map<String, Long> getMetrics() {
         Map<String, Long> cpuUsageFinalMap = new HashMap<>();
 
-        List<GaugeMetricData> gaugeMetricList = mStatsdHelper.getGaugeMetrics();
+        List<StatsLog.GaugeMetricData> gaugeMetricList = mStatsdHelper.getGaugeMetrics();
 
         if (isCpuUtilizationEnabled) {
             mEndTime = System.currentTimeMillis();
@@ -89,21 +81,22 @@
 
         ListMultimap<String, Long> cpuUsageMap = ArrayListMultimap.create();
 
-        for (GaugeMetricData gaugeMetric : gaugeMetricList) {
-            Log.v(LOG_TAG, "Bucket Size: " + gaugeMetric.getBucketInfoCount());
-            for (GaugeBucketInfo gaugeBucketInfo : gaugeMetric.getBucketInfoList()) {
-                for (Atom atom : gaugeBucketInfo.getAtomList()) {
-
+        for (StatsLog.GaugeMetricData gaugeMetric : gaugeMetricList) {
+            Log.v(LOG_TAG, "Bucket Size: " + gaugeMetric.bucketInfo.length);
+            for (StatsLog.GaugeBucketInfo gaugeBucketInfo : gaugeMetric.bucketInfo) {
+                for (AtomsProto.Atom atom : gaugeBucketInfo.atom) {
                     // Track CPU usage in user time and system time per package or UID
-                    if (atom.getCpuTimePerUid().hasUid()) {
-                        int uId = atom.getCpuTimePerUid().getUid();
+                    if (atom.hasCpuTimePerUid()) {
+                        int uId = atom.getCpuTimePerUid().uid;
                         String packageName = mStatsdHelper.getPackageName(uId);
                         // Convert to milliseconds to compare with CpuTimePerFreq
-                        long userTimeMillis = atom.getCpuTimePerUid().getUserTimeMicros() / 1000;
-                        long sysTimeMillis = atom.getCpuTimePerUid().getSysTimeMicros() / 1000;
-                        Log.v(LOG_TAG, String.format("Uid:%d, Pkg Name: %s, User_Time: %d,"
-                                + " System_Time: %d", uId, packageName, userTimeMillis,
-                                sysTimeMillis));
+                        long userTimeMillis = atom.getCpuTimePerUid().userTimeMicros / 1000;
+                        long sysTimeMillis = atom.getCpuTimePerUid().sysTimeMicros / 1000;
+                        Log.v(
+                                LOG_TAG,
+                                String.format(
+                                        "Uid:%d, Pkg Name: %s, User_Time: %d," + " System_Time: %d",
+                                        uId, packageName, userTimeMillis, sysTimeMillis));
 
                         // Use the package name if exist for the UID otherwise use the UID.
                         // Note: UID for the apps will be different across the builds.
@@ -126,27 +119,12 @@
                         cpuUsageMap.put(UserTimeKey, userTimeMillis);
                         cpuUsageMap.put(SystemTimeKey, sysTimeMillis);
                     }
-
-                    // Track cpu usage per cluster_id and freq_index
-                    if (atom.getCpuTimePerFreq().hasFreqIndex()) {
-                        int clusterId = atom.getCpuTimePerFreq().getCluster();
-                        int freqIndex = atom.getCpuTimePerFreq().getFreqIndex();
-                        long timeInFreq = atom.getCpuTimePerFreq().getTimeMillis();
-                        Log.v(LOG_TAG, String.format("Cluster Id: %d FreqIndex: %d,"
-                                + " Time_in_Freq: %d", clusterId, freqIndex, timeInFreq));
-                        String finalFreqIndexKey = MetricUtility.constructKey(
-                                CPU_USAGE_FREQ, CLUSTER_ID, String.valueOf(clusterId), FREQ_INDEX,
-                                String.valueOf(freqIndex));
-                        cpuUsageMap.put(finalFreqIndexKey, timeInFreq);
-                    }
-
                 }
             }
         }
 
         // Compute the final result map
         Long totalCpuUsage = 0L;
-        Long totalCpuFreq = 0L;
         for (String key : cpuUsageMap.keySet()) {
             List<Long> cpuUsageList = cpuUsageMap.get(key);
             if (cpuUsageList.size() > 1) {
@@ -167,18 +145,11 @@
                             cpuUsageFinalMap.put(finalKey, cpuUsage);
                         }
                     }
-                    if (key.startsWith(CPU_USAGE_FREQ)
-                            && !isPerFreqDisabled) {
-                        cpuUsageFinalMap.put(key, cpuUsage);
-                    }
                 }
                 // Add the CPU time to their respective (usage or frequency) total metric.
                 if (key.startsWith(CPU_USAGE_PKG_UID)
                         && !isTotalPkgDisabled) {
                     totalCpuUsage += cpuUsage;
-                } else if (key.startsWith(CPU_USAGE_FREQ)
-                        && !isTotalFreqDisabled) {
-                    totalCpuFreq += cpuUsage;
                 }
             }
         }
@@ -186,9 +157,6 @@
         if (!isTotalPkgDisabled) {
             cpuUsageFinalMap.put(TOTAL_CPU_USAGE, totalCpuUsage);
         }
-        if (!isTotalFreqDisabled) {
-            cpuUsageFinalMap.put(TOTAL_CPU_USAGE_FREQ, totalCpuFreq);
-        }
 
         // Calculate cpu utilization
         if (isCpuUtilizationEnabled) {
@@ -220,13 +188,6 @@
     }
 
     /**
-     * Disable the cpu metric collection per frequency.
-     */
-    public void setDisablePerFrequency() {
-        isPerFreqDisabled = true;
-    }
-
-    /**
      * Disable the total cpu metric collection by all the packages.
      */
     public void setDisableTotalPackage() {
@@ -234,13 +195,6 @@
     }
 
     /**
-     * Disable the total cpu metric collection by all the frequency.
-     */
-    public void setDisableTotalFrequency() {
-        isTotalFreqDisabled = true;
-    }
-
-    /**
      * Enable the collection of cpu utilization.
      */
     public void setEnableCpuUtilization() {
diff --git a/libraries/collectors-helper/statsd/src/com/android/helpers/CrashHelper.java b/libraries/collectors-helper/statsd/src/com/android/helpers/CrashHelper.java
index f4ebb0d..c6f1411 100644
--- a/libraries/collectors-helper/statsd/src/com/android/helpers/CrashHelper.java
+++ b/libraries/collectors-helper/statsd/src/com/android/helpers/CrashHelper.java
@@ -18,10 +18,8 @@
 
 import android.util.Log;
 
-import com.android.os.AtomsProto.ANROccurred;
-import com.android.os.AtomsProto.AppCrashOccurred;
-import com.android.os.AtomsProto.Atom;
-import com.android.os.StatsLog.EventMetricData;
+import com.android.os.nano.AtomsProto;
+import com.android.os.nano.StatsLog;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -50,8 +48,8 @@
     public boolean startCollecting() {
         Log.i(LOG_TAG, "Adding AppCrashOccured config to statsd.");
         List<Integer> atomIdList = new ArrayList<>();
-        atomIdList.add(Atom.APP_CRASH_OCCURRED_FIELD_NUMBER);
-        atomIdList.add(Atom.ANR_OCCURRED_FIELD_NUMBER);
+        atomIdList.add(AtomsProto.Atom.APP_CRASH_OCCURRED_FIELD_NUMBER);
+        atomIdList.add(AtomsProto.Atom.ANR_OCCURRED_FIELD_NUMBER);
         return mStatsdHelper.addEventConfig(atomIdList);
     }
 
@@ -60,43 +58,48 @@
      */
     @Override
     public Map<String, Integer> getMetrics() {
-        List<EventMetricData> eventMetricData = mStatsdHelper.getEventMetrics();
+        List<StatsLog.EventMetricData> eventMetricData = mStatsdHelper.getEventMetrics();
         Map<String, Integer> appCrashResultMap = new HashMap<>();
         // We need this because even if there are no crashes we need to report 0 count
         // in the dashboard for the total crash, native crash and ANR.
         appCrashResultMap.put(TOTAL_PREFIX + EVENT_JAVA_CRASH, 0);
         appCrashResultMap.put(TOTAL_PREFIX + EVENT_NATIVE_CRASH, 0);
         appCrashResultMap.put(TOTAL_PREFIX + EVENT_ANR, 0);
-        for (EventMetricData dataItem : eventMetricData) {
-            if (dataItem.getAtom().hasAppCrashOccurred()) {
-                AppCrashOccurred appCrashAtom = dataItem.getAtom().getAppCrashOccurred();
-                String eventType = appCrashAtom.getEventType();
-                String pkgName = appCrashAtom.getPackageName();
-                AppCrashOccurred.ForegroundState foregroundState =
-                        appCrashAtom.getForegroundState();
-                Log.i(LOG_TAG, String.format("Event Type:%s Pkg Name: %s "
-                        + " ForegroundState: %s", eventType, pkgName, foregroundState.toString()));
+        for (StatsLog.EventMetricData dataItem : eventMetricData) {
+            if (dataItem.atom.hasAppCrashOccurred()) {
+                AtomsProto.AppCrashOccurred appCrashAtom = dataItem.atom.getAppCrashOccurred();
+                String eventType = appCrashAtom.eventType;
+                String pkgName = appCrashAtom.packageName;
+                int foregroundState = appCrashAtom.foregroundState;
+                Log.i(
+                        LOG_TAG,
+                        String.format(
+                                "Event Type:%s Pkg Name: %s " + " ForegroundState: %s",
+                                eventType, pkgName, foregroundState));
 
                 // Track the total crash and native crash count.
                 MetricUtility.addMetric(TOTAL_PREFIX + eventType, appCrashResultMap);
                 // Add more detailed crash count key metrics.
-                String detailKey = MetricUtility.constructKey(eventType, pkgName,
-                        foregroundState.toString());
+                String detailKey =
+                        MetricUtility.constructKey(
+                                eventType, pkgName, String.valueOf(foregroundState));
                 MetricUtility.addMetric(detailKey, appCrashResultMap);
-            } else if (dataItem.getAtom().hasAnrOccurred()) {
-                ANROccurred anrAtom = dataItem.getAtom().getAnrOccurred();
-                String processName = anrAtom.getProcessName();
-                String reason = anrAtom.getReason();
-                ANROccurred.ForegroundState foregoundState = anrAtom.getForegroundState();
-                Log.i(LOG_TAG,
-                        String.format("ANR occurred in process %s due to %s; foregound state is %s",
-                                processName, reason, foregoundState.toString()));
+            } else if (dataItem.atom.hasAnrOccurred()) {
+                AtomsProto.ANROccurred anrAtom = dataItem.atom.getAnrOccurred();
+                String processName = anrAtom.processName;
+                String reason = anrAtom.reason;
+                int foregoundState = anrAtom.foregroundState;
+                Log.i(
+                        LOG_TAG,
+                        String.format(
+                                "ANR occurred in process %s due to %s; foregound state is %s",
+                                processName, reason, foregoundState));
 
                 // Track the total ANR count.
                 MetricUtility.addMetric(TOTAL_PREFIX + EVENT_ANR, appCrashResultMap);
                 String detailKey =
                         MetricUtility.constructKey(
-                                EVENT_ANR, processName, foregoundState.toString());
+                                EVENT_ANR, processName, String.valueOf(foregoundState));
                 MetricUtility.addMetric(detailKey, appCrashResultMap);
             }
         }
diff --git a/libraries/collectors-helper/statsd/src/com/android/helpers/DeviceConfigHelper.java b/libraries/collectors-helper/statsd/src/com/android/helpers/DeviceConfigHelper.java
new file mode 100644
index 0000000..ee9f241
--- /dev/null
+++ b/libraries/collectors-helper/statsd/src/com/android/helpers/DeviceConfigHelper.java
@@ -0,0 +1,36 @@
+/*
+ * 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;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.uiautomator.UiDevice;
+
+import java.io.IOException;
+
+/** Basic utilities to configure metric collection in tests through Device Config API. */
+public class DeviceConfigHelper {
+    /** Runs a shell command to set the configuration value in Device Config. */
+    public static String setConfigValue(String namespace, String setting, String value) {
+        try {
+            UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+            String command = String.format("device_config put %s %s %s", namespace, setting, value);
+            return device.executeShellCommand(command);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/libraries/collectors-helper/statsd/src/com/android/helpers/StatsdHelper.java b/libraries/collectors-helper/statsd/src/com/android/helpers/StatsdHelper.java
index fe490ed..f5e3294 100644
--- a/libraries/collectors-helper/statsd/src/com/android/helpers/StatsdHelper.java
+++ b/libraries/collectors-helper/statsd/src/com/android/helpers/StatsdHelper.java
@@ -16,31 +16,24 @@
 
 package com.android.helpers;
 
-import android.content.Context;
 import android.app.StatsManager;
 import android.app.StatsManager.StatsUnavailableException;
+import android.content.Context;
 import android.os.SystemClock;
 import android.util.Log;
 import android.util.StatsLog;
+
 import androidx.test.InstrumentationRegistry;
 
-import com.android.internal.os.StatsdConfigProto.AtomMatcher;
-import com.android.internal.os.StatsdConfigProto.EventMetric;
-import com.android.internal.os.StatsdConfigProto.FieldFilter;
-import com.android.internal.os.StatsdConfigProto.GaugeMetric;
-import com.android.internal.os.StatsdConfigProto.SimpleAtomMatcher;
-import com.android.internal.os.StatsdConfigProto.StatsdConfig;
-import com.android.internal.os.StatsdConfigProto.TimeUnit;
-import com.android.os.AtomsProto.Atom;
-import com.android.os.StatsLog.ConfigMetricsReport;
-import com.android.os.StatsLog.ConfigMetricsReportList;
-import com.android.os.StatsLog.EventMetricData;
-import com.android.os.StatsLog.GaugeMetricData;
-import com.android.os.StatsLog.StatsLogReport;
+import com.android.internal.os.nano.StatsdConfigProto;
+import com.android.os.nano.AtomsProto;
 
-import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.nano.CodedOutputByteBufferNano;
+import com.google.protobuf.nano.InvalidProtocolBufferNanoException;
 
+import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.UUID;
 
@@ -63,20 +56,22 @@
      */
     public boolean addEventConfig(List<Integer> atomIdList) {
         long configId = System.currentTimeMillis();
-        StatsdConfig.Builder statsConfigBuilder = getSimpleSources(configId);
-
+        StatsdConfigProto.StatsdConfig config = getSimpleSources(configId);
+        List<StatsdConfigProto.EventMetric> metrics = new ArrayList<>(atomIdList.size());
+        List<StatsdConfigProto.AtomMatcher> atomMatchers = new ArrayList<>(atomIdList.size());
         for (Integer atomId : atomIdList) {
             int atomUniqueId = getUniqueId();
-            statsConfigBuilder
-                    .addEventMetric(
-                            EventMetric.newBuilder()
-                                    .setId(getUniqueId())
-                                    .setWhat(atomUniqueId))
-                    .addAtomMatcher(getSimpleAtomMatcher(atomUniqueId, atomId));
+            StatsdConfigProto.EventMetric metric = new StatsdConfigProto.EventMetric();
+            metric.id = getUniqueId();
+            metric.what = atomUniqueId;
+            metrics.add(metric);
+            atomMatchers.add(getSimpleAtomMatcher(atomUniqueId, atomId));
         }
+        config.eventMetric = metrics.toArray(new StatsdConfigProto.EventMetric[0]);
+        config.atomMatcher = atomMatchers.toArray(new StatsdConfigProto.AtomMatcher[0]);
         try {
             adoptShellIdentity();
-            getStatsManager().addConfig(configId, statsConfigBuilder.build().toByteArray());
+            getStatsManager().addConfig(configId, toByteArray(config));
             dropShellIdentity();
         } catch (Exception e) {
             Log.e(LOG_TAG, "Not able to setup the event config.", e);
@@ -99,41 +94,38 @@
      */
     public boolean addGaugeConfig(List<Integer> atomIdList) {
         long configId = System.currentTimeMillis();
-        StatsdConfig.Builder statsConfigBuilder = getSimpleSources(configId);
+        StatsdConfigProto.StatsdConfig config = getSimpleSources(configId);
         int appBreadCrumbUniqueId = getUniqueId();
-
+        config.whitelistedAtomIds =
+                new int[] {AtomsProto.Atom.APP_BREADCRUMB_REPORTED_FIELD_NUMBER};
+        List<StatsdConfigProto.AtomMatcher> matchers = new ArrayList<>(atomIdList.size());
+        List<StatsdConfigProto.GaugeMetric> gaugeMetrics = new ArrayList<>();
         // Needed for collecting gauge metric based on trigger events.
-        statsConfigBuilder
-                .addAtomMatcher(
-                        getSimpleAtomMatcher(
-                                appBreadCrumbUniqueId, Atom.APP_BREADCRUMB_REPORTED_FIELD_NUMBER))
-                .addWhitelistedAtomIds(Atom.APP_BREADCRUMB_REPORTED_FIELD_NUMBER);
-
+        matchers.add(
+                getSimpleAtomMatcher(
+                        appBreadCrumbUniqueId,
+                        AtomsProto.Atom.APP_BREADCRUMB_REPORTED_FIELD_NUMBER));
         for (Integer atomId : atomIdList) {
             int atomUniqueId = getUniqueId();
             // Build Gauge metric config.
-            GaugeMetric gaugeMetric =
-                    GaugeMetric.newBuilder()
-                            .setId(getUniqueId())
-                            .setWhat(atomUniqueId)
-                            .setGaugeFieldsFilter(
-                                    FieldFilter.newBuilder().setIncludeAll(true).build())
-                            .setMaxNumGaugeAtomsPerBucket(MAX_ATOMS)
-                            .setSamplingType(GaugeMetric.SamplingType.FIRST_N_SAMPLES)
-                            .setTriggerEvent(appBreadCrumbUniqueId)
-                            .setBucket(TimeUnit.CTS)
-                            .build();
-
-            // add the gauge config.
-            statsConfigBuilder
-                    .addAtomMatcher(getSimpleAtomMatcher(atomUniqueId, atomId))
-                    .addGaugeMetric(gaugeMetric);
+            StatsdConfigProto.GaugeMetric gaugeMetric = new StatsdConfigProto.GaugeMetric();
+            gaugeMetric.id = getUniqueId();
+            gaugeMetric.what = atomUniqueId;
+            StatsdConfigProto.FieldFilter fieldFilter = new StatsdConfigProto.FieldFilter();
+            fieldFilter.includeAll = true;
+            gaugeMetric.gaugeFieldsFilter = fieldFilter;
+            gaugeMetric.maxNumGaugeAtomsPerBucket = MAX_ATOMS;
+            gaugeMetric.samplingType = StatsdConfigProto.GaugeMetric.FIRST_N_SAMPLES;
+            gaugeMetric.triggerEvent = appBreadCrumbUniqueId;
+            gaugeMetric.bucket = StatsdConfigProto.CTS;
+            matchers.add(getSimpleAtomMatcher(atomUniqueId, atomId));
+            gaugeMetrics.add(gaugeMetric);
         }
-
+        config.atomMatcher = matchers.toArray(new StatsdConfigProto.AtomMatcher[0]);
+        config.gaugeMetric = gaugeMetrics.toArray(new StatsdConfigProto.GaugeMetric[0]);
         try {
             adoptShellIdentity();
-            getStatsManager().addConfig(configId,
-                    statsConfigBuilder.build().toByteArray());
+            getStatsManager().addConfig(configId, toByteArray(config));
             StatsLog.logEvent(0);
             // Dump the counters before the test started.
             SystemClock.sleep(METRIC_DELAY_MS);
@@ -148,17 +140,15 @@
         return true;
     }
 
-    /**
-     * Create simple atom matcher with the given id and the field id.
-     *
-     * @param id a unique identifier for this {@code AtomMatcher}.
-     * @param fieldId the field id of the atom to match.
-     */
-    private static AtomMatcher.Builder getSimpleAtomMatcher(int id, int fieldId) {
-        return AtomMatcher.newBuilder()
-                .setId(id)
-                .setSimpleAtomMatcher(SimpleAtomMatcher.newBuilder()
-                        .setAtomId(fieldId));
+    /** Create simple atom matcher with the given id and the field id. */
+    private StatsdConfigProto.AtomMatcher getSimpleAtomMatcher(int id, int fieldId) {
+        StatsdConfigProto.AtomMatcher atomMatcher = new StatsdConfigProto.AtomMatcher();
+        atomMatcher.id = id;
+        StatsdConfigProto.SimpleAtomMatcher simpleAtomMatcher =
+                new StatsdConfigProto.SimpleAtomMatcher();
+        simpleAtomMatcher.atomId = fieldId;
+        atomMatcher.setSimpleAtomMatcher(simpleAtomMatcher);
+        return atomMatcher;
     }
 
     /**
@@ -166,75 +156,92 @@
      *
      * @param configId unique id of the configuration tracked by StatsManager.
      */
-    private static StatsdConfig.Builder getSimpleSources(long configId) {
-        return StatsdConfig.newBuilder()
-                .setId(configId)
-                .addAllowedLogSource("AID_ROOT")
-                .addAllowedLogSource("AID_SYSTEM")
-                .addAllowedLogSource("AID_RADIO")
-                .addAllowedLogSource("AID_BLUETOOTH")
-                .addAllowedLogSource("AID_GRAPHICS")
-                .addAllowedLogSource("AID_STATSD")
-                .addAllowedLogSource("AID_INCIENTD")
-                .addDefaultPullPackages("AID_SYSTEM")
-                .addDefaultPullPackages("AID_RADIO")
-                .addDefaultPullPackages("AID_STATSD")
-                .addDefaultPullPackages("AID_GPU_SERVICE");
+    private static StatsdConfigProto.StatsdConfig getSimpleSources(long configId) {
+        StatsdConfigProto.StatsdConfig config = new StatsdConfigProto.StatsdConfig();
+        config.id = configId;
+        String[] allowedLogSources =
+                new String[] {
+                    "AID_ROOT",
+                    "AID_SYSTEM",
+                    "AID_RADIO",
+                    "AID_BLUETOOTH",
+                    "AID_GRAPHICS",
+                    "AID_STATSD",
+                    "AID_INCIENTD"
+                };
+        String[] defaultPullPackages =
+                new String[] {"AID_SYSTEM", "AID_RADIO", "AID_STATSD", "AID_GPU_SERVICE"};
+        int[] whitelistedAtomIds =
+                new int[] {
+                    AtomsProto.Atom.UI_INTERACTION_FRAME_INFO_REPORTED_FIELD_NUMBER,
+                    AtomsProto.Atom.UI_ACTION_LATENCY_REPORTED_FIELD_NUMBER
+                };
+        config.allowedLogSource = allowedLogSources;
+        config.defaultPullPackages = defaultPullPackages;
+        config.whitelistedAtomIds = whitelistedAtomIds;
+        return config;
     }
 
-    /**
-     * Returns the list of EventMetricData tracked under the config.
-     */
-    public List<EventMetricData> getEventMetrics() {
-        ConfigMetricsReportList reportList = null;
-        List<EventMetricData> eventData = new ArrayList<>();
+    /** Returns the list of EventMetricData tracked under the config. */
+    public List<com.android.os.nano.StatsLog.EventMetricData> getEventMetrics() {
+        List<com.android.os.nano.StatsLog.EventMetricData> eventData = new ArrayList<>();
+        com.android.os.nano.StatsLog.ConfigMetricsReportList reportList = null;
         try {
             if (getConfigId() != -1) {
                 adoptShellIdentity();
-                reportList = ConfigMetricsReportList.parser()
-                        .parseFrom(getStatsManager().getReports(getConfigId()));
+                byte[] serializedReports = getStatsManager().getReports(getConfigId());
+                reportList =
+                        com.android.os.nano.StatsLog.ConfigMetricsReportList.parseFrom(
+                                serializedReports);
                 dropShellIdentity();
             }
-        } catch (InvalidProtocolBufferException | StatsUnavailableException se) {
+        } catch (InvalidProtocolBufferNanoException | StatsUnavailableException se) {
             Log.e(LOG_TAG, "Retreiving event metrics failed.", se);
             return eventData;
         }
 
-        if (reportList != null) {
-            ConfigMetricsReport configReport = reportList.getReports(0);
-            for (StatsLogReport metric : configReport.getMetricsList()) {
-                eventData.addAll(metric.getEventMetrics().getDataList());
+        if (reportList != null && reportList.reports.length > 0) {
+            com.android.os.nano.StatsLog.ConfigMetricsReport configReport = reportList.reports[0];
+            for (com.android.os.nano.StatsLog.StatsLogReport metric : configReport.metrics) {
+                com.android.os.nano.StatsLog.StatsLogReport.EventMetricDataWrapper
+                        eventMetricDataWrapper = metric.getEventMetrics();
+                if (eventMetricDataWrapper != null) {
+                    eventData.addAll(Arrays.asList(eventMetricDataWrapper.data));
+                }
             }
         }
         Log.i(LOG_TAG, "Number of events: " + eventData.size());
         return eventData;
     }
 
-    /**
-     * Returns the list of GaugeMetric data tracked under the config.
-     */
-    public List<GaugeMetricData> getGaugeMetrics() {
-        ConfigMetricsReportList reportList = null;
-        List<GaugeMetricData> gaugeData = new ArrayList<>();
+    /** Returns the list of GaugeMetric data tracked under the config. */
+    public List<com.android.os.nano.StatsLog.GaugeMetricData> getGaugeMetrics() {
+        com.android.os.nano.StatsLog.ConfigMetricsReportList reportList = null;
+        List<com.android.os.nano.StatsLog.GaugeMetricData> gaugeData = new ArrayList<>();
         try {
             if (getConfigId() != -1) {
                 adoptShellIdentity();
                 StatsLog.logEvent(0);
                 // Dump the the counters after the test completed.
                 SystemClock.sleep(METRIC_DELAY_MS);
-                reportList = ConfigMetricsReportList.parser()
-                        .parseFrom(getStatsManager().getReports(getConfigId()));
+                reportList =
+                        com.android.os.nano.StatsLog.ConfigMetricsReportList.parseFrom(
+                                getStatsManager().getReports(getConfigId()));
                 dropShellIdentity();
             }
-        } catch (InvalidProtocolBufferException | StatsUnavailableException se) {
+        } catch (InvalidProtocolBufferNanoException | StatsUnavailableException se) {
             Log.e(LOG_TAG, "Retreiving gauge metrics failed.", se);
             return gaugeData;
         }
 
-        if (reportList != null) {
-            ConfigMetricsReport configReport = reportList.getReports(0);
-            for (StatsLogReport metric : configReport.getMetricsList()) {
-                gaugeData.addAll(metric.getGaugeMetrics().getDataList());
+        if (reportList != null && reportList.reports.length > 0) {
+            com.android.os.nano.StatsLog.ConfigMetricsReport configReport = reportList.reports[0];
+            for (com.android.os.nano.StatsLog.StatsLogReport metric : configReport.metrics) {
+                com.android.os.nano.StatsLog.StatsLogReport.GaugeMetricDataWrapper
+                        gaugeMetricDataWrapper = metric.getGaugeMetrics();
+                if (gaugeMetricDataWrapper != null) {
+                    gaugeData.addAll(Arrays.asList(gaugeMetricDataWrapper.data));
+                }
             }
         }
         Log.i(LOG_TAG, "Number of Gauge data: " + gaugeData.size());
@@ -261,6 +268,18 @@
         }
     }
 
+    /** Returns the package name for the UID if it is available. Otherwise return null. */
+    public String getPackageName(int uid) {
+        String pkgName =
+                InstrumentationRegistry.getTargetContext().getPackageManager().getNameForUid(uid);
+        // Remove the UID appended at the end of the package name.
+        if (pkgName != null) {
+            String[] pkgNameSplit = pkgName.split(String.format("\\:%d", uid));
+            return pkgNameSplit[0];
+        }
+        return pkgName;
+    }
+
     /** Gets {@code StatsManager}, used to configure, collect and remove the statsd configs. */
     private StatsManager getStatsManager() {
         if (mStatsManager == null) {
@@ -271,15 +290,17 @@
     }
 
     /** Returns the package name associated with this UID if available, or null otherwise. */
-    public String getPackageName(int uid) {
-        String pkgName = InstrumentationRegistry.getTargetContext().getPackageManager()
-                .getNameForUid(uid);
-        // Remove the UID appended at the end of the package name.
-        if (pkgName != null) {
-            String[] pkgNameSplit = pkgName.split(String.format("\\:%d", uid));
-            return pkgNameSplit[0];
-        }
-        return pkgName;
+    /**
+     * Serializes a {@link StatsdConfigProto.StatsdConfig}.
+     *
+     * @return byte[]
+     */
+    private static byte[] toByteArray(StatsdConfigProto.StatsdConfig config) throws IOException {
+        byte[] serialized = new byte[config.getSerializedSize()];
+        CodedOutputByteBufferNano outputByteBufferNano =
+                CodedOutputByteBufferNano.newInstance(serialized);
+        config.writeTo(outputByteBufferNano);
+        return serialized;
     }
 
     /** Sets the statsd config id currently tracked by this class. */
diff --git a/libraries/collectors-helper/statsd/src/com/android/helpers/ThermalHelper.java b/libraries/collectors-helper/statsd/src/com/android/helpers/ThermalHelper.java
index a15c877..874fb2e 100644
--- a/libraries/collectors-helper/statsd/src/com/android/helpers/ThermalHelper.java
+++ b/libraries/collectors-helper/statsd/src/com/android/helpers/ThermalHelper.java
@@ -16,15 +16,21 @@
 
 package com.android.helpers;
 
-import android.os.TemperatureTypeEnum;
+import static android.os.nano.OsProtoEnums.TEMPERATURE_TYPE_BATTERY;
+import static android.os.nano.OsProtoEnums.TEMPERATURE_TYPE_CPU;
+import static android.os.nano.OsProtoEnums.TEMPERATURE_TYPE_GPU;
+import static android.os.nano.OsProtoEnums.TEMPERATURE_TYPE_POWER_AMPLIFIER;
+import static android.os.nano.OsProtoEnums.TEMPERATURE_TYPE_SKIN;
+import static android.os.nano.OsProtoEnums.TEMPERATURE_TYPE_USB_PORT;
+
 import android.util.Log;
 
 import androidx.annotation.VisibleForTesting;
 import androidx.test.InstrumentationRegistry;
 import androidx.test.uiautomator.UiDevice;
 
-import com.android.os.AtomsProto.Atom;
-import com.android.os.StatsLog.EventMetricData;
+import com.android.os.nano.AtomsProto;
+import com.android.os.nano.StatsLog;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -44,6 +50,8 @@
     private static final int UNDEFINED_SEVERITY = -1;
     private static final Pattern SEVERITY_DUMPSYS_PATTERN =
             Pattern.compile("Thermal Status: (\\d+)");
+    private static final Pattern TEMPERATURE_DUMPSYS_PATTERN =
+            Pattern.compile(".*mValue=(.*), mType=(.*), mName=(.*), mStatus=(\\d+).*");
 
     private StatsdHelper mStatsdHelper;
     private UiDevice mDevice;
@@ -64,10 +72,10 @@
                 }
             }
         } catch (NumberFormatException nfe) {
-            Log.w(LOG_TAG, String.format("Couldn't identify severity. Error parsing: %s", nfe));
+            Log.e(LOG_TAG, String.format("Couldn't identify severity. Error parsing: %s", nfe));
             return false;
         } catch (IOException ioe) {
-            Log.w(LOG_TAG, String.format("Failed to query thermalservice. Error: %s", ioe));
+            Log.e(LOG_TAG, String.format("Failed to query thermalservice. Error: %s", ioe));
             return false;
         }
 
@@ -79,7 +87,7 @@
 
         // Register the thermal event config to statsd.
         List<Integer> atomIdList = new ArrayList<>();
-        atomIdList.add(Atom.THERMAL_THROTTLING_SEVERITY_STATE_CHANGED_FIELD_NUMBER);
+        atomIdList.add(AtomsProto.Atom.THERMAL_THROTTLING_SEVERITY_STATE_CHANGED_FIELD_NUMBER);
         return getStatsdHelper().addEventConfig(atomIdList);
     }
 
@@ -92,18 +100,14 @@
         String severityKey = MetricUtility.constructKey("thermal", "throttling", "severity");
         MetricUtility.addMetric(severityKey, mInitialSeverity, results);
 
-        List<EventMetricData> eventMetricData = getStatsdHelper().getEventMetrics();
+        List<StatsLog.EventMetricData> eventMetricData = getStatsdHelper().getEventMetrics();
         Log.i(LOG_TAG, String.format("%d thermal data points found.", eventMetricData.size()));
         // Collect all thermal throttling severity state change events.
-        for (EventMetricData dataItem : eventMetricData) {
-            if (dataItem.getAtom().hasThermalThrottlingSeverityStateChanged()) {
+        for (StatsLog.EventMetricData dataItem : eventMetricData) {
+            if (dataItem.atom.hasThermalThrottlingSeverityStateChanged()) {
                 // TODO(b/137878503): Add elapsed_timestamp_nanos for timpestamp data.
                 // Get thermal throttling severity state change data point.
-                int severity =
-                        dataItem.getAtom()
-                                .getThermalThrottlingSeverityStateChanged()
-                                .getSeverity()
-                                .getNumber();
+                int severity = dataItem.atom.getThermalThrottlingSeverityStateChanged().severity;
                 // Store the severity state change ignoring where the measurement came from.
                 MetricUtility.addMetric(severityKey, severity, results);
                 // Set the initial severity to the last value, in case #getMetrics is called again.
@@ -111,6 +115,43 @@
             }
         }
 
+        try {
+            String[] output = getDevice().executeShellCommand("dumpsys thermalservice").split("\n");
+            boolean inCurrentTempSection = false;
+            for (String line : output) {
+                Matcher temperatureMatcher = TEMPERATURE_DUMPSYS_PATTERN.matcher(line);
+                if (inCurrentTempSection && temperatureMatcher.matches()) {
+                    Log.v(LOG_TAG, "Matched " + line);
+                    String name = temperatureMatcher.group(3);
+                    MetricUtility.addMetric(
+                            MetricUtility.constructKey("temperature", name, "value"),
+                            Double.parseDouble(temperatureMatcher.group(1)), // value group
+                            results);
+                    MetricUtility.addMetric(
+                            MetricUtility.constructKey("temperature", name, "type"),
+                            Integer.parseInt(temperatureMatcher.group(2)), // type group
+                            results);
+                    MetricUtility.addMetric(
+                            MetricUtility.constructKey("temperature", name, "status"),
+                            Integer.parseInt(temperatureMatcher.group(4)), // status group
+                            results);
+                }
+
+                if (line.contains("Current temperatures")) {
+                    inCurrentTempSection = true;
+                } else if (line.trim().startsWith("Current") || line.trim().startsWith("Cached")) {
+                    // We're entering another section of data.
+                    inCurrentTempSection = false;
+                }
+            }
+        } catch (NumberFormatException nfe) {
+            Log.e(
+                    LOG_TAG,
+                    String.format("Couldn't identify temperature info. Error parsing: %s", nfe));
+        } catch (IOException ioe) {
+            Log.e(LOG_TAG, String.format("Failed to query thermalservice. Error: %s", ioe));
+        }
+
         return results;
     }
 
@@ -123,7 +164,7 @@
 
     /** A shorthand name for temperature sensor types used in metric keys. */
     @VisibleForTesting
-    static String getShorthandSensorType(TemperatureTypeEnum type) {
+    static String getShorthandSensorType(int type) {
         switch (type) {
             case TEMPERATURE_TYPE_CPU:
                 return "cpu";
diff --git a/libraries/collectors-helper/statsd/src/com/android/helpers/UiActionLatencyHelper.java b/libraries/collectors-helper/statsd/src/com/android/helpers/UiActionLatencyHelper.java
new file mode 100644
index 0000000..f727faa
--- /dev/null
+++ b/libraries/collectors-helper/statsd/src/com/android/helpers/UiActionLatencyHelper.java
@@ -0,0 +1,79 @@
+/*
+ * 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;
+
+import android.util.Log;
+
+import com.android.internal.util.LatencyTracker;
+import com.android.os.nano.AtomsProto;
+import com.android.os.nano.StatsLog;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Helper consisting of helper methods to set system interactions configs in statsd and retrieve the
+ * necessary information from statsd using the config id.
+ */
+public class UiActionLatencyHelper implements ICollectorHelper<StringBuilder> {
+
+    private static final String LOG_TAG = UiActionLatencyHelper.class.getSimpleName();
+
+    private final StatsdHelper mStatsdHelper = new StatsdHelper();
+
+    /** Set up the system actions latency statsd config. */
+    @Override
+    public boolean startCollecting() {
+        Log.i(LOG_TAG, "Enabling metric collection and disabling sampling.");
+        DeviceConfigHelper.setConfigValue("latency_tracker", "enabled", "true");
+        DeviceConfigHelper.setConfigValue("latency_tracker", "sampling_interval", "1");
+        Log.i(LOG_TAG, "Adding system actions latency config to statsd.");
+        List<Integer> atomIdList = new ArrayList<>();
+        atomIdList.add(AtomsProto.Atom.UI_ACTION_LATENCY_REPORTED_FIELD_NUMBER);
+        return mStatsdHelper.addEventConfig(atomIdList);
+    }
+
+    /** Collect the system actions latency metrics from the statsd. */
+    @Override
+    public Map<String, StringBuilder> getMetrics() {
+        Log.i(LOG_TAG, "get metrics.");
+        Map<String, StringBuilder> latenciesMap = new HashMap<>();
+        for (StatsLog.EventMetricData dataItem : mStatsdHelper.getEventMetrics()) {
+            final AtomsProto.Atom atom = dataItem.atom;
+            if (atom.hasUiActionLatencyReported()) {
+                final AtomsProto.UIActionLatencyReported uiActionLatencyReported =
+                        atom.getUiActionLatencyReported();
+                final String action =
+                        LatencyTracker.getNameOfAction(uiActionLatencyReported.action);
+                MetricUtility.addMetric(
+                        MetricUtility.constructKey("latency", action),
+                        uiActionLatencyReported.latencyMillis,
+                        latenciesMap);
+            }
+        }
+
+        return latenciesMap;
+    }
+
+    /** Remove the statsd config. */
+    @Override
+    public boolean stopCollecting() {
+        return mStatsdHelper.removeStatsConfig();
+    }
+}
diff --git a/libraries/collectors-helper/statsd/src/com/android/helpers/UiInteractionFrameInfoHelper.java b/libraries/collectors-helper/statsd/src/com/android/helpers/UiInteractionFrameInfoHelper.java
new file mode 100644
index 0000000..be805c5
--- /dev/null
+++ b/libraries/collectors-helper/statsd/src/com/android/helpers/UiInteractionFrameInfoHelper.java
@@ -0,0 +1,118 @@
+/*
+ * 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;
+
+import static com.android.helpers.MetricUtility.addMetric;
+import static com.android.helpers.MetricUtility.constructKey;
+
+import android.util.Log;
+
+import com.android.internal.jank.InteractionJankMonitor;
+import com.android.os.nano.AtomsProto;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Helper consisting of helper methods to set system interactions configs in statsd and retrieve the
+ * necessary information from statsd using the config id.
+ */
+public class UiInteractionFrameInfoHelper implements ICollectorHelper<StringBuilder> {
+
+    private static final String LOG_TAG = UiInteractionFrameInfoHelper.class.getSimpleName();
+    public static final String KEY_PREFIX_CUJ = "cuj";
+    public static final String SUFFIX_MAX_FRAME_MS = "max_frame_time_ms";
+
+    private final StatsdHelper mStatsdHelper = new StatsdHelper();
+
+    /** Set up the system interactions jank statsd config. */
+    @Override
+    public boolean startCollecting() {
+        enforceSampling(true);
+        Log.i(LOG_TAG, "Adding system interactions config to statsd.");
+        List<Integer> atomIdList = new ArrayList<>();
+        atomIdList.add(AtomsProto.Atom.UI_INTERACTION_FRAME_INFO_REPORTED_FIELD_NUMBER);
+        return mStatsdHelper.addEventConfig(atomIdList);
+    }
+
+    // convert 0 to 1e-6 to make logarithmic dashboards look better.
+    public static double makeLogFriendly(double metric) {
+        return Math.max(0.01, metric);
+    }
+
+    /**
+     * Enabling metric collection and disabling sampling.
+     *
+     * @param enabled true to enable sampling, false to disable.
+     */
+    public void enforceSampling(boolean enabled) {
+        final String namespace = "interaction_jank_monitor";
+        DeviceConfigHelper.setConfigValue(namespace, "enabled", enabled ? "true" : "false");
+        DeviceConfigHelper.setConfigValue(namespace, "sampling_interval", "1");
+    }
+
+    /** Collect the system interactions jank metrics from the statsd. */
+    @Override
+    public Map<String, StringBuilder> getMetrics() {
+        Log.i(LOG_TAG, "get metrics.");
+        Map<String, StringBuilder> frameInfoMap = new HashMap<>();
+        for (com.android.os.nano.StatsLog.EventMetricData dataItem :
+                mStatsdHelper.getEventMetrics()) {
+            final AtomsProto.Atom atom = dataItem.atom;
+            if (atom.hasUiInteractionFrameInfoReported()) {
+                final AtomsProto.UIInteractionFrameInfoReported uiInteractionFrameInfoReported =
+                        atom.getUiInteractionFrameInfoReported();
+
+                final String interactionType =
+                        InteractionJankMonitor.getNameOfInteraction(
+                                uiInteractionFrameInfoReported.interactionType);
+
+                addMetric(
+                        constructKey(KEY_PREFIX_CUJ, interactionType, "total_frames"),
+                        makeLogFriendly(uiInteractionFrameInfoReported.totalFrames),
+                        frameInfoMap);
+
+                addMetric(
+                        constructKey(KEY_PREFIX_CUJ, interactionType, "missed_frames"),
+                        makeLogFriendly(uiInteractionFrameInfoReported.missedFrames),
+                        frameInfoMap);
+
+                addMetric(
+                        constructKey(KEY_PREFIX_CUJ, interactionType, "sf_missed_frames"),
+                        makeLogFriendly(uiInteractionFrameInfoReported.sfMissedFrames),
+                        frameInfoMap);
+
+                addMetric(
+                        constructKey(KEY_PREFIX_CUJ, interactionType, SUFFIX_MAX_FRAME_MS),
+                        makeLogFriendly(
+                                uiInteractionFrameInfoReported.maxFrameTimeNanos / 1000000.0),
+                        frameInfoMap);
+            }
+        }
+
+        return frameInfoMap;
+    }
+
+    /** Remove the statsd config. */
+    @Override
+    public boolean stopCollecting() {
+        enforceSampling(false);
+        return mStatsdHelper.removeStatsConfig();
+    }
+}
diff --git a/libraries/collectors-helper/statsd/test/Android.bp b/libraries/collectors-helper/statsd/test/Android.bp
index d04049b..6dcbdfa 100644
--- a/libraries/collectors-helper/statsd/test/Android.bp
+++ b/libraries/collectors-helper/statsd/test/Android.bp
@@ -24,14 +24,16 @@
 
     static_libs: [
         "androidx.test.runner",
-        "android-support-test",
+        "androidx.test.rules",
         "statsd-helper",
         "app-helpers-handheld-interfaces",
         "junit",
         "ub-uiautomator",
-        "platformprotoslite",
-        "statsdprotolite",
+        "platformprotosnano",
+        "platform-test-rules",
+        "statsdprotonano",
         "mockito-target-minus-junit4",
+        "launcher-aosp-tapl"
     ],
 
     platform_apis: true,
diff --git a/libraries/collectors-helper/statsd/test/src/com/android/helpers/AppStartupHelperTest.java b/libraries/collectors-helper/statsd/test/src/com/android/helpers/AppStartupHelperTest.java
index 2c42038..234fa2f 100644
--- a/libraries/collectors-helper/statsd/test/src/com/android/helpers/AppStartupHelperTest.java
+++ b/libraries/collectors-helper/statsd/test/src/com/android/helpers/AppStartupHelperTest.java
@@ -17,13 +17,18 @@
 
 import android.os.SystemClock;
 import android.platform.helpers.HelperAccessor;
-import android.platform.helpers.ICalendarHelper;
+import android.platform.helpers.ICalculatorHelper;
+import android.platform.helpers.IClockHelper;
+import android.platform.test.rule.FinishActivitiesWithoutProcessKillRule;
 import androidx.test.runner.AndroidJUnit4;
 
+
 import com.android.helpers.AppStartupHelper;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
+import org.junit.runner.Description;
 import org.junit.runner.RunWith;
 
 import java.util.Map;
@@ -44,14 +49,14 @@
 @RunWith(AndroidJUnit4.class)
 public class AppStartupHelperTest {
 
-    // Kill the calendar app.
+    // Kill the calculator app.
     private static final String KILL_TEST_APP_CMD_TEMPLATE = "am force-stop %s";
     // Package names used for testing.
-    private static final String CALENDAR_PKG_NAME = "com.google.android.calendar";
-    private static final String SETTINGS_PKG_NAME = "com.android.settings";
-    // Key prefixes to store the cold, warm or hot launch time of the calendar app, respectively.
+    private static final String CALCULATOR_PKG_NAME = "com.google.android.calculator";
+    private static final String CLOCK_PKG_NAME = "com.google.android.deskclock";
+    // Key prefixes to store the cold, warm or hot launch time of the calculator app, respectively.
     private static final String COLD_LAUNCH_KEY_TEMPLATE = "cold_startup_%s";
-    private static final String COLD_LAUNCH_PROCESSS_FG_KEY_TEMPLATE =
+    private static final String COLD_LAUNCH_PROCESS_FG_KEY_TEMPLATE =
             "cold_startup_process_start_delay_%s_fg";
     private static final String COLD_LAUNCH_TRANSITION_DELAY_MILLIS_KEY_TEMPLATE =
             "cold_startup_transition_delay_millis_%s";
@@ -68,6 +73,8 @@
             "cold_startup_process_start_total_count";
     private static final String WARM_LAUNCH_KEY_TEMPLATE = "warm_startup_%s";
     private static final String HOT_LAUNCH_KEY_TEMPLATE = "hot_startup_%s";
+    private static final String SOURCE_EVENT_DELAY_MILLIS_KEY_TEMPLATE =
+            "source_event_delay_millis_%s";
     // Keyword for keys to store the app startup fully drawn metric.
     private static final String FULLY_DRAWN_KEY_KEYWORD = "fully_drawn";
 
@@ -75,17 +82,21 @@
     private static final String KEYCODE_HOME = "KEYCODE_HOME";
 
     private AppStartupHelper mAppStartupHelper = new AppStartupHelper();
-    private HelperAccessor<ICalendarHelper> mHelper =
-            new HelperAccessor<>(ICalendarHelper.class);
+    private HelperAccessor<ICalculatorHelper> mHelper =
+            new HelperAccessor<>(ICalculatorHelper.class);
+    private HelperAccessor<IClockHelper> mClockHelper =
+            new HelperAccessor<>(IClockHelper.class);
 
     @Before
     public void setUp() {
         mAppStartupHelper = new AppStartupHelper();
         // Make sure the apps are starting from the clean state.
-        HelperTestUtility.clearApp(String.format(KILL_TEST_APP_CMD_TEMPLATE, CALENDAR_PKG_NAME));
-        HelperTestUtility.clearApp(String.format(KILL_TEST_APP_CMD_TEMPLATE, SETTINGS_PKG_NAME));
+        HelperTestUtility.clearApp(String.format(KILL_TEST_APP_CMD_TEMPLATE, CALCULATOR_PKG_NAME));
+        HelperTestUtility.clearApp(String.format(KILL_TEST_APP_CMD_TEMPLATE, CLOCK_PKG_NAME));
         // Make sure display is on and unlocked.
         HelperTestUtility.wakeUpAndUnlock();
+        // Lock the orientation.
+        HelperTestUtility.setOrientationNatural();
     }
 
     /**
@@ -116,9 +127,9 @@
         mHelper.get().open();
         Map<String, StringBuilder> appLaunchMetrics = mAppStartupHelper.getMetrics();
         // A metric key for the app cold launching should exist, and should only hold one value.
-        String coldLaunchMetricKey = String.format(COLD_LAUNCH_KEY_TEMPLATE, CALENDAR_PKG_NAME);
+        String coldLaunchMetricKey = String.format(COLD_LAUNCH_KEY_TEMPLATE, CALCULATOR_PKG_NAME);
         String coldLaunchCountPkgKey = String.format(COLD_LAUNCH_COUNT_PKG_KEY_TEMPLATE,
-                CALENDAR_PKG_NAME);
+                CALCULATOR_PKG_NAME);
         assertTrue(appLaunchMetrics.keySet().contains(coldLaunchMetricKey));
         assertEquals(1, appLaunchMetrics.get(coldLaunchMetricKey).toString().split(",").length);
         assertEquals(1, Integer.parseInt(appLaunchMetrics.get(coldLaunchCountPkgKey).toString()));
@@ -128,16 +139,16 @@
         // Verify transition metrics.
         String coldLaunchTransitionMetricKey = String.format(
                 COLD_LAUNCH_TRANSITION_DELAY_MILLIS_KEY_TEMPLATE,
-                CALENDAR_PKG_NAME);
+                CALCULATOR_PKG_NAME);
         assertTrue(appLaunchMetrics.keySet().contains(coldLaunchTransitionMetricKey));
         assertEquals(1,
                 appLaunchMetrics.get(coldLaunchTransitionMetricKey).toString().split(",").length);
 
         // Verify process start values.
-        String coldLaunchProcessMetricKey = String.format(COLD_LAUNCH_PROCESSS_FG_KEY_TEMPLATE,
-                CALENDAR_PKG_NAME);
+        String coldLaunchProcessMetricKey = String.format(COLD_LAUNCH_PROCESS_FG_KEY_TEMPLATE,
+                CALCULATOR_PKG_NAME);
         String coldLaunchProcessCountPkgKey = String.format(COLD_LAUNCH_COUNT_PKG_KEY_TEMPLATE,
-                CALENDAR_PKG_NAME);
+                CALCULATOR_PKG_NAME);
         assertTrue(appLaunchMetrics.keySet().contains(coldLaunchProcessMetricKey));
         assertEquals(1,
                 appLaunchMetrics.get(coldLaunchProcessMetricKey).toString().split(",").length);
@@ -148,6 +159,14 @@
                 Integer.parseInt(appLaunchMetrics.get(
                         COLD_LAUNCH_PROCESS_START_TOTAL_COUNT_KEY_TEMPLATE)
                         .toString()));
+
+        // Verify source event metrics (see ActivityOptionsCompat#setLauncherSourceInfo).
+        String sourceEventDelayMetricKey = String.format(SOURCE_EVENT_DELAY_MILLIS_KEY_TEMPLATE,
+                CALCULATOR_PKG_NAME);
+        assertTrue(appLaunchMetrics.keySet().contains(sourceEventDelayMetricKey));
+        assertEquals(1,
+                appLaunchMetrics.get(sourceEventDelayMetricKey).toString().split(",").length);
+
         assertTrue(mAppStartupHelper.stopCollecting());
         mHelper.get().exit();
     }
@@ -161,13 +180,13 @@
         mHelper.get().open();
         SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
         mHelper.get().exit();
-        HelperTestUtility.clearApp(String.format(KILL_TEST_APP_CMD_TEMPLATE, CALENDAR_PKG_NAME));
+        HelperTestUtility.clearApp(String.format(KILL_TEST_APP_CMD_TEMPLATE, CALCULATOR_PKG_NAME));
         mHelper.get().open();
         Map<String, StringBuilder> appLaunchMetrics = mAppStartupHelper.getMetrics();
         // A metric key for the app cold launching should exist, and should hold two values.
-        String coldLaunchMetricKey = String.format(COLD_LAUNCH_KEY_TEMPLATE, CALENDAR_PKG_NAME);
+        String coldLaunchMetricKey = String.format(COLD_LAUNCH_KEY_TEMPLATE, CALCULATOR_PKG_NAME);
         String coldLaunchCountPkgKey = String.format(COLD_LAUNCH_COUNT_PKG_KEY_TEMPLATE,
-                CALENDAR_PKG_NAME);
+                CALCULATOR_PKG_NAME);
         assertTrue(appLaunchMetrics.keySet().contains(coldLaunchMetricKey));
         assertEquals(2, appLaunchMetrics.get(coldLaunchMetricKey).toString().split(",").length);
         assertEquals(2, Integer.parseInt(appLaunchMetrics.get(coldLaunchCountPkgKey).toString()));
@@ -175,10 +194,10 @@
                 .toString()));
 
         // Verify process start values.
-        String coldLaunchProcessMetricKey = String.format(COLD_LAUNCH_PROCESSS_FG_KEY_TEMPLATE,
-                CALENDAR_PKG_NAME);
+        String coldLaunchProcessMetricKey = String.format(COLD_LAUNCH_PROCESS_FG_KEY_TEMPLATE,
+                CALCULATOR_PKG_NAME);
         String coldLaunchProcessCountPkgKey = String.format(COLD_LAUNCH_COUNT_PKG_KEY_TEMPLATE,
-                CALENDAR_PKG_NAME);
+                CALCULATOR_PKG_NAME);
         assertTrue(appLaunchMetrics.keySet().contains(coldLaunchProcessMetricKey));
         assertEquals(2,
                 appLaunchMetrics.get(coldLaunchProcessMetricKey).toString().split(",").length);
@@ -201,50 +220,50 @@
     @Test
     public void testDifferentAppColdLaunchMetric() throws Exception {
 
-        // Open the calendar app.
+        // Open the calculator app.
         assertTrue(mAppStartupHelper.startCollecting());
         mHelper.get().open();
         SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
         mHelper.get().exit();
-        HelperTestUtility.clearApp(String.format(KILL_TEST_APP_CMD_TEMPLATE, CALENDAR_PKG_NAME));
+        HelperTestUtility.clearApp(String.format(KILL_TEST_APP_CMD_TEMPLATE, CALCULATOR_PKG_NAME));
 
-        // Open settings app
-        HelperTestUtility.launchPackageViaAdb(SETTINGS_PKG_NAME);
+        // Open clock app
+        mClockHelper.get().open();
         SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
-        HelperTestUtility.sendKeyCode(KEYCODE_HOME);
+        mClockHelper.get().exit();
         SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
-        HelperTestUtility.clearApp(String.format(KILL_TEST_APP_CMD_TEMPLATE, SETTINGS_PKG_NAME));
+        HelperTestUtility.clearApp(String.format(KILL_TEST_APP_CMD_TEMPLATE, CLOCK_PKG_NAME));
         SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
 
         Map<String, StringBuilder> appLaunchMetrics = mAppStartupHelper.getMetrics();
-        String coldLaunchCalendarMetricKey = String.format(COLD_LAUNCH_KEY_TEMPLATE,
-                CALENDAR_PKG_NAME);
-        String coldLaunchSettingsMetricKey = String.format(COLD_LAUNCH_KEY_TEMPLATE,
-                SETTINGS_PKG_NAME);
-        String coldLaunchCalendarCountPkgKey = String.format(COLD_LAUNCH_COUNT_PKG_KEY_TEMPLATE,
-                CALENDAR_PKG_NAME);
-        String coldLaunchSettingsCountPkgKey = String.format(COLD_LAUNCH_COUNT_PKG_KEY_TEMPLATE,
-                CALENDAR_PKG_NAME);
-        assertTrue(appLaunchMetrics.keySet().contains(coldLaunchCalendarMetricKey));
-        assertTrue(appLaunchMetrics.keySet().contains(coldLaunchSettingsMetricKey));
+        String coldLaunchCalculatorMetricKey = String.format(COLD_LAUNCH_KEY_TEMPLATE,
+                CALCULATOR_PKG_NAME);
+        String coldLaunchClockMetricKey = String.format(COLD_LAUNCH_KEY_TEMPLATE,
+                CLOCK_PKG_NAME);
+        String coldLaunchCalculatorCountPkgKey = String.format(COLD_LAUNCH_COUNT_PKG_KEY_TEMPLATE,
+                CALCULATOR_PKG_NAME);
+        String coldLaunchClockCountPkgKey = String.format(COLD_LAUNCH_COUNT_PKG_KEY_TEMPLATE,
+                CALCULATOR_PKG_NAME);
+        assertTrue(appLaunchMetrics.keySet().contains(coldLaunchCalculatorMetricKey));
+        assertTrue(appLaunchMetrics.keySet().contains(coldLaunchClockMetricKey));
         assertEquals(1,
-                appLaunchMetrics.get(coldLaunchCalendarMetricKey).toString().split(",").length);
+                appLaunchMetrics.get(coldLaunchCalculatorMetricKey).toString().split(",").length);
         assertEquals(1,
-                appLaunchMetrics.get(coldLaunchSettingsCountPkgKey).toString().split(",").length);
+                appLaunchMetrics.get(coldLaunchClockCountPkgKey).toString().split(",").length);
         assertEquals(1,
-                Integer.parseInt(appLaunchMetrics.get(coldLaunchCalendarCountPkgKey).toString()));
+                Integer.parseInt(appLaunchMetrics.get(coldLaunchCalculatorCountPkgKey).toString()));
         assertEquals(1,
-                Integer.parseInt(appLaunchMetrics.get(coldLaunchSettingsCountPkgKey).toString()));
+                Integer.parseInt(appLaunchMetrics.get(coldLaunchClockCountPkgKey).toString()));
         assertEquals(2, Integer.parseInt(appLaunchMetrics.get(COLD_LAUNCH_TOTAL_COUNT_KEY_TEMPLATE)
                 .toString()));
 
         // Verify transition metrics.
         String coldLaunchTransCalMetricKey = String.format(
                 COLD_LAUNCH_TRANSITION_DELAY_MILLIS_KEY_TEMPLATE,
-                CALENDAR_PKG_NAME);
+                CALCULATOR_PKG_NAME);
         String coldLaunchTransSetMetricKey = String.format(
                 COLD_LAUNCH_TRANSITION_DELAY_MILLIS_KEY_TEMPLATE,
-                SETTINGS_PKG_NAME);
+                CLOCK_PKG_NAME);
         assertTrue(appLaunchMetrics.keySet().contains(coldLaunchTransCalMetricKey));
         assertTrue(appLaunchMetrics.keySet().contains(coldLaunchTransSetMetricKey));
         assertEquals(1,
@@ -253,15 +272,15 @@
                 appLaunchMetrics.get(coldLaunchTransSetMetricKey).toString().split(",").length);
 
         // Verify process start values.
-        String coldLaunchProcessMetricKey = String.format(COLD_LAUNCH_PROCESSS_FG_KEY_TEMPLATE,
-                CALENDAR_PKG_NAME);
+        String coldLaunchProcessMetricKey = String.format(COLD_LAUNCH_PROCESS_FG_KEY_TEMPLATE,
+                CALCULATOR_PKG_NAME);
         String coldLaunchProcessCountPkgKey = String.format(COLD_LAUNCH_COUNT_PKG_KEY_TEMPLATE,
-                CALENDAR_PKG_NAME);
-        String coldLaunchProcessMetricSettingsKey = String.format(
-                COLD_LAUNCH_PROCESSS_FG_KEY_TEMPLATE, SETTINGS_PKG_NAME);
-        String coldLaunchProcessCountSettingsPkgKey = String.format(
+                CALCULATOR_PKG_NAME);
+        String coldLaunchProcessMetricClockKey = String.format(
+                COLD_LAUNCH_PROCESS_FG_KEY_TEMPLATE, CLOCK_PKG_NAME);
+        String coldLaunchProcessCountClockPkgKey = String.format(
                 COLD_LAUNCH_COUNT_PKG_KEY_TEMPLATE,
-                SETTINGS_PKG_NAME);
+                CLOCK_PKG_NAME);
 
         assertTrue(appLaunchMetrics.keySet().contains(coldLaunchProcessMetricKey));
         assertEquals(1,
@@ -270,8 +289,8 @@
                 Integer.parseInt(appLaunchMetrics.get(coldLaunchProcessCountPkgKey).toString()));
         assertEquals(
                 1, appLaunchMetrics.get(
-                        coldLaunchProcessMetricSettingsKey).toString().split(",").length);
-        assertEquals(1, Integer.parseInt(appLaunchMetrics.get(coldLaunchProcessCountSettingsPkgKey)
+                        coldLaunchProcessMetricClockKey).toString().split(",").length);
+        assertEquals(1, Integer.parseInt(appLaunchMetrics.get(coldLaunchProcessCountClockPkgKey)
                 .toString()));
 
         // Sometimes I see background process started during the test counted towards total count
@@ -284,17 +303,30 @@
 
     }
 
+    public class TestableRule extends FinishActivitiesWithoutProcessKillRule {
+
+        public TestableRule(String appPackageName) {
+            super(appPackageName);
+        }
+
+        @Override
+        public void starting(Description description) {
+            super.starting(description);
+        }
+    }
+
     /**
      * Test warm launch metric.
      */
     @Test
     public void testWarmLaunchMetric() throws Exception {
-        // Launch the app once and exit it so it resides in memory.
+        TestableRule finishActivitiesRule = new TestableRule(CALCULATOR_PKG_NAME);
+
+
         mHelper.get().open();
         SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
-        // Press home and clear the cache explicitly.
-        HelperTestUtility.sendKeyCode(KEYCODE_HOME);
-        HelperTestUtility.clearCache();
+        finishActivitiesRule.starting(Description.createTestDescription("clzz", "mthd"));
+
         SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
         // Start the collection here to test warm launch.
         assertTrue(mAppStartupHelper.startCollecting());
@@ -302,15 +334,15 @@
         mHelper.get().open();
         SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
         Map<String, StringBuilder> appLaunchMetrics = mAppStartupHelper.getMetrics();
-        String calendarWarmLaunchKey = String.format(WARM_LAUNCH_KEY_TEMPLATE, CALENDAR_PKG_NAME);
-        assertTrue(appLaunchMetrics.keySet().contains(calendarWarmLaunchKey));
-        assertEquals(1, appLaunchMetrics.get(calendarWarmLaunchKey).toString().split(",").length);
+        String calculatorWarmLaunchKey = String.format(WARM_LAUNCH_KEY_TEMPLATE, CALCULATOR_PKG_NAME);
+        assertTrue(appLaunchMetrics.keySet().contains(calculatorWarmLaunchKey));
+        assertEquals(1, appLaunchMetrics.get(calculatorWarmLaunchKey).toString().split(",").length);
         assertTrue(mAppStartupHelper.stopCollecting());
 
         // Verify transition metrics.
         String warmLaunchTransitionMetricKey = String.format(
                 WARM_LAUNCH_TRANSITION_DELAY_MILLIS_KEY_TEMPLATE,
-                CALENDAR_PKG_NAME);
+                CALCULATOR_PKG_NAME);
         assertTrue(appLaunchMetrics.keySet().contains(warmLaunchTransitionMetricKey));
         assertEquals(1,
                 appLaunchMetrics.get(warmLaunchTransitionMetricKey).toString().split(",").length);
@@ -319,33 +351,34 @@
     }
 
     /**
-     * Test hot launch metric on settings, which is lightweight enough to trigger a hot launch.
+     * Test hot launch metric on clock, which is lightweight enough to trigger a hot launch.
      */
     @Test
     public void testHotLaunchMetric() throws Exception {
         // Launch the app once and go home so the app resides in memory.
-        HelperTestUtility.launchPackageViaAdb(SETTINGS_PKG_NAME);
-        HelperTestUtility.sendKeyCode(KEYCODE_HOME);
+        mHelper.get().open();
+        SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
+        mHelper.get().exit();
         // Start the collection here to test hot launch.
         assertTrue(mAppStartupHelper.startCollecting());
         SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
         // Launch the app; a hot launch occurs.
-        HelperTestUtility.launchPackageViaAdb(SETTINGS_PKG_NAME);
+        mHelper.get().open();
         SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
         Map<String, StringBuilder> appLaunchMetrics = mAppStartupHelper.getMetrics();
-        String calculatoHotLaunchKey = String.format(HOT_LAUNCH_KEY_TEMPLATE, SETTINGS_PKG_NAME);
-        assertTrue(appLaunchMetrics.keySet().contains(calculatoHotLaunchKey));
-        assertEquals(1, appLaunchMetrics.get(calculatoHotLaunchKey).toString().split(",").length);
+        String calculatorHotLaunchKey = String.format(HOT_LAUNCH_KEY_TEMPLATE, CALCULATOR_PKG_NAME);
+        assertTrue(appLaunchMetrics.keySet().contains(calculatorHotLaunchKey));
+        assertEquals(1, appLaunchMetrics.get(calculatorHotLaunchKey).toString().split(",").length);
         assertTrue(mAppStartupHelper.stopCollecting());
-        HelperTestUtility.sendKeyCode(KEYCODE_HOME);
+        mHelper.get().exit();
         SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
-        HelperTestUtility.clearApp(String.format(KILL_TEST_APP_CMD_TEMPLATE, SETTINGS_PKG_NAME));
+        HelperTestUtility.clearApp(String.format(KILL_TEST_APP_CMD_TEMPLATE, CALCULATOR_PKG_NAME));
         SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
 
         // Verify transition metrics.
         String hotLaunchTransitionMetricKey = String.format(
                 HOT_LAUNCH_TRANSITION_DELAY_MILLIS_KEY_TEMPLATE,
-                SETTINGS_PKG_NAME);
+                CALCULATOR_PKG_NAME);
         assertTrue(appLaunchMetrics.keySet().contains(hotLaunchTransitionMetricKey));
         assertEquals(1,
                 appLaunchMetrics.get(hotLaunchTransitionMetricKey).toString().split(",").length);
@@ -357,25 +390,25 @@
      */
     @Test
     public void testSingleLaunchStartupFullyDrawnMetric() throws Exception {
-        // The Settings app is used here as it calls reportFullyDrawn(), which is required for the
+        // The Clock app is used here as it calls reportFullyDrawn(), which is required for the
         // AppStartFullyDrawn metric to be collected.
-        // Start metric collection and then launch the Settings app.
+        // Start metric collection and then launch the Clock app.
         assertTrue(mAppStartupHelper.startCollecting());
-        HelperTestUtility.launchPackageViaAdb(SETTINGS_PKG_NAME);
+        mClockHelper.get().open();
         SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
         // Check that the collected metrics contains the key for the AppStartFullyDrawn metric.
         boolean hasFullyDrawnKey = false;
         Map<String, StringBuilder> appLaunchMetrics = mAppStartupHelper.getMetrics();
         for (String key : appLaunchMetrics.keySet()) {
-            if (key.contains(FULLY_DRAWN_KEY_KEYWORD) && key.contains(SETTINGS_PKG_NAME)) {
+            if (key.contains(FULLY_DRAWN_KEY_KEYWORD) && key.contains(CLOCK_PKG_NAME)) {
                 hasFullyDrawnKey = true;
             }
         }
         assertTrue(hasFullyDrawnKey);
         assertTrue(mAppStartupHelper.stopCollecting());
-        HelperTestUtility.sendKeyCode(KEYCODE_HOME);
+        mClockHelper.get().exit();
         SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
-        HelperTestUtility.clearApp(String.format(KILL_TEST_APP_CMD_TEMPLATE, SETTINGS_PKG_NAME));
+        HelperTestUtility.clearApp(String.format(KILL_TEST_APP_CMD_TEMPLATE, CLOCK_PKG_NAME));
         SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
     }
 
@@ -384,28 +417,28 @@
      */
     @Test
     public void testMultipleLaunchStartupFullyDrawnMetric() throws Exception {
-        // The Settings app is used here as it calls reportFullyDrawn(), which is required for the
+        // The Clock app is used here as it calls reportFullyDrawn(), which is required for the
         // AppStartFullyDrawn metric to be collected.
-        // Start metric collection and then cold launch the Settings app twice, as in this app
+        // Start metric collection and then cold launch the clock app twice, as in this app
         // reportFullyDrawn() is only called during cold launch (the calling function is only called
         // during onCreate()).
         assertTrue(mAppStartupHelper.startCollecting());
         // 1st launch and kill.
-        HelperTestUtility.launchPackageViaAdb(SETTINGS_PKG_NAME);
+        mClockHelper.get().open();
         SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
-        HelperTestUtility.sendKeyCode(KEYCODE_HOME);
+        mClockHelper.get().exit();
         SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
-        HelperTestUtility.clearApp(String.format(KILL_TEST_APP_CMD_TEMPLATE, SETTINGS_PKG_NAME));
+        HelperTestUtility.clearApp(String.format(KILL_TEST_APP_CMD_TEMPLATE, CLOCK_PKG_NAME));
         SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
         // 2nd launch.
-        HelperTestUtility.launchPackageViaAdb(SETTINGS_PKG_NAME);
+        mClockHelper.get().open();
         SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
         // Check that the collected metrics contains the key for the AppStartFullyDrawn metric,
         // and that there are two values under this key.
         boolean hasFullyDrawnKey = false;
         Map<String, StringBuilder> appLaunchMetrics = mAppStartupHelper.getMetrics();
         for (String key : appLaunchMetrics.keySet()) {
-            if (key.contains(FULLY_DRAWN_KEY_KEYWORD) && key.contains(SETTINGS_PKG_NAME)) {
+            if (key.contains(FULLY_DRAWN_KEY_KEYWORD) && key.contains(CLOCK_PKG_NAME)) {
                 hasFullyDrawnKey = true;
                 // There should be two values under this key.
                 assertEquals(2, appLaunchMetrics.get(key).toString().split(",").length);
@@ -413,9 +446,9 @@
         }
         assertTrue(hasFullyDrawnKey);
         assertTrue(mAppStartupHelper.stopCollecting());
-        HelperTestUtility.sendKeyCode(KEYCODE_HOME);
+        mClockHelper.get().exit();
         SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
-        HelperTestUtility.clearApp(String.format(KILL_TEST_APP_CMD_TEMPLATE, SETTINGS_PKG_NAME));
+        HelperTestUtility.clearApp(String.format(KILL_TEST_APP_CMD_TEMPLATE, CLOCK_PKG_NAME));
         SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
     }
 
@@ -429,9 +462,9 @@
         mHelper.get().open();
         Map<String, StringBuilder> appLaunchMetrics = mAppStartupHelper.getMetrics();
         // A metric key for the app cold launching should exist, and should only hold one value.
-        String coldLaunchMetricKey = String.format(COLD_LAUNCH_KEY_TEMPLATE, CALENDAR_PKG_NAME);
+        String coldLaunchMetricKey = String.format(COLD_LAUNCH_KEY_TEMPLATE, CALCULATOR_PKG_NAME);
         String coldLaunchCountPkgKey = String.format(COLD_LAUNCH_COUNT_PKG_KEY_TEMPLATE,
-                CALENDAR_PKG_NAME);
+                CALCULATOR_PKG_NAME);
         assertTrue(appLaunchMetrics.keySet().contains(coldLaunchMetricKey));
         assertEquals(1, appLaunchMetrics.get(coldLaunchMetricKey).toString().split(",").length);
         assertEquals(1, Integer.parseInt(appLaunchMetrics.get(coldLaunchCountPkgKey).toString()));
@@ -439,10 +472,10 @@
                 .toString()));
 
         // Verify process start detailed values are not added.
-        String coldLaunchProcessMetricKey = String.format(COLD_LAUNCH_PROCESSS_FG_KEY_TEMPLATE,
-                CALENDAR_PKG_NAME);
+        String coldLaunchProcessMetricKey = String.format(COLD_LAUNCH_PROCESS_FG_KEY_TEMPLATE,
+                CALCULATOR_PKG_NAME);
         String coldLaunchProcessCountPkgKey = String.format(COLD_LAUNCH_COUNT_PKG_KEY_TEMPLATE,
-                CALENDAR_PKG_NAME);
+                CALCULATOR_PKG_NAME);
         assertFalse(appLaunchMetrics.keySet().contains(coldLaunchProcessMetricKey));
         assertTrue(appLaunchMetrics.keySet().contains(coldLaunchProcessCountPkgKey));
 
@@ -452,4 +485,11 @@
         assertTrue(mAppStartupHelper.stopCollecting());
         mHelper.get().exit();
     }
+
+    @After
+    public void tearDown() {
+        mAppStartupHelper.stopCollecting();
+        // Unlock the orientation
+        HelperTestUtility.unfreezeRotation();
+    }
 }
diff --git a/libraries/collectors-helper/statsd/test/src/com/android/helpers/CpuUsageHelperTest.java b/libraries/collectors-helper/statsd/test/src/com/android/helpers/CpuUsageHelperTest.java
index ed55156..d39fd87 100644
--- a/libraries/collectors-helper/statsd/test/src/com/android/helpers/CpuUsageHelperTest.java
+++ b/libraries/collectors-helper/statsd/test/src/com/android/helpers/CpuUsageHelperTest.java
@@ -16,11 +16,11 @@
 package com.android.helpers;
 
 import android.platform.helpers.HelperAccessor;
-import android.platform.helpers.ICalendarHelper;
+import android.platform.helpers.ICalculatorHelper;
+
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.helpers.CpuUsageHelper;
-
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -41,24 +41,20 @@
 @RunWith(AndroidJUnit4.class)
 public class CpuUsageHelperTest {
 
-    // Kill the calendar app.
-    private static final String KILL_TEST_APP_CMD = "am force-stop com.google.android.calendar";
-    // Key prefix used for cpu usage by frequency index.
-    private static final String CPU_USAGE_FREQ_PREFIX = "cpu_usage_freq";
+    // Kill the calculator app.
+    private static final String KILL_TEST_APP_CMD = "am force-stop com.google.android.calculator";
     // Key prefix used for cpu usage by package name or uid
     private static final String CPU_USAGE_PKG_UID_PREFIX = "cpu_usage_pkg_or_uid";
     // Key used for total CPU usage
     private static final String TOTAL_CPU_USAGE = "total_cpu_usage";
-    // Key used for total CPU usage in frequency buckets
-    private static final String TOTAL_CPU_USAGE_FREQ = "total_cpu_usage_freq";
     // key used for total CPU time
     private static final String TOTAL_CPU_TIME = "total_cpu_time";
     // Key used for CPU utilization average over each core
     private static final String CPU_UTILIZATION = "cpu_utilization_average_per_core_percent";
 
     private CpuUsageHelper mCpuUsageHelper;
-    private HelperAccessor<ICalendarHelper> mHelper =
-            new HelperAccessor<>(ICalendarHelper.class);
+    private HelperAccessor<ICalculatorHelper> mHelper =
+            new HelperAccessor<>(ICalculatorHelper.class);
 
     @Before
     public void setUp() {
@@ -96,29 +92,16 @@
     @Test
     public void testCpuUsageMetricsKey() throws Exception {
         // Variables to verify existence of collected metrics.
-        boolean isFreqIndexPresent = false;
         boolean isPkgorUidPresent = false;
-        boolean isFreqUsed = false;
         boolean isUIDUsed = false;
         boolean isTotalCpuUsageEntryPresent = false;
         boolean isTotalCpuUsageValuePresent = false;
-        boolean isTotalCpuFreqEntryPresent = false;
-        boolean isTotalCpuFreqValuePresent = false;
         assertTrue(mCpuUsageHelper.startCollecting());
         mHelper.get().open();
         // Variables to Verify that the reported usage does sum up to the reported total usage.
         Long sumCpuUsage = 0L;
-        Long sumCpuUsageFreq = 0L;
         Long reportedTotalCpuUsage = 0L;
-        Long reportedTotalCpuUsageFreq = 0L;
         for (Map.Entry<String, Long> cpuUsageEntry : mCpuUsageHelper.getMetrics().entrySet()) {
-            if (cpuUsageEntry.getKey().startsWith(CPU_USAGE_FREQ_PREFIX)) {
-                isFreqIndexPresent = true;
-                if (cpuUsageEntry.getValue() > 0) {
-                    isFreqUsed = true;
-                }
-                sumCpuUsageFreq += cpuUsageEntry.getValue();
-            }
             if (cpuUsageEntry.getKey().startsWith(CPU_USAGE_PKG_UID_PREFIX)) {
                 isPkgorUidPresent = true;
                 if (cpuUsageEntry.getValue() > 0) {
@@ -126,13 +109,6 @@
                 }
                 sumCpuUsage += cpuUsageEntry.getValue();
             }
-            if (cpuUsageEntry.getKey().equals(TOTAL_CPU_USAGE_FREQ)) {
-                isTotalCpuFreqEntryPresent = true;
-                if (cpuUsageEntry.getValue() > 0) {
-                    isTotalCpuFreqValuePresent = true;
-                }
-                reportedTotalCpuUsageFreq = cpuUsageEntry.getValue();
-            }
             if (cpuUsageEntry.getKey().equals(TOTAL_CPU_USAGE)) {
                 isTotalCpuUsageEntryPresent = true;
                 if (cpuUsageEntry.getValue() > 0) {
@@ -141,29 +117,24 @@
                 reportedTotalCpuUsage = cpuUsageEntry.getValue();
             }
         }
-        assertTrue(isFreqIndexPresent && isFreqUsed);
         assertTrue(isPkgorUidPresent && isUIDUsed);
         assertTrue(isTotalCpuUsageEntryPresent && isTotalCpuUsageValuePresent);
-        assertTrue(isTotalCpuFreqEntryPresent && isTotalCpuFreqValuePresent);
-        assertEquals(sumCpuUsageFreq, reportedTotalCpuUsageFreq);
         assertEquals(sumCpuUsage, reportedTotalCpuUsage);
         assertTrue(mCpuUsageHelper.stopCollecting());
         mHelper.get().exit();
     }
 
     /**
-     * Test cpu usage metrics are not collected per pkg and per freq
-     * and collected only total cpu usage by freq and packages.
+     * Test cpu usage metrics are not collected per pkg and per freq and collected only total cpu
+     * usage by freq and packages.
      */
     @Test
-    public void testCpuDisabledPerPkgPerFreq() throws Exception {
+    public void testCpuDisabledPerPkg() throws Exception {
         mCpuUsageHelper.setDisablePerPackage();
-        mCpuUsageHelper.setDisablePerFrequency();
         assertTrue(mCpuUsageHelper.startCollecting());
         mHelper.get().open();
         Map<String, Long> cpuUsage = mCpuUsageHelper.getMetrics();
-        assertTrue(cpuUsage.size() == 2);
-        assertTrue(cpuUsage.containsKey(TOTAL_CPU_USAGE_FREQ));
+        assertTrue(cpuUsage.size() == 1);
         assertTrue(cpuUsage.containsKey(TOTAL_CPU_USAGE));
         assertTrue(mCpuUsageHelper.stopCollecting());
         mHelper.get().exit();
@@ -177,8 +148,6 @@
     @Test
     public void testCpuUsageOnlyPerPkg() throws Exception {
         mCpuUsageHelper.setDisableTotalPackage();
-        mCpuUsageHelper.setDisableTotalFrequency();
-        mCpuUsageHelper.setDisablePerFrequency();
         assertTrue(mCpuUsageHelper.startCollecting());
         mHelper.get().open();
         Map<String, Long> cpuUsage = mCpuUsageHelper.getMetrics();
@@ -191,27 +160,6 @@
     }
 
     /**
-     * Test cpu usage metrics are not collected per package, total
-     * usage by freq and by packages and collected only cpu usage
-     * per frequency.
-     */
-    @Test
-    public void testCpuUsageOnlyPerFreq() throws Exception {
-        mCpuUsageHelper.setDisableTotalPackage();
-        mCpuUsageHelper.setDisableTotalFrequency();
-        mCpuUsageHelper.setDisablePerPackage();
-        assertTrue(mCpuUsageHelper.startCollecting());
-        mHelper.get().open();
-        Map<String, Long> cpuUsage = mCpuUsageHelper.getMetrics();
-        assertTrue(cpuUsage.size() > 2);
-        for (Map.Entry<String, Long> cpuUsageEntry : mCpuUsageHelper.getMetrics().entrySet()) {
-            assertTrue(cpuUsageEntry.getKey().startsWith(CPU_USAGE_FREQ_PREFIX));
-        }
-        assertTrue(mCpuUsageHelper.stopCollecting());
-        mHelper.get().exit();
-    }
-
-    /**
      * Test cpu utilization is collected.
      */
     @Test
@@ -233,4 +181,9 @@
         assertTrue(mCpuUsageHelper.stopCollecting());
         mHelper.get().exit();
     }
+
+    @After
+    public void tearDown() {
+        mCpuUsageHelper.stopCollecting();
+    }
 }
diff --git a/libraries/collectors-helper/statsd/test/src/com/android/helpers/CrashHelperTest.java b/libraries/collectors-helper/statsd/test/src/com/android/helpers/CrashHelperTest.java
index 40d473e..c27c896 100644
--- a/libraries/collectors-helper/statsd/test/src/com/android/helpers/CrashHelperTest.java
+++ b/libraries/collectors-helper/statsd/test/src/com/android/helpers/CrashHelperTest.java
@@ -24,6 +24,7 @@
 import androidx.test.uiautomator.By;
 import androidx.test.uiautomator.UiObject2;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -54,12 +55,12 @@
     // Key used to store the ANR.
     private static final String TOTAL_ANRS_KEY = "total_anr";
     // Detailed crash key associated with the package name and the foreground status.
-    private static final String CRASH_PKG_KEY = "crash_com.android.development_FOREGROUND";
+    private static final String CRASH_PKG_KEY = "crash_com.android.development_2";
     // Detailed native crash key associated with the package name and the foreground status.
     private static final String NATIVE_CRASH_PKG_KEY =
-            "native_crash_com.android.development_FOREGROUND";
+            "native_crash_com.android.development_2";
     // Detailed event key associated with the ANR: process, reason and foreground status.
-    private static final String ANR_DETAIL_KEY = "anr_com.android.development_FOREGROUND";
+    private static final String ANR_DETAIL_KEY = "anr_com.android.development_2";
     // Button id to cause the crash.
     private static final String CRASH_BTN_NAME = "bad_behavior_crash_main";
     // Button id to cause the native crash.
@@ -77,6 +78,8 @@
         mCrashHelper = new CrashHelper();
         // Make the apps are starting from the clean state.
         HelperTestUtility.clearApp(KILL_TEST_APP_CMD);
+        // Lock the orientation.
+        HelperTestUtility.setOrientationNatural();
     }
 
     /**
@@ -216,4 +219,11 @@
     private void invokeBehavior(String resourceName) {
         invokeBehavior(resourceName, 0);
     }
+
+    @After
+    public void tearDown() {
+        mCrashHelper.stopCollecting();
+        // Unlock the orientation
+        HelperTestUtility.unfreezeRotation();
+    }
 }
diff --git a/libraries/collectors-helper/statsd/test/src/com/android/helpers/HelperTestUtility.java b/libraries/collectors-helper/statsd/test/src/com/android/helpers/HelperTestUtility.java
index 0d9be6a..5c726b7 100644
--- a/libraries/collectors-helper/statsd/test/src/com/android/helpers/HelperTestUtility.java
+++ b/libraries/collectors-helper/statsd/test/src/com/android/helpers/HelperTestUtility.java
@@ -18,6 +18,7 @@
 import android.app.KeyguardManager;
 import android.content.Context;
 import android.hardware.display.DisplayManager;
+import android.os.RemoteException;
 import android.os.SystemClock;
 import android.view.Display;
 
@@ -40,7 +41,7 @@
     // Keycode(s) for convenience functions.
     private static final String KEYCODE_WAKEUP = "KEYCODE_WAKEUP";
     // Delay between actions happening in the device.
-    public static final int ACTION_DELAY = 2000;
+    public static final int ACTION_DELAY = 4000;
 
     private static UiDevice mDevice;
 
@@ -119,5 +120,27 @@
             SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
         }
     }
+
+    /**
+     * This method will lock orientation.
+     */
+    public static void setOrientationNatural() {
+        try {
+            getUiDevice().setOrientationNatural();
+        } catch (RemoteException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * This method will unlock orientation.
+     */
+    public static void unfreezeRotation() {
+        try {
+            mDevice.unfreezeRotation();
+        } catch (RemoteException e) {
+            throw new RuntimeException(e);
+        }
+    }
 }
 
diff --git a/libraries/collectors-helper/statsd/test/src/com/android/helpers/ThermalHelperTest.java b/libraries/collectors-helper/statsd/test/src/com/android/helpers/ThermalHelperTest.java
index 77ca5b4..e100519 100644
--- a/libraries/collectors-helper/statsd/test/src/com/android/helpers/ThermalHelperTest.java
+++ b/libraries/collectors-helper/statsd/test/src/com/android/helpers/ThermalHelperTest.java
@@ -20,16 +20,15 @@
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.when;
 
-import android.os.TemperatureTypeEnum;
-import android.os.ThrottlingSeverityEnum;
+import android.os.nano.OsProtoEnums;
 
 import androidx.test.runner.AndroidJUnit4;
 import androidx.test.uiautomator.UiDevice;
 
-import com.android.os.AtomsProto.Atom;
-import com.android.os.AtomsProto.ThermalThrottlingSeverityStateChanged;
-import com.android.os.StatsLog.EventMetricData;
+import com.android.os.nano.AtomsProto;
+import com.android.os.nano.StatsLog;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -85,11 +84,12 @@
     /** Test that only the initial value shows up when there are no events. */
     @Test
     public void testInitialMetricsWithoutEvents() throws Exception {
-        when(mStatsdHelper.getEventMetrics()).thenReturn(new ArrayList<EventMetricData>());
+        when(mStatsdHelper.getEventMetrics()).thenReturn(new ArrayList<StatsLog.EventMetricData>());
         assertTrue(mThermalHelper.startCollecting());
+
         assertEquals(
                 mThermalHelper.getMetrics().get(THROTTLING_KEY).toString(),
-                String.valueOf(ThrottlingSeverityEnum.MODERATE.getNumber()));
+                String.valueOf(OsProtoEnums.MODERATE));
         assertTrue(mThermalHelper.stopCollecting());
     }
 
@@ -100,17 +100,17 @@
                 .thenReturn(
                         getFakeEventMetrics(
                                 getThermalThrottlingSeverityStateChangedEvent(
-                                        TemperatureTypeEnum.TEMPERATURE_TYPE_SKIN,
+                                        OsProtoEnums.TEMPERATURE_TYPE_SKIN,
                                         "sensor_name",
-                                        ThrottlingSeverityEnum.LIGHT)));
+                                        OsProtoEnums.LIGHT)));
         assertTrue(mThermalHelper.startCollecting());
         Map<String, StringBuilder> metrics = mThermalHelper.getMetrics();
         assertEquals(
                 metrics.get(THROTTLING_KEY).toString(),
                 String.join(
                         ",",
-                        String.valueOf(ThrottlingSeverityEnum.MODERATE.getNumber()),
-                        String.valueOf(ThrottlingSeverityEnum.LIGHT.getNumber())));
+                        String.valueOf(OsProtoEnums.MODERATE),
+                        String.valueOf(OsProtoEnums.LIGHT)));
         assertTrue(mThermalHelper.stopCollecting());
     }
 
@@ -121,17 +121,17 @@
                 .thenReturn(
                         getFakeEventMetrics(
                                 getThermalThrottlingSeverityStateChangedEvent(
-                                        TemperatureTypeEnum.TEMPERATURE_TYPE_SKIN,
+                                        OsProtoEnums.TEMPERATURE_TYPE_SKIN,
                                         "sensor1_name",
-                                        ThrottlingSeverityEnum.LIGHT),
+                                        OsProtoEnums.LIGHT),
                                 getThermalThrottlingSeverityStateChangedEvent(
-                                        TemperatureTypeEnum.TEMPERATURE_TYPE_CPU,
+                                        OsProtoEnums.TEMPERATURE_TYPE_CPU,
                                         "sensor2_name",
-                                        ThrottlingSeverityEnum.MODERATE),
+                                        OsProtoEnums.MODERATE),
                                 getThermalThrottlingSeverityStateChangedEvent(
-                                        TemperatureTypeEnum.TEMPERATURE_TYPE_GPU,
+                                        OsProtoEnums.TEMPERATURE_TYPE_GPU,
                                         "sensor3_name",
-                                        ThrottlingSeverityEnum.NONE)));
+                                        OsProtoEnums.NONE)));
 
         assertTrue(mThermalHelper.startCollecting());
         Map<String, StringBuilder> metrics = mThermalHelper.getMetrics();
@@ -139,41 +139,81 @@
                 metrics.get(THROTTLING_KEY).toString(),
                 String.join(
                         ",",
-                        String.valueOf(ThrottlingSeverityEnum.MODERATE.getNumber()),
-                        String.valueOf(ThrottlingSeverityEnum.LIGHT.getNumber()),
-                        String.valueOf(ThrottlingSeverityEnum.MODERATE.getNumber()),
-                        String.valueOf(ThrottlingSeverityEnum.NONE.getNumber())));
+                        String.valueOf(OsProtoEnums.MODERATE),
+                        String.valueOf(OsProtoEnums.LIGHT),
+                        String.valueOf(OsProtoEnums.MODERATE),
+                        String.valueOf(OsProtoEnums.NONE)));
         assertTrue(mThermalHelper.stopCollecting());
     }
 
-    /** Returns a list of {@link EventMetricData} that statsd returns. */
-    private List<EventMetricData> getFakeEventMetrics(
-            ThermalThrottlingSeverityStateChanged... throttleSeverityEvents) {
-        List<EventMetricData> result = new ArrayList<>();
-        for (ThermalThrottlingSeverityStateChanged event : throttleSeverityEvents) {
-            result.add(
-                    EventMetricData.newBuilder()
-                            .setAtom(
-                                    Atom.newBuilder()
-                                            .setThermalThrottlingSeverityStateChanged(event))
-                            .build());
+    /** Test that the temperature section is parsed correctly. */
+    @Test
+    public void testParseTemperature() throws Exception {
+        // Use real data for this test. It should work everywhere.
+        mThermalHelper = new ThermalHelper();
+        mThermalHelper.setStatsdHelper(mStatsdHelper);
+        assertTrue(mThermalHelper.startCollecting());
+        Map<String, StringBuilder> metrics = mThermalHelper.getMetrics();
+        // Validate at least 2 temperature keys exist with all 3 metrics.
+        int statusMetricsFound = 0;
+        int valueMetricsFound = 0;
+        int typeMetricsFound = 0;
+        for (String key : metrics.keySet()) {
+            if (!key.startsWith("temperature")) {
+                continue;
+            }
+
+            if (key.endsWith("status")) {
+                statusMetricsFound++;
+            } else if (key.endsWith("value")) {
+                valueMetricsFound++;
+            } else if (key.endsWith("type")) {
+                typeMetricsFound++;
+            }
+        }
+
+        assertTrue(
+                "Didn't find at least 2 status, value, and type temperature metrics.",
+                statusMetricsFound >= 2 && valueMetricsFound >= 2 && typeMetricsFound >= 2);
+        assertTrue(mThermalHelper.stopCollecting());
+    }
+
+    /**
+     * Returns a list of {@link com.android.os.nano.StatsLog.EventMetricData} that statsd returns.
+     */
+    private List<StatsLog.EventMetricData> getFakeEventMetrics(
+            AtomsProto.ThermalThrottlingSeverityStateChanged... throttleSeverityEvents) {
+        List<StatsLog.EventMetricData> result = new ArrayList<>();
+        for (AtomsProto.ThermalThrottlingSeverityStateChanged event : throttleSeverityEvents) {
+            AtomsProto.Atom atom = new AtomsProto.Atom();
+            atom.setThermalThrottlingSeverityStateChanged(event);
+            StatsLog.EventMetricData metricData = new StatsLog.EventMetricData();
+            metricData.atom = atom;
+            result.add(metricData);
         }
         return result;
     }
 
     /** Returns a state change protobuf for thermal throttling severity. */
-    private ThermalThrottlingSeverityStateChanged getThermalThrottlingSeverityStateChangedEvent(
-            TemperatureTypeEnum type, String name, ThrottlingSeverityEnum severity) {
-        return ThermalThrottlingSeverityStateChanged.newBuilder()
-                .setSensorType(type)
-                .setSensorName(name)
-                .setSeverity(severity)
-                .build();
+    private AtomsProto.ThermalThrottlingSeverityStateChanged
+            getThermalThrottlingSeverityStateChangedEvent(int type, String name, int severity) {
+        AtomsProto.ThermalThrottlingSeverityStateChanged stateChanged =
+                new AtomsProto.ThermalThrottlingSeverityStateChanged();
+
+        stateChanged.sensorType = type;
+        stateChanged.sensorName = name;
+        stateChanged.severity = severity;
+        return stateChanged;
     }
 
     /** Get the thermal metric key for a thermal sensor type and name. */
-    private String getMetricKey(TemperatureTypeEnum type, String name) {
+    private String getMetricKey(int type, String name) {
         return MetricUtility.constructKey(
                 "thermal", ThermalHelper.getShorthandSensorType(type), name);
     }
+
+    @After
+    public void tearDown() {
+        mThermalHelper.stopCollecting();
+    }
 }
diff --git a/libraries/collectors-helper/statsd/test/src/com/android/helpers/UiActionLatencyHelperTest.java b/libraries/collectors-helper/statsd/test/src/com/android/helpers/UiActionLatencyHelperTest.java
new file mode 100644
index 0000000..04f43c7
--- /dev/null
+++ b/libraries/collectors-helper/statsd/test/src/com/android/helpers/UiActionLatencyHelperTest.java
@@ -0,0 +1,114 @@
+/*
+ * 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;
+
+import static org.junit.Assert.assertTrue;
+
+import android.os.SystemClock;
+import android.util.Log;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.launcher3.tapl.AllApps;
+import com.android.launcher3.tapl.LauncherInstrumentation;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Map;
+
+/**
+ * Android Unit tests for {@link UiActionLatencyHelper}.
+ *
+ * <p>To run: Disable SELinux: adb shell setenforce 0; if this fails with "permission denied", try
+ * "adb shell su 0 setenforce 0" atest
+ * CollectorsHelperTest:com.android.helpers.UiActionLatencyHelperTest
+ */
+@RunWith(AndroidJUnit4.class)
+public class UiActionLatencyHelperTest {
+    private static final String LOG_TAG = UiActionLatencyHelperTest.class.getSimpleName();
+
+    // Keycode for pressing the home button.
+    private static final String KEYCODE_HOME = "KEYCODE_HOME";
+
+    private UiActionLatencyHelper mActionLatencyHelper;
+
+    @Before
+    public void setUp() {
+        mActionLatencyHelper = new UiActionLatencyHelper();
+        HelperTestUtility.wakeUpAndUnlock();
+    }
+
+    /** Test successful latency helper config. */
+    @Test
+    public void testConfig() throws Exception {
+        assertTrue(mActionLatencyHelper.startCollecting());
+        assertTrue(mActionLatencyHelper.stopCollecting());
+    }
+
+    /** Test no error is thrown if there is no CUJ. */
+    @Test
+    public void testEmptyMetric() throws Exception {
+        assertTrue(mActionLatencyHelper.startCollecting());
+        assertTrue(mActionLatencyHelper.getMetrics().isEmpty());
+        assertTrue(mActionLatencyHelper.stopCollecting());
+    }
+
+    /** Test that shade quick switch metric is collected. Enable after b/173623876 is fixed */
+    @Test
+    public void testQuickSwitchMetric() throws Exception {
+        final LauncherInstrumentation sLauncher = new LauncherInstrumentation();
+
+        // Lock the orientation.
+        HelperTestUtility.setOrientationNatural();
+
+        startApp(sLauncher, "Chrome", "com.android.chrome");
+        startApp(sLauncher, "Calculator", "com.google.android.calculator");
+
+        assertTrue(mActionLatencyHelper.startCollecting());
+        Log.d(LOG_TAG, "testQuickSwitchMetric: started collecting");
+
+        sLauncher.getBackground().quickSwitchToPreviousApp();
+
+        // Checking metrics produced by the CUJ.
+        final Map<String, StringBuilder> latencyMetrics = mActionLatencyHelper.getMetrics();
+        Log.d(
+                LOG_TAG,
+                "testQuickSwitchMetric: got metrics: " + String.join(",", latencyMetrics.keySet()));
+        assertTrue(
+                "No metric latency_ACTION_TOGGLE_RECENTS",
+                latencyMetrics.containsKey("latency_ACTION_TOGGLE_RECENTS"));
+
+        assertTrue(mActionLatencyHelper.stopCollecting());
+        HelperTestUtility.sendKeyCode(KEYCODE_HOME);
+        SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
+
+        // Unlock the orientation
+        HelperTestUtility.unfreezeRotation();
+
+    }
+
+    private void startApp(LauncherInstrumentation sLauncher, String appName, String appPackage) {
+        final AllApps allApps = sLauncher.pressHome().switchToAllApps();
+        allApps.freeze();
+        try {
+            allApps.getAppIcon(appName).launch(appPackage);
+        } finally {
+            allApps.unfreeze();
+        }
+    }
+}
diff --git a/libraries/collectors-helper/statsd/test/src/com/android/helpers/UiInteractionFrameInfoHelperTest.java b/libraries/collectors-helper/statsd/test/src/com/android/helpers/UiInteractionFrameInfoHelperTest.java
new file mode 100644
index 0000000..e6b0f64
--- /dev/null
+++ b/libraries/collectors-helper/statsd/test/src/com/android/helpers/UiInteractionFrameInfoHelperTest.java
@@ -0,0 +1,108 @@
+/*
+ * 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;
+
+import static org.junit.Assert.assertTrue;
+
+import android.app.Application;
+import android.os.SystemClock;
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.INotificationHelper;
+import android.support.test.uiautomator.UiObject2;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Map;
+
+/**
+ * Android Unit tests for {@link UiInteractionFrameInfoHelper}.
+ *
+ * <p>To run: Disable SELinux: adb shell setenforce 0; if this fails with "permission denied", try
+ * "adb shell su 0 setenforce 0" atest
+ * CollectorsHelperTest:com.android.helpers.UiInteractionFrameInfoHelperTest
+ */
+@RunWith(AndroidJUnit4.class)
+public class UiInteractionFrameInfoHelperTest {
+
+    // Keycode for pressing the home button.
+    private static final String KEYCODE_HOME = "KEYCODE_HOME";
+
+    private UiInteractionFrameInfoHelper mInteractionFrameHelper;
+
+    @Before
+    public void setUp() {
+        mInteractionFrameHelper = new UiInteractionFrameInfoHelper();
+        HelperTestUtility.wakeUpAndUnlock();
+    }
+
+    /** Test successful frame helper config. */
+    @Test
+    public void testConfig() throws Exception {
+        assertTrue(mInteractionFrameHelper.startCollecting());
+        assertTrue(mInteractionFrameHelper.stopCollecting());
+    }
+
+    /** Test no error is thrown if there is no CUJ. */
+    @Test
+    public void testEmptyMetric() throws Exception {
+        assertTrue(mInteractionFrameHelper.startCollecting());
+        assertTrue(mInteractionFrameHelper.getMetrics().isEmpty());
+        assertTrue(mInteractionFrameHelper.stopCollecting());
+    }
+
+    /** Test that shade fling metric is collected.. */
+    @Test
+    public void testShadeFlingMetric() throws Exception {
+        assertTrue(mInteractionFrameHelper.startCollecting());
+        final HelperAccessor<INotificationHelper> notificationHelper =
+                new HelperAccessor<>(INotificationHelper.class);
+        notificationHelper.get().setAppName(Application.getProcessName());
+
+        // The CUJ
+        notificationHelper.get().open();
+        UiObject2 notification = notificationHelper.get().postBigTextNotification(null /* pkg */);
+        notificationHelper.get().showGuts(notification);
+        notificationHelper.get().hideGuts(notification);
+        notificationHelper.get().cancelNotifications();
+        notificationHelper.get().exit();
+
+        // Checking metrics produced by the CUJ.
+        final Map<String, StringBuilder> frameMetrics = mInteractionFrameHelper.getMetrics();
+        assertTrue(
+                "No metric missed_frames_cuj_SHADE_SCROLL_FLING missing",
+                frameMetrics.containsKey("cuj_SHADE_SCROLL_FLING_missed_frames"));
+        assertTrue(
+                "No metric total_frames_cuj_SHADE_SCROLL_FLING",
+                frameMetrics.containsKey("cuj_SHADE_SCROLL_FLING_total_frames"));
+        assertTrue(
+                "No metric max_frame_time_nanos_cuj_SHADE_SCROLL_FLING",
+                frameMetrics.containsKey("cuj_SHADE_SCROLL_FLING_max_frame_time_ms"));
+
+        assertTrue(mInteractionFrameHelper.stopCollecting());
+        HelperTestUtility.sendKeyCode(KEYCODE_HOME);
+        SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
+    }
+
+    @After
+    public void tearDown() {
+        mInteractionFrameHelper.stopCollecting();
+    }
+}
diff --git a/libraries/collectors-helper/system/src/com/android/helpers/DumpsysServiceHelper.java b/libraries/collectors-helper/system/src/com/android/helpers/DumpsysServiceHelper.java
new file mode 100644
index 0000000..535ca17
--- /dev/null
+++ b/libraries/collectors-helper/system/src/com/android/helpers/DumpsysServiceHelper.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2021 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 android.util.Log;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.uiautomator.UiDevice;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * This is a collector helper that collects the dumpsys output for specified services and puts them
+ * into files.
+ */
+public class DumpsysServiceHelper implements ICollectorHelper<String> {
+
+    private static final String TAG = DumpsysServiceHelper.class.getSimpleName();
+
+    private static final String DUMPSYS_CMD = "dumpsys %s";
+
+    private static final String DUMPSYS_OUTPUT_DIRECTORY = "/sdcard/test_results";
+
+    private static final String DUMPSYS_OUTPUT_FILENAME = "dumpsys_service_%s.txt";
+
+    private static final String DUMPSYS_SERVICE_KEY = "dumpsys_service_%s";
+
+    private String[] mServiceNames = {};
+    private UiDevice mUiDevice;
+
+    public void setUp(String... serviceNames) {
+        if (serviceNames == null) {
+            return;
+        }
+        mServiceNames = serviceNames;
+        File outputDirectory = new File(DUMPSYS_OUTPUT_DIRECTORY);
+        boolean success = outputDirectory.mkdirs();
+        if (!success) {
+            Log.w(TAG, "Failed to create output directory.");
+        }
+    }
+
+    @Override
+    public boolean startCollecting() {
+        mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        return true;
+    }
+
+    @Override
+    public Map<String, String> getMetrics() {
+        Map<String, String> metrics = new HashMap<>();
+        for (String serviceName : mServiceNames) {
+            String outputPath = runDumpsysCmd(serviceName);
+            metrics.put(String.format(DUMPSYS_SERVICE_KEY, serviceName), outputPath);
+        }
+        return metrics;
+    }
+
+    @Override
+    public boolean stopCollecting() {
+        return true;
+    }
+
+    private String runDumpsysCmd(String serviceName) {
+        String outputPath =
+                Paths.get(
+                                DUMPSYS_OUTPUT_DIRECTORY,
+                                String.format(DUMPSYS_OUTPUT_FILENAME, serviceName))
+                        .toString();
+        final String cmd = String.format(DUMPSYS_CMD, serviceName);
+        try {
+            String res = mUiDevice.executeShellCommand(cmd);
+            FileOutputStream outputStream = new FileOutputStream(outputPath);
+            outputStream.write(res.getBytes());
+            outputStream.close();
+        } catch (IOException e) {
+            Log.e(TAG, "Failed to run dumpsys command.");
+        }
+        return outputPath;
+    }
+}
diff --git a/libraries/collectors-helper/system/test/src/com/android/helpers/tests/DumpsysServiceHelperTest.java b/libraries/collectors-helper/system/test/src/com/android/helpers/tests/DumpsysServiceHelperTest.java
new file mode 100644
index 0000000..360f5f9
--- /dev/null
+++ b/libraries/collectors-helper/system/test/src/com/android/helpers/tests/DumpsysServiceHelperTest.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2021 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 org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.helpers.DumpsysServiceHelper;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.nio.file.Paths;
+import java.util.Map;
+
+/**
+ * Android unit test for {@link DumpsysServiceHelper}
+ *
+ * <p>To run: atest CollectorsHelperTest:com.android.helpers.tests.DumpsysServiceHelperTest
+ */
+@RunWith(AndroidJUnit4.class)
+public class DumpsysServiceHelperTest {
+
+    private static final String DUMPSYS_OUTPUT_DIRECTORY = "/sdcard/test_results";
+
+    private static final String DUMPSYS_OUTPUT_FILENAME = "dumpsys_service_%s.txt";
+
+    private static final String DUMPSYS_SERVICE_KEY = "dumpsys_service_%s";
+
+    // Service name used for testing
+    private static final String TEST_SERVICE_NAME = "media.camera";
+
+    private static final String TEST_SERVICE_NAME_2 = "meminfo";
+
+    private DumpsysServiceHelper mDumpsysServiceHelper;
+
+    @Before
+    public void setUp() {
+        mDumpsysServiceHelper = new DumpsysServiceHelper();
+    }
+
+    @After
+    public void tearDown() {
+        new File(DUMPSYS_OUTPUT_DIRECTORY).delete();
+    }
+
+    @Test
+    public void testCollectDumpsys_noService() {
+        mDumpsysServiceHelper.startCollecting();
+        mDumpsysServiceHelper.stopCollecting();
+        Map<String, String> results = mDumpsysServiceHelper.getMetrics();
+
+        assertTrue(results.isEmpty());
+    }
+
+    @Test
+    public void testCollectDumpsys_oneService() {
+        mDumpsysServiceHelper.setUp(TEST_SERVICE_NAME);
+
+        mDumpsysServiceHelper.startCollecting();
+        mDumpsysServiceHelper.stopCollecting();
+        Map<String, String> results = mDumpsysServiceHelper.getMetrics();
+
+        assertFalse(results.isEmpty());
+        String key = String.format(DUMPSYS_SERVICE_KEY, TEST_SERVICE_NAME);
+        String path =
+                Paths.get(
+                                DUMPSYS_OUTPUT_DIRECTORY,
+                                String.format(DUMPSYS_OUTPUT_FILENAME, TEST_SERVICE_NAME))
+                        .toString();
+        assertEquals(results.get(key), path);
+    }
+
+    @Test
+    public void testCollectDumpsys_multipleServices() {
+        mDumpsysServiceHelper.setUp(TEST_SERVICE_NAME, TEST_SERVICE_NAME_2);
+
+        mDumpsysServiceHelper.startCollecting();
+        mDumpsysServiceHelper.stopCollecting();
+        Map<String, String> results = mDumpsysServiceHelper.getMetrics();
+
+        String key1 = String.format(DUMPSYS_SERVICE_KEY, TEST_SERVICE_NAME);
+        String path1 =
+                Paths.get(
+                                DUMPSYS_OUTPUT_DIRECTORY,
+                                String.format(DUMPSYS_OUTPUT_FILENAME, TEST_SERVICE_NAME))
+                        .toString();
+        assertEquals(results.get(key1), path1);
+        String key2 = String.format(DUMPSYS_SERVICE_KEY, TEST_SERVICE_NAME_2);
+        String path2 =
+                Paths.get(
+                                DUMPSYS_OUTPUT_DIRECTORY,
+                                String.format(DUMPSYS_OUTPUT_FILENAME, TEST_SERVICE_NAME_2))
+                        .toString();
+        assertEquals(results.get(key2), path2);
+    }
+}
diff --git a/libraries/collectors-helper/utilities/src/com/android/helpers/MetricUtility.java b/libraries/collectors-helper/utilities/src/com/android/helpers/MetricUtility.java
index 23462cb..5e7e8e5 100644
--- a/libraries/collectors-helper/utilities/src/com/android/helpers/MetricUtility.java
+++ b/libraries/collectors-helper/utilities/src/com/android/helpers/MetricUtility.java
@@ -7,6 +7,7 @@
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.text.DecimalFormat;
 import java.util.Map;
 
 /**
@@ -17,9 +18,10 @@
 
     private static final String TAG = MetricUtility.class.getSimpleName();
     private static final String KEY_JOIN = "_";
-    private static final String METRIC_SEPARATOR = ",";
+    public static final String METRIC_SEPARATOR = ",";
 
     public static final int BUFFER_SIZE = 1024;
+    private static final DecimalFormat DOUBLE_FORMAT = new DecimalFormat("#0.000001");
 
     /**
      * Append the given array of string to construct the final key used to track the metrics.
@@ -44,6 +46,22 @@
     }
 
     /**
+     * Add metric to the result map. If metric key already exist append the new metric.
+     *
+     * @param metricKey Unique key to track the metric.
+     * @param metric metric to track.
+     * @param resultMap map of all the metrics.
+     */
+    public static void addMetric(
+            String metricKey, double metric, Map<String, StringBuilder> resultMap) {
+        resultMap.compute(
+                metricKey,
+                (key, value) ->
+                        (value == null ? new StringBuilder() : value.append(METRIC_SEPARATOR))
+                                .append(DOUBLE_FORMAT.format(metric)));
+    }
+
+    /**
      * Add metric to the result map. If metric key already exist increment the value by 1.
      *
      * @param metricKey Unique key to track the metric.
diff --git a/libraries/compatibility-common-util/src/com/android/compatibility/common/util/GasTest.java b/libraries/compatibility-common-util/src/com/android/compatibility/common/util/GasTest.java
index f98ac26..8330401 100644
--- a/libraries/compatibility-common-util/src/com/android/compatibility/common/util/GasTest.java
+++ b/libraries/compatibility-common-util/src/com/android/compatibility/common/util/GasTest.java
@@ -30,5 +30,23 @@
 @Retention(RetentionPolicy.RUNTIME)
 @Target({ElementType.METHOD, ElementType.TYPE})
 public @interface GasTest {
+    // The GAS requirement ID the GasTest applies to.
+    // Example: @GasTest(requirement = "G-0-000")
     String requirement();
+
+    // The minimum GAS software requirement version the GasTest applies to.
+    // Example: @GasTest(requirement = "G-0-000", minSoftwareVersion = 0.1)
+    double minSoftwareVersion() default 0;
+
+    // The maximum GAS software requirement version the GasTest applies to.
+    // Example: @GasTest(requirement = "G-0-000", maxSoftwareVersion = 0.1)
+    double maxSoftwareVersion() default 0;
+
+    // The minimum GAS hardware requirement version the GasTest applies to.
+    // Example: @GasTest(requirement = "G-0-000", minHardwareVersion = 0.1)
+    double minHardwareVersion() default 0;
+
+    // The maximum GAS hardware requirement version the GasTest applies to.
+    // Example: @GasTest(requirement = "G-0-000", maxHardwareVersion = 0.1)
+    double maxHardwareVersion() default 0;
 }
diff --git a/libraries/compatibility-common-util/src/com/android/compatibility/common/util/VersionCodes.java b/libraries/compatibility-common-util/src/com/android/compatibility/common/util/VersionCodes.java
index 224b6c8..8760d40 100644
--- a/libraries/compatibility-common-util/src/com/android/compatibility/common/util/VersionCodes.java
+++ b/libraries/compatibility-common-util/src/com/android/compatibility/common/util/VersionCodes.java
@@ -48,5 +48,6 @@
     public static final int P = 28;
     public static final int Q = 29;
     public static final int R = 30;
-    public static final int S = CUR_DEVELOPMENT;
+    public static final int S = 31;
+
 }
diff --git a/libraries/device-collectors/src/main/Android.bp b/libraries/device-collectors/src/main/Android.bp
index ed0d29a..f9bee71 100644
--- a/libraries/device-collectors/src/main/Android.bp
+++ b/libraries/device-collectors/src/main/Android.bp
@@ -23,16 +23,18 @@
     srcs: ["java/**/*.java"],
 
     static_libs: [
+        "androidx.annotation_annotation",
         "androidx.test.runner",
+        "androidx.test.uiautomator",
         "jank-helper",
         "junit",
+        "lyric-metric-helper",
         "memory-helper",
         "perfetto-helper",
         "power-helper",
         "simpleperf-helper",
-        "ub-uiautomator",
         "system-metric-helper",
     ],
 
-    sdk_version: "current",
+    sdk_version: "test_current",
 }
diff --git a/libraries/device-collectors/src/main/java/android/device/collectors/BaseCollectionListener.java b/libraries/device-collectors/src/main/java/android/device/collectors/BaseCollectionListener.java
index 8003aed..709e87b 100644
--- a/libraries/device-collectors/src/main/java/android/device/collectors/BaseCollectionListener.java
+++ b/libraries/device-collectors/src/main/java/android/device/collectors/BaseCollectionListener.java
@@ -23,8 +23,8 @@
 import com.android.helpers.ICollectorHelper;
 
 import org.junit.runner.Description;
-import org.junit.runner.notification.Failure;
 import org.junit.runner.Result;
+import org.junit.runner.notification.Failure;
 
 import java.util.Map;
 
@@ -79,7 +79,7 @@
     @Override
     public final void onTestStart(DataRecord testData, Description description) {
         mIsTestFailed = false;
-        if (!mIsCollectPerRun) {
+        if (!mIsCollectPerRun && !onTestStartAlternative(testData)) {
             mHelper.startCollecting();
         }
     }
@@ -91,18 +91,15 @@
 
     @Override
     public final void onTestEnd(DataRecord testData, Description description) {
-        if (!mIsCollectPerRun) {
+        if (!mIsCollectPerRun && !onTestEndAlternative(testData)) {
             // Skip adding the metrics collected during the test failure
             // if the skip metrics on test failure flag is enabled and the
             // current test is failed.
-            if (mSkipTestFailureMetrics && mIsTestFailed) {
+            if (shouldSkipFailureTestMetrics()) {
                 Log.i(getTag(), "Skipping the metric collection.");
             } else {
                 // Collect the metrics.
-                Map<String, T> metrics = mHelper.getMetrics();
-                for (Map.Entry<String, T> entry : metrics.entrySet()) {
-                    testData.addStringMetric(entry.getKey(), entry.getValue().toString());
-                }
+                collectMetrics(testData);
             }
             mHelper.stopCollecting();
         }
@@ -111,10 +108,7 @@
     @Override
     public void onTestRunEnd(DataRecord runData, Result result) {
         if (mIsCollectPerRun) {
-            Map<String, T> metrics = mHelper.getMetrics();
-            for (Map.Entry<String, T> entry : metrics.entrySet()) {
-                runData.addStringMetric(entry.getKey(), entry.getValue().toString());
-            }
+            collectMetrics(runData);
             mHelper.stopCollecting();
         }
     }
@@ -130,4 +124,37 @@
     protected void createHelperInstance(ICollectorHelper helper) {
         mHelper = helper;
     }
+
+    protected void collectMetrics(DataRecord data) {
+        Map<String, T> metrics = mHelper.getMetrics();
+        for (Map.Entry<String, T> entry : metrics.entrySet()) {
+            data.addStringMetric(entry.getKey(), entry.getValue().toString());
+        }
+    }
+
+    protected boolean shouldSkipFailureTestMetrics() {
+        return mSkipTestFailureMetrics && mIsTestFailed;
+    }
+
+    /**
+     * Offer an alternative of final method onTestStart, override this method when you need to do
+     * your own actions while collecting metrics per test.
+     *
+     * @param data Used to collect metrics
+     * @return true indicates the event has been handled then skips the default implementation.
+     */
+    protected boolean onTestStartAlternative(DataRecord data) {
+        return false;
+    }
+
+    /**
+     * Offer an alternative of final method onTestEnd, override this method when you need to do your
+     * own actions while collecting metrics per test.
+     *
+     * @param data Used to collect metrics
+     * @return true indicates the event has been handled then skips the default implementation.
+     */
+    protected boolean onTestEndAlternative(DataRecord data) {
+        return false;
+    }
 }
diff --git a/libraries/device-collectors/src/main/java/android/device/collectors/DataRecord.java b/libraries/device-collectors/src/main/java/android/device/collectors/DataRecord.java
index 32f1347..1d9b2d7 100644
--- a/libraries/device-collectors/src/main/java/android/device/collectors/DataRecord.java
+++ b/libraries/device-collectors/src/main/java/android/device/collectors/DataRecord.java
@@ -106,4 +106,10 @@
     Bundle createBundle() {
         return new Bundle();
     }
+
+    final void clear() {
+        mCurrentStringMetrics.clear();
+        mCurrentFileMetrics.clear();
+        mCurrentBinaryMetrics.clear();
+    }
 }
diff --git a/libraries/device-collectors/src/main/java/android/device/collectors/DumpsysMeminfoListener.java b/libraries/device-collectors/src/main/java/android/device/collectors/DumpsysMeminfoListener.java
index fae1619..b6656b9 100644
--- a/libraries/device-collectors/src/main/java/android/device/collectors/DumpsysMeminfoListener.java
+++ b/libraries/device-collectors/src/main/java/android/device/collectors/DumpsysMeminfoListener.java
@@ -19,11 +19,10 @@
 import android.device.collectors.annotations.OptionClass;
 import android.os.Bundle;
 import android.util.Log;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.helpers.DumpsysMeminfoHelper;
 
-import com.google.common.annotations.VisibleForTesting;
-
 @OptionClass(alias = "dumpsys-meminfo-listener")
 public class DumpsysMeminfoListener extends BaseCollectionListener<Long> {
 
diff --git a/libraries/device-collectors/src/main/java/android/device/collectors/DumpsysServiceListener.java b/libraries/device-collectors/src/main/java/android/device/collectors/DumpsysServiceListener.java
new file mode 100644
index 0000000..7505930
--- /dev/null
+++ b/libraries/device-collectors/src/main/java/android/device/collectors/DumpsysServiceListener.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2021 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.DumpsysServiceHelper;
+
+@OptionClass(alias = "dumpsys-service-listener")
+public class DumpsysServiceListener extends BaseCollectionListener<String> {
+
+    private static final String TAG = DumpsysServiceListener.class.getSimpleName();
+    @VisibleForTesting static final String SERVICE_SEPARATOR = ",";
+    @VisibleForTesting static final String SERVICE_NAMES_KEY = "service-names";
+
+    public DumpsysServiceListener() {
+        createHelperInstance(new DumpsysServiceHelper());
+    }
+
+    @VisibleForTesting
+    public DumpsysServiceListener(Bundle args, DumpsysServiceHelper helper) {
+        super(args, helper);
+    }
+
+    @Override
+    public void setupAdditionalArgs() {
+        Bundle args = getArgsBundle();
+        String servicesString = args.getString(SERVICE_NAMES_KEY);
+        if (servicesString == null) {
+            Log.w(TAG, "No process name provided. Nothing will be collected");
+            return;
+        }
+        ((DumpsysServiceHelper) mHelper).setUp(servicesString.split(SERVICE_SEPARATOR));
+    }
+}
diff --git a/libraries/device-collectors/src/main/java/android/device/collectors/GcaEventLogCollector.java b/libraries/device-collectors/src/main/java/android/device/collectors/GcaEventLogCollector.java
index c75b660..9a196de 100644
--- a/libraries/device-collectors/src/main/java/android/device/collectors/GcaEventLogCollector.java
+++ b/libraries/device-collectors/src/main/java/android/device/collectors/GcaEventLogCollector.java
@@ -18,6 +18,7 @@
 
 import android.device.collectors.annotations.OptionClass;
 import android.os.Bundle;
+import android.os.SystemClock;
 import android.util.Log;
 
 import androidx.annotation.Nullable;
@@ -32,6 +33,7 @@
 import java.io.File;
 import java.nio.charset.StandardCharsets;
 import java.util.HashMap;
+import java.util.concurrent.TimeUnit;
 
 /**
  * A {@link GcaEventLogCollector} that captures Google Camera App (GCA) on-device event log protos
@@ -54,6 +56,7 @@
     static final String COLLECT_CAMERA_LOGS_PER_RUN = "collect_camera_logs_per_run";
 
     private static final String GOOGLE_CAMERA_APP_PACKAGE = "google_camera_app_package";
+    private static final long WAIT_AFTER_APP_FORCE_STOP_MILLIS = TimeUnit.SECONDS.toMillis(2);
 
     enum SeLinuxEnforceProperty {
         ENFORCING("1"),
@@ -196,6 +199,9 @@
 
     private void killGoogleCameraApp() {
         executeCommandBlocking("am force-stop " + mGcaPkg);
+        // Add delay after force stop GCA to avoid new instance start before the app fully die.
+        // (https://b.corp.google.com/issues/181777896#comment27)
+        SystemClock.sleep(WAIT_AFTER_APP_FORCE_STOP_MILLIS);
     }
 
     private SeLinuxEnforceProperty getSeLinuxEnforceProperty() {
diff --git a/libraries/device-collectors/src/main/java/android/device/collectors/LyricCpuUtilizationCollector.java b/libraries/device-collectors/src/main/java/android/device/collectors/LyricCpuUtilizationCollector.java
new file mode 100644
index 0000000..b9719a0
--- /dev/null
+++ b/libraries/device-collectors/src/main/java/android/device/collectors/LyricCpuUtilizationCollector.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2021 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 com.android.helpers.LyricCpuUtilizationHelper;
+
+@OptionClass(alias = "lyric-cpu-utilization-collector")
+public class LyricCpuUtilizationCollector extends BaseCollectionListener<Double> {
+
+    public LyricCpuUtilizationCollector() {
+        createHelperInstance(new LyricCpuUtilizationHelper());
+    }
+}
diff --git a/libraries/device-collectors/src/main/java/android/device/collectors/MobilityChangeCollector.java b/libraries/device-collectors/src/main/java/android/device/collectors/MobilityChangeCollector.java
new file mode 100644
index 0000000..3c3b6b7
--- /dev/null
+++ b/libraries/device-collectors/src/main/java/android/device/collectors/MobilityChangeCollector.java
@@ -0,0 +1,87 @@
+/*
+ * 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.util.Log;
+
+import org.junit.runner.Description;
+import org.junit.runner.Result;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.text.SimpleDateFormat;
+
+/**
+ * A {@link BaseMetricListener} that captures mobility changes to stationary state(3) in logcat.
+ *
+ * <p>This class needs external storage permission. See {@link BaseMetricListener} how to grant
+ * external storage permission, especially at install time.
+ */
+@OptionClass(alias = "mobility-change-collector")
+public class MobilityChangeCollector extends BaseMetricListener {
+    static final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat("MM-dd HH:mm:ss.SSS");
+
+    public static final String DEFAULT_DIR = "run_listeners/mobility_changes";
+
+    private File mDestDir;
+    private String mStartTime = null;
+
+    @Override
+    public void onTestRunStart(DataRecord runData, Description description) {
+        mDestDir = createAndEmptyDirectory(DEFAULT_DIR);
+        mStartTime = DATE_FORMATTER.format(System.currentTimeMillis());
+    }
+
+    /** On test run end, check for mobility state changes to 3 */
+    @Override
+    public void onTestRunEnd(DataRecord runData, Result result) {
+        try {
+            final String fileName = "mobility-changes.txt";
+            File mobilityLog = new File(mDestDir, fileName);
+            getMobilityChangesSince(mStartTime, mobilityLog);
+            runData.addFileMetric(
+                    String.format("%s_%s", getTag(), mobilityLog.getName()), mobilityLog);
+            String mobilityChanges =
+                    new String(
+                            executeCommandBlocking(
+                                    String.format("cat %s | wc -l", mobilityLog.getAbsolutePath())),
+                            StandardCharsets.UTF_8);
+            runData.addStringMetric("mobility-changes", mobilityChanges);
+        } catch (IOException | InterruptedException e) {
+            Log.e(getTag(), "Error trying to retrieve mobility logcat.", e);
+        }
+    }
+
+    /** @hide */
+    protected void getMobilityChangesSince(String startTime, File saveTo)
+            throws IOException, InterruptedException {
+        // ProcessBuilder is used here in favor of UiAutomation.executeShellCommand() because the
+        // logcat command requires the timestamp to be quoted which in Java requires
+        // Runtime.exec(String[]) or ProcessBuilder to work properly, and UiAutomation does not
+        // support this for now.
+        ProcessBuilder pb =
+                new ProcessBuilder(
+                        Arrays.asList(
+                                "logcat", "-t", startTime, "-d", "updateSconeInfo mobility:3"));
+        pb.redirectOutput(saveTo);
+        Process proc = pb.start();
+        // Make the process blocking to ensure consistent behavior.
+        proc.waitFor();
+    }
+}
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 8658c5f..08539e8 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
@@ -76,6 +76,10 @@
     public static final String PERFETTO_START_WAIT_TIME_ARG = "perfetto_start_wait_time_ms";
     // Default wait time before starting the perfetto trace.
     public static final String DEFAULT_START_WAIT_TIME_MSECS = "3000";
+    // Regular expression pattern to identify multiple spaces.
+    public static final String SPACES_PATTERN = "\\s+";
+    // Space replacement value
+    public static final String REPLACEMENT_CHAR = "#";
 
     private final WakeLockContext mWakeLockContext;
     private final Supplier<WakeLock> mWakelockSupplier;
@@ -401,10 +405,12 @@
     }
 
     /**
-     * Returns the packagename.classname_methodname which has no special characters and used to
-     * create file names.
+     * Returns the packagename.classname_methodname which has no spaces and used to create file
+     * names.
      */
     public static String getTestFileName(Description description) {
-        return String.format("%s_%s", description.getClassName(), description.getMethodName());
+        return String.format("%s_%s",
+                description.getClassName().replaceAll(SPACES_PATTERN, REPLACEMENT_CHAR).trim(),
+                description.getMethodName().replaceAll(SPACES_PATTERN, REPLACEMENT_CHAR).trim());
     }
 }
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
index a1703ac..895ddd1 100644
--- a/libraries/device-collectors/src/main/java/android/device/collectors/ScreenRecordCollector.java
+++ b/libraries/device-collectors/src/main/java/android/device/collectors/ScreenRecordCollector.java
@@ -19,9 +19,9 @@
 
 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 androidx.test.uiautomator.UiDevice;
 
 import java.io.IOException;
 import java.io.File;
diff --git a/libraries/device-collectors/src/main/java/android/device/collectors/ScreenshotOnFailureCollector.java b/libraries/device-collectors/src/main/java/android/device/collectors/ScreenshotOnFailureCollector.java
index b53a9ea..0d00396 100644
--- a/libraries/device-collectors/src/main/java/android/device/collectors/ScreenshotOnFailureCollector.java
+++ b/libraries/device-collectors/src/main/java/android/device/collectors/ScreenshotOnFailureCollector.java
@@ -18,9 +18,9 @@
 import android.device.collectors.annotations.OptionClass;
 import android.graphics.Bitmap;
 import android.os.Bundle;
-import androidx.annotation.VisibleForTesting;
-import android.support.test.uiautomator.UiDevice;
 import android.util.Log;
+import androidx.annotation.VisibleForTesting;
+import androidx.test.uiautomator.UiDevice;
 
 import org.junit.runner.Description;
 import org.junit.runner.notification.Failure;
diff --git a/libraries/device-collectors/src/main/java/android/device/collectors/SimpleperfListener.java b/libraries/device-collectors/src/main/java/android/device/collectors/SimpleperfListener.java
index 61fc87a..a4ca773 100644
--- a/libraries/device-collectors/src/main/java/android/device/collectors/SimpleperfListener.java
+++ b/libraries/device-collectors/src/main/java/android/device/collectors/SimpleperfListener.java
@@ -41,6 +41,10 @@
 
     // Default output folder to store the simpleperf sample files.
     private static final String DEFAULT_OUTPUT_ROOT = "/sdcard/test_results";
+    // Default subcommand passed to simpleperf
+    private static final String DEFAULT_SUBCOMMAND = "record";
+    // Default arguments passed to simpleperf command
+    private static final String DEFAULT_ARGUMENTS = "-g --post-unwind=yes -f 500 -a --exclude-perf";
     // Destination directory to save the trace results.
     private static final String TEST_OUTPUT_ROOT = "test_output_root";
     // Simpleperf file path key.
@@ -50,6 +54,10 @@
     public static final String SIMPLEPERF_PREFIX = "simpleperf_";
     // Skip failure metrics collection if set to true.
     public static final String SKIP_TEST_FAILURE_METRICS = "skip_test_failure_metrics";
+    // Subcommand to pass to simpleperf on start.
+    public static final String SUBCOMMAND = "subcommand";
+    // Arguments to pass to simpleperf on start.
+    public static final String ARGUMENTS = "arguments";
 
     // Simpleperf samples collected during the test will be saved under this root folder.
     private String mTestOutputRoot;
@@ -59,6 +67,8 @@
     private boolean mIsCollectPerRun;
     private boolean mIsTestFailed = false;
     private boolean mSkipTestFailureMetrics;
+    private String mSubcommand;
+    private String mArguments;
 
     private SimpleperfHelper mSimpleperfHelper = new SimpleperfHelper();
 
@@ -91,12 +101,18 @@
         // By default this flag is set to false to collect metrics on test failure.
         mSkipTestFailureMetrics = "true".equals(args.getString(SKIP_TEST_FAILURE_METRICS));
 
+        // Subcommand passed to simpleperf. Defaults to record.
+        mSubcommand = args.getString(SUBCOMMAND, DEFAULT_SUBCOMMAND);
+
+        // Command arguments passed to simpleperf.
+        mArguments = args.getString(ARGUMENTS, DEFAULT_ARGUMENTS);
+
         if (!mIsCollectPerRun) {
             return;
         }
 
         Log.i(getTag(), "Starting simpleperf before test run started.");
-        startSimpleperf();
+        startSimpleperf(mSubcommand, mArguments);
     }
 
     @Override
@@ -109,7 +125,7 @@
         mTestIdInvocationCount.compute(
                 getTestFileName(description), (key, value) -> (value == null) ? 1 : value + 1);
         Log.i(getTag(), "Starting simpleperf before test started.");
-        startSimpleperf();
+        startSimpleperf(mSubcommand, mArguments);
     }
 
     @Override
@@ -181,8 +197,8 @@
     }
 
     /** Start simpleperf sampling. */
-    public void startSimpleperf() {
-        mSimpleperfStartSuccess = mSimpleperfHelper.startCollecting();
+    public void startSimpleperf(String subcommand, String arguments) {
+        mSimpleperfStartSuccess = mSimpleperfHelper.startCollecting(subcommand, arguments);
         if (!mSimpleperfStartSuccess) {
             Log.e(getTag(), "Simpleperf did not start successfully.");
         }
diff --git a/libraries/device-collectors/src/main/java/android/device/preparers/MemhogPreparer.java b/libraries/device-collectors/src/main/java/android/device/preparers/MemhogPreparer.java
new file mode 100644
index 0000000..7cf82bc
--- /dev/null
+++ b/libraries/device-collectors/src/main/java/android/device/preparers/MemhogPreparer.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2021 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.preparers;
+
+import android.device.collectors.BaseMetricListener;
+import android.device.collectors.DataRecord;
+import android.device.collectors.annotations.OptionClass;
+import android.os.Bundle;
+import android.util.Log;
+import com.android.helpers.MemhogHelper;
+
+import org.junit.runner.Description;
+import org.junit.runner.Result;
+
+@OptionClass(alias = "memhog-preparer")
+public final class MemhogPreparer extends BaseMetricListener {
+    private static final String TAG = MemhogPreparer.class.getSimpleName();
+    private static final String MEMORY_SIZE_BYTES = "memory_size_bytes";
+    private static final String DEFAULT_MEMORY_SIZE_BYTES = "1337";
+
+    private boolean mMemhogStarted;
+
+    private final MemhogHelper mMemhogHelper = new MemhogHelper();
+
+    public MemhogPreparer() {
+        super();
+    }
+
+    @Override
+    public void onTestRunStart(DataRecord runData, Description description) {
+        Bundle args = getArgsBundle();
+
+        // Amount of memory (in bytes) to be allocated.
+        long mMemorySizeBytes =
+                Long.parseLong(args.getString(MEMORY_SIZE_BYTES, DEFAULT_MEMORY_SIZE_BYTES));
+        mMemhogStarted = mMemhogHelper.startMemhog(mMemorySizeBytes);
+        if (!mMemhogStarted) {
+            throw new IllegalStateException("Memhog failed to start.");
+        }
+    }
+
+    @Override
+    public void onTestRunEnd(DataRecord runData, Result result) {
+        if (!mMemhogStarted) {
+            Log.i(TAG, "Skipping memhog stop attempt because it never started.");
+            return;
+        }
+        if (!mMemhogHelper.stopMemhog()) {
+            Log.e(TAG, "Memhog failed to stop.");
+        }
+    }
+}
diff --git a/libraries/device-collectors/src/main/java/android/device/stressmodes/BackgroundTranscodingStressTestMode.java b/libraries/device-collectors/src/main/java/android/device/stressmodes/BackgroundTranscodingStressTestMode.java
new file mode 100644
index 0000000..5ea3283
--- /dev/null
+++ b/libraries/device-collectors/src/main/java/android/device/stressmodes/BackgroundTranscodingStressTestMode.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2021 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.stressmodes;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.media.ApplicationMediaCapabilities;
+import android.net.Uri;
+import android.util.Log;
+import android.media.MediaFormat;
+import android.media.MediaTranscodingManager;
+import android.media.MediaTranscodingManager.TranscodingRequest;
+import android.media.MediaTranscodingManager.TranscodingSession;
+import android.media.MediaTranscodingManager.VideoTranscodingRequest;
+import static org.junit.Assert.assertNotNull;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.internal.runner.listener.InstrumentationRunListener;
+
+import org.junit.runner.Description;
+
+import java.io.File;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+public class BackgroundTranscodingStressTestMode extends InstrumentationRunListener {
+    private static final String TAG = BackgroundTranscodingStressTestMode.class.getSimpleName();
+    private Context mContext;
+    private MediaTranscodingManager mMediaTranscodingManager = null;
+
+    // Default setting for transcoding to H.264.
+    private static final String MIME_TYPE = MediaFormat.MIMETYPE_VIDEO_AVC;
+    private static final int BIT_RATE = 20000000; // 20Mbps
+    private static final int WIDTH = 1920;
+    private static final int HEIGHT = 1080;
+
+    private TranscodingSession mTranscodingSession = null;
+
+    public BackgroundTranscodingStressTestMode() {
+        mContext = InstrumentationRegistry.getInstrumentation().getContext();
+        InstrumentationRegistry.getInstrumentation()
+                .getUiAutomation()
+                .adoptShellPermissionIdentity("android.permission.WRITE_MEDIA_STORAGE");
+        mMediaTranscodingManager = mContext.getSystemService(MediaTranscodingManager.class);
+        assertNotNull(mMediaTranscodingManager);
+    }
+
+    @Override
+    public final void testStarted(Description description) throws Exception {
+        Log.i(TAG, "BackgroundTranscodingStressTestMode stress started");
+        String path = "/data/local/tmp/testHevc.mp4";
+        final File file = new File(path);
+
+        Log.i(TAG, "Transcoding file " + file);
+
+        // Create a file Uri: file:///data/user/0/android.media.cts/cache/HevcTranscode.mp4
+        Uri destinationUri =
+                Uri.parse(
+                        ContentResolver.SCHEME_FILE
+                                + "://"
+                                + mContext.getCacheDir().getAbsolutePath()
+                                + "/HevcTranscode.mp4");
+
+        ApplicationMediaCapabilities clientCaps =
+                new ApplicationMediaCapabilities.Builder().build();
+
+        TranscodingRequest.VideoFormatResolver resolver =
+                new TranscodingRequest.VideoFormatResolver(
+                        clientCaps,
+                        MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_HEVC, 1920, 1080));
+        MediaFormat videoTrackFormat = resolver.resolveVideoFormat();
+        assertNotNull(videoTrackFormat);
+
+        int pid = android.os.Process.myPid();
+        int uid = android.os.Process.myUid();
+
+        VideoTranscodingRequest.Builder builder =
+                new VideoTranscodingRequest.Builder(
+                                Uri.fromFile(file), destinationUri, videoTrackFormat)
+                        .setClientPid(pid)
+                        .setClientUid(uid);
+        TranscodingRequest request = builder.build();
+        Executor listenerExecutor = Executors.newSingleThreadExecutor();
+
+        Log.d(TAG, "transcoding to format: " + videoTrackFormat);
+
+        mTranscodingSession =
+                mMediaTranscodingManager.enqueueRequest(
+                        request,
+                        listenerExecutor,
+                        transcodingSession -> {
+                            Log.d(
+                                    TAG,
+                                    "Transcoding completed with result: "
+                                            + transcodingSession.getResult());
+                        });
+        assertNotNull(mTranscodingSession);
+    }
+
+    @Override
+    public final void testFinished(Description description) throws Exception {
+        Log.i(TAG, "BackgroundTranscodingStressTestMode stress finished");
+        mTranscodingSession.cancel();
+    }
+}
diff --git a/libraries/device-collectors/src/main/java/android/device/stressmodes/IOContentionStressTestMode.java b/libraries/device-collectors/src/main/java/android/device/stressmodes/IOContentionStressTestMode.java
new file mode 100644
index 0000000..241d68b
--- /dev/null
+++ b/libraries/device-collectors/src/main/java/android/device/stressmodes/IOContentionStressTestMode.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2021 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.stressmodes;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.media.MediaCodec;
+import android.media.MediaExtractor;
+import android.media.MediaMuxer;
+import android.net.Uri;
+import android.util.Log;
+import android.media.MediaFormat;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.internal.runner.listener.InstrumentationRunListener;
+
+import org.junit.runner.Description;
+
+import java.io.File;
+import java.nio.ByteBuffer;
+import java.util.HashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * This stressor test read file from a mp4 file and write to a new file to simulate the IO
+ * contention.
+ */
+public class IOContentionStressTestMode extends InstrumentationRunListener {
+    private static final String TAG = IOContentionStressTestMode.class.getSimpleName();
+    private Context mContext;
+    private static final int MAX_SAMPLE_SIZE = 256 * 1024;
+    private ContentResolver mContentResolver;
+    private boolean mStop = false;
+    private ExecutorService mExecutorService = null;
+
+    public IOContentionStressTestMode() {
+        mContext = InstrumentationRegistry.getInstrumentation().getContext();
+        InstrumentationRegistry.getInstrumentation()
+                .getUiAutomation()
+                .adoptShellPermissionIdentity("android.permission.WRITE_MEDIA_STORAGE");
+        mContentResolver = mContext.getContentResolver();
+        mExecutorService = Executors.newSingleThreadExecutor();
+    }
+
+    public final void doCopyFile() throws Exception {
+        Log.i(TAG, "doCopyFile");
+        String path = "/data/local/tmp/testHevc.mp4";
+        final File file = new File(path);
+
+        Log.i(TAG, "Copying file " + file);
+
+        // Create a file Uri: file:///data/user/0/android.media.cts/cache/HevcTranscode.mp4
+        Uri destinationUri =
+                Uri.parse(
+                        ContentResolver.SCHEME_FILE
+                                + "://"
+                                + mContext.getCacheDir().getAbsolutePath()
+                                + "/HevcTranscode.mp4");
+
+        // Set up MediaExtractor to read from the source.
+        AssetFileDescriptor srcFd =
+                mContentResolver.openAssetFileDescriptor(Uri.fromFile(file), "r");
+        AssetFileDescriptor dstFd = null;
+        MediaExtractor extractor = null;
+        MediaMuxer muxer = null;
+
+        try {
+            extractor = new MediaExtractor();
+            extractor.setDataSource(
+                    srcFd.getFileDescriptor(), srcFd.getStartOffset(), srcFd.getLength());
+
+            int trackCount = extractor.getTrackCount();
+
+            // Set up MediaMuxer for the destination.
+            muxer =
+                    new MediaMuxer(
+                            destinationUri.getPath(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
+
+            // Set up the tracks.
+            HashMap<Integer, Integer> indexMap = new HashMap<Integer, Integer>(trackCount);
+            for (int i = 0; i < trackCount; i++) {
+                extractor.selectTrack(i);
+                MediaFormat format = extractor.getTrackFormat(i);
+                int dstIndex = muxer.addTrack(format);
+                indexMap.put(i, dstIndex);
+            }
+
+            // Copy the samples from MediaExtractor to MediaMuxer.
+            boolean sawEOS = false;
+            int bufferSize = MAX_SAMPLE_SIZE;
+            int frameCount = 0;
+            int offset = 100;
+
+            ByteBuffer dstBuf = ByteBuffer.allocate(bufferSize);
+            MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
+
+            muxer.start();
+            while (!sawEOS && !mStop) {
+                bufferInfo.offset = offset;
+                bufferInfo.size = extractor.readSampleData(dstBuf, offset);
+
+                if (bufferInfo.size < 0) {
+                    sawEOS = true;
+                    bufferInfo.size = 0;
+                } else {
+                    bufferInfo.presentationTimeUs = extractor.getSampleTime();
+                    bufferInfo.flags = extractor.getSampleFlags();
+                    int trackIndex = extractor.getSampleTrackIndex();
+
+                    muxer.writeSampleData(indexMap.get(trackIndex), dstBuf, bufferInfo);
+                    extractor.advance();
+
+                    frameCount++;
+                }
+            }
+        } finally {
+            if (muxer != null) {
+                muxer.stop();
+                muxer.release();
+            }
+            if (extractor != null) {
+                extractor.release();
+            }
+            if (srcFd != null) {
+                srcFd.close();
+            }
+        }
+    }
+
+    @Override
+    public final void testStarted(Description description) throws Exception {
+        mExecutorService = Executors.newSingleThreadExecutor();
+        mStop = false;
+        Log.i(TAG, "IOContentionStressTestMode stress started");
+        mExecutorService.submit(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        while (!mStop) {
+                            try {
+                                doCopyFile();
+                            } catch (Exception ex) {
+                                Log.e(TAG, "Copy file get exception: " + ex);
+                            }
+                        }
+                    }
+                });
+    }
+
+    @Override
+    public final void testFinished(Description description) throws Exception {
+        Log.i(TAG, "IOContentionStressTestMode stress finished");
+        mStop = true;
+        if (mExecutorService != null) {
+            mExecutorService.shutdown();
+        }
+    }
+}
diff --git a/libraries/device-collectors/src/main/java/android/device/stressmodes/LockContentionStressMode.java b/libraries/device-collectors/src/main/java/android/device/stressmodes/LockContentionStressMode.java
new file mode 100644
index 0000000..f3fac47
--- /dev/null
+++ b/libraries/device-collectors/src/main/java/android/device/stressmodes/LockContentionStressMode.java
@@ -0,0 +1,144 @@
+/*
+ * 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.stressmodes;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.WindowManager;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.internal.runner.listener.InstrumentationRunListener;
+
+import org.junit.runner.Description;
+
+import java.util.concurrent.BrokenBarrierException;
+import java.util.concurrent.CyclicBarrier;
+import java.util.function.BiConsumer;
+
+// TODO(b/169588447): Consider converting to @Rule
+public class LockContentionStressMode extends InstrumentationRunListener {
+    private static final String TAG = LockContentionStressMode.class.getSimpleName();
+    private int mPauseBetweenAttemptsMs;
+    private double mProbabilityOfHoldingLock;
+    private int mMaxHoldingLockTimeMs;
+    private final CyclicBarrier mEnterStress = new CyclicBarrier(4);
+    private final CyclicBarrier mLeaveStress = new CyclicBarrier(4);
+    private IBinder mHoldLockToken;
+
+    public LockContentionStressMode() {
+        final Context context = InstrumentationRegistry.getInstrumentation().getContext();
+
+        final PackageManager pm = context.getPackageManager();
+        createLockContentionThread("Contention-PM", pm::holdLock);
+
+        final WindowManager wm = context.getSystemService(WindowManager.class);
+        createLockContentionThread("Contention-WM", wm::holdLock);
+
+        final ActivityManager am = context.getSystemService(ActivityManager.class);
+        createLockContentionThread("Contention-AM", am::holdLock);
+    }
+
+    private void createLockContentionThread(
+            String threadName, BiConsumer<IBinder, Integer> holdLockMethod) {
+        new Thread(
+                        () -> {
+                            while (true) {
+                                lockContentionUntilToldToLeave(holdLockMethod);
+                            }
+                        },
+                        threadName)
+                .start();
+    }
+
+    // Creates a lock contention for a single invocation of the test.
+    private void lockContentionUntilToldToLeave(BiConsumer<IBinder, Integer> holdLockMethod) {
+        try {
+            // Sync with the main thread before entering stress mode.
+            mEnterStress.await();
+
+            // While the main thread doesn't indicate that it wants to leave the stress mode...
+            while (mLeaveStress.getNumberWaiting() == 0) {
+                SystemClock.sleep(mPauseBetweenAttemptsMs);
+                if (Math.random() < mProbabilityOfHoldingLock) {
+                    holdLockMethod.accept(
+                            mHoldLockToken, (int) (Math.random() * mMaxHoldingLockTimeMs));
+                }
+            }
+
+            // Sync with the main thread after leaving stress mode.
+            mLeaveStress.await();
+        } catch (InterruptedException | BrokenBarrierException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public void testRunStarted(Description description) throws Exception {
+        super.testRunStarted(description);
+        final Bundle args = InstrumentationRegistry.getArguments();
+        final String intensity = args.getString("lock-stress-intensity", "P50");
+        Log.e(TAG, "lock-stress-intensity: " + intensity);
+        switch (intensity) {
+            case "P50":
+                mPauseBetweenAttemptsMs = 5;
+                mProbabilityOfHoldingLock = 0.5;
+                mMaxHoldingLockTimeMs = 100;
+                break;
+            case "P95":
+                mPauseBetweenAttemptsMs = 0;
+                mProbabilityOfHoldingLock = 1;
+                mMaxHoldingLockTimeMs = 100;
+                break;
+            default:
+                throw new IllegalArgumentException(
+                        "lock-stress-intensity: incorrect value: " + intensity);
+        }
+    }
+
+    @Override
+    public final void testStarted(Description description) throws Exception {
+        Log.d(
+                TAG,
+                "LockContentionStressMode.testStarted: entering lock contention mode: "
+                        + "mPauseBetweenAttemptsMs="
+                        + mPauseBetweenAttemptsMs
+                        + ", mProbabilityOfHoldingLock="
+                        + mProbabilityOfHoldingLock
+                        + ", mMaxHoldingLockTimeMs="
+                        + mMaxHoldingLockTimeMs);
+
+        getInstrumentation().getUiAutomation().adoptShellPermissionIdentity();
+        mHoldLockToken = getInstrumentation().getContext().getPackageManager().getHoldLockToken();
+        getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
+
+        mEnterStress.await(); // Sync with stress threads before entering stress mode.
+        super.testStarted(description);
+    }
+
+    @Override
+    public final void testFinished(Description description) throws Exception {
+        mLeaveStress.await(); // Sync with stress threads before leaving stress mode.
+        mHoldLockToken = null;
+        super.testFinished(description);
+        Log.d(TAG, "LockContentionStressMode.testFinished: leaving lock contention mode");
+    }
+}
diff --git a/libraries/device-collectors/src/main/platform-collectors/Android.bp b/libraries/device-collectors/src/main/platform-collectors/Android.bp
index 1950ed7..25e82bc 100644
--- a/libraries/device-collectors/src/main/platform-collectors/Android.bp
+++ b/libraries/device-collectors/src/main/platform-collectors/Android.bp
@@ -49,10 +49,9 @@
 
     static_libs: [
         "collector-device-lib",
-        "libprotobuf-java-lite",
-        "statsd-config-protos",
+        "statsdprotonano",
         "statsd-helper",
-        "ub-uiautomator",
+        "statsd-config-protos",
     ],
     platform_apis: true,
 }
diff --git a/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/wifi-controller-energy/README.md b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/wifi-controller-energy/README.md
new file mode 100644
index 0000000..047f96c
--- /dev/null
+++ b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/wifi-controller-energy/README.md
@@ -0,0 +1,4 @@
+# WiFi Controller Energy Configs
+
+These configs are used to gauge the amount of energy consumed by the WiFi controller throughout a
+test or a test run, as defined by the WifiActivityInfo atom.
diff --git a/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/wifi-controller-energy/wifi-controller-energy-run-level.pb b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/wifi-controller-energy/wifi-controller-energy-run-level.pb
new file mode 100644
index 0000000..955bc62
--- /dev/null
+++ b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/wifi-controller-energy/wifi-controller-energy-run-level.pb
Binary files differ
diff --git a/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/wifi-controller-energy/wifi-controller-energy-test-level.pb b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/wifi-controller-energy/wifi-controller-energy-test-level.pb
new file mode 100644
index 0000000..d7300fc
--- /dev/null
+++ b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/wifi-controller-energy/wifi-controller-energy-test-level.pb
Binary files differ
diff --git a/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/wifi-health-stat-reported/README.md b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/wifi-health-stat-reported/README.md
new file mode 100644
index 0000000..6f2e62b
--- /dev/null
+++ b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/wifi-health-stat-reported/README.md
@@ -0,0 +1,4 @@
+# WiFi Health Stat Reported Config
+
+This config is used to log all changes in WiFi strength and sufficiency during a test, as defined by
+the WifiHealthStatReported atom.
diff --git a/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/wifi-health-stat-reported/wifi-health-stat-reported.pb b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/wifi-health-stat-reported/wifi-health-stat-reported.pb
new file mode 100644
index 0000000..7507a15
--- /dev/null
+++ b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/wifi-health-stat-reported/wifi-health-stat-reported.pb
Binary files differ
diff --git a/libraries/device-collectors/src/main/platform-collectors/src/android/device/collectors/CpuUsageListener.java b/libraries/device-collectors/src/main/platform-collectors/src/android/device/collectors/CpuUsageListener.java
index cefcb41..b046067 100644
--- a/libraries/device-collectors/src/main/platform-collectors/src/android/device/collectors/CpuUsageListener.java
+++ b/libraries/device-collectors/src/main/platform-collectors/src/android/device/collectors/CpuUsageListener.java
@@ -30,9 +30,7 @@
 public class CpuUsageListener extends BaseCollectionListener<Long> {
 
     private static final String DISABLE_PER_PACKAGE = "disable_per_pkg";
-    private static final String DISABLE_PER_FREQ = "disable_per_freq";
     private static final String DISABLE_TOTAL_PKG = "disable_total_pkg";
-    private static final String DISABLE_TOTAL_FREQ = "disable_total_freq";
     private static final String ENABLE_CPU_UTILIZATION = "enable_cpu_utilization";
 
     public CpuUsageListener() {
@@ -50,18 +48,10 @@
             cpuUsageHelper.setDisablePerPackage();
         }
 
-        if ("true".equals(args.getString(DISABLE_PER_FREQ))) {
-            cpuUsageHelper.setDisablePerFrequency();
-        }
-
         if ("true".equals(args.getString(DISABLE_TOTAL_PKG))) {
             cpuUsageHelper.setDisableTotalPackage();
         }
 
-        if ("true".equals(args.getString(DISABLE_TOTAL_FREQ))) {
-            cpuUsageHelper.setDisableTotalFrequency();
-        }
-
         if ("true".equals(args.getString(ENABLE_CPU_UTILIZATION))) {
             cpuUsageHelper.setEnableCpuUtilization();
         }
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 b59d876..060ccdc 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
@@ -24,17 +24,21 @@
 import android.os.SystemClock;
 import android.util.Log;
 import android.util.StatsLog;
+
 import androidx.annotation.VisibleForTesting;
 import androidx.test.InstrumentationRegistry;
 
-import com.android.internal.os.StatsdConfigProto.StatsdConfig;
-import com.android.os.AtomsProto.Atom;
-import com.android.os.StatsLog.ConfigMetricsReportList;
-import com.google.protobuf.InvalidProtocolBufferException;
+import com.android.internal.os.nano.StatsdConfigProto;
+import com.android.os.nano.AtomsProto;
+
+import com.google.protobuf.nano.CodedOutputByteBufferNano;
+import com.google.protobuf.nano.ExtendableMessageNano;
+import com.google.protobuf.nano.InvalidProtocolBufferNanoException;
 
 import org.junit.runner.Description;
 import org.junit.runner.Result;
 
+import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
@@ -83,8 +87,10 @@
     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>();
+    private Map<String, StatsdConfigProto.StatsdConfig> mRunLevelConfigs =
+            new HashMap<String, StatsdConfigProto.StatsdConfig>();
+    private Map<String, StatsdConfigProto.StatsdConfig> mTestLevelConfigs =
+            new HashMap<String, StatsdConfigProto.StatsdConfig>();
 
     // Map to associate config names with their config Ids.
     private Map<String, Long> mRunLevelConfigIds = new HashMap<String, Long>();
@@ -172,17 +178,18 @@
      * @return Map of (config name, config id)
      */
     private Map<String, Long> registerConfigsWithStatsManager(
-            final Map<String, StatsdConfig> configs) {
+            final Map<String, StatsdConfigProto.StatsdConfig> configs) {
         Map<String, Long> configIds = new HashMap<String, Long>();
         adoptShellPermissionIdentity();
         for (String configName : configs.keySet()) {
-            long configId = getUniqueIdForConfig(configs.get(configName));
-            StatsdConfig newConfig = configs.get(configName).toBuilder().setId(configId).build();
             try {
+                long configId = getUniqueIdForConfig(configs.get(configName));
+                StatsdConfigProto.StatsdConfig newConfig = clone(configs.get(configName));
+                newConfig.id = configId;
                 Log.i(LOG_TAG, String.format("Adding config %s with ID %d.", configName, configId));
-                addStatsConfig(configId, newConfig.toByteArray());
+                addStatsConfig(configId, serialize(newConfig));
                 configIds.put(configName, configId);
-            } catch (StatsUnavailableException e) {
+            } catch (IOException | StatsUnavailableException e) {
                 Log.e(
                         LOG_TAG,
                         String.format(
@@ -217,7 +224,7 @@
         adoptShellPermissionIdentity();
         for (String configName : configIds.keySet()) {
             // Dump the metric report to external storage.
-            ConfigMetricsReportList reportList;
+            com.android.os.nano.StatsLog.ConfigMetricsReportList reportList;
             try {
                 Log.i(
                         LOG_TAG,
@@ -225,20 +232,20 @@
                                 "Pulling metrics for config %s with ID %d.",
                                 configName, configIds.get(configName)));
                 reportList =
-                        ConfigMetricsReportList.parseFrom(
+                        com.android.os.nano.StatsLog.ConfigMetricsReportList.parseFrom(
                                 getStatsReports(configIds.get(configName)));
                 Log.i(
                         LOG_TAG,
                         String.format(
                                 "Found %d metric %s from config %s.",
-                                reportList.getReportsCount(),
-                                reportList.getReportsCount() == 1 ? "report" : "reports",
+                                reportList.reports.length,
+                                reportList.reports.length == 1 ? "report" : "reports",
                                 configName));
                 File reportFile =
                         new File(
                                 saveDirectory,
                                 REPORT_FILENAME_PREFIX + configName + suffix + PROTO_EXTENSION);
-                writeToFile(reportFile, reportList.toByteArray());
+                writeToFile(reportFile, serialize(reportList));
                 savedConfigFiles.put(configName, reportFile);
             } catch (StatsUnavailableException e) {
                 Log.e(
@@ -246,7 +253,7 @@
                         String.format(
                                 "Failed to retrieve metrics for config %s due to %s.",
                                 configName, e.toString()));
-            } catch (InvalidProtocolBufferException e) {
+            } catch (InvalidProtocolBufferNanoException e) {
                 Log.e(
                         LOG_TAG,
                         String.format(
@@ -392,7 +399,7 @@
      * @hide
      */
     @VisibleForTesting
-    protected long getUniqueIdForConfig(StatsdConfig config) {
+    protected long getUniqueIdForConfig(StatsdConfigProto.StatsdConfig config) {
         return (long) UUID.randomUUID().hashCode();
     }
 
@@ -415,11 +422,12 @@
      *
      * <p>The option name is passed in for better error messaging.
      */
-    private StatsdConfig parseConfigFromName(
+    private StatsdConfigProto.StatsdConfig parseConfigFromName(
             final AssetManager manager, String optionName, String configName) {
         try (InputStream configStream = openConfigWithAssetManager(manager, configName)) {
             try {
-                return fixPermissions(StatsdConfig.parseFrom(configStream));
+                byte[] serializedConfig = readInputStream(configStream);
+                return fixPermissions(StatsdConfigProto.StatsdConfig.parseFrom(serializedConfig));
             } catch (IOException e) {
                 throw new RuntimeException(
                         String.format(
@@ -439,7 +447,7 @@
      * @hide
      */
     @VisibleForTesting
-    protected Map<String, StatsdConfig> getConfigsFromOption(String optionName) {
+    protected Map<String, StatsdConfigProto.StatsdConfig> getConfigsFromOption(String optionName) {
         List<String> configNames =
                 Arrays.asList(getArguments().getString(optionName, "").split(","))
                         .stream()
@@ -483,13 +491,60 @@
      *
      * <p>This is related to some new permission restrictions in RVC.
      */
-    private StatsdConfig fixPermissions(StatsdConfig config) {
-        StatsdConfig.Builder builder = config.toBuilder();
-        // Allow system power stats to be pulled.
-        builder.addDefaultPullPackages("AID_SYSTEM");
-        // Gauge metrics rely on AppBreadcrumbReported as metric dump triggers.
-        builder.addWhitelistedAtomIds(Atom.APP_BREADCRUMB_REPORTED_FIELD_NUMBER);
+    private StatsdConfigProto.StatsdConfig fixPermissions(StatsdConfigProto.StatsdConfig config)
+            throws IOException {
+        StatsdConfigProto.StatsdConfig newConfig = clone(config);
+        newConfig.defaultPullPackages =
+                concat(config.defaultPullPackages, new String[] {"AID_SYSTEM"});
+        newConfig.whitelistedAtomIds =
+                concat(
+                        config.whitelistedAtomIds,
+                        new int[] {AtomsProto.Atom.APP_BREADCRUMB_REPORTED_FIELD_NUMBER});
+        return newConfig;
+    }
 
-        return builder.build();
+    // Some utilities for Nano protos.
+
+    private static <T extends ExtendableMessageNano<T>> byte[] serialize(
+            ExtendableMessageNano<T> message) throws IOException {
+        byte[] serialized = new byte[message.getSerializedSize()];
+        CodedOutputByteBufferNano buffer = CodedOutputByteBufferNano.newInstance(serialized);
+        message.writeTo(buffer);
+        return serialized;
+    }
+
+    private static StatsdConfigProto.StatsdConfig clone(StatsdConfigProto.StatsdConfig config)
+            throws IOException {
+        byte[] output = serialize(config);
+        return StatsdConfigProto.StatsdConfig.parseFrom(output);
+    }
+
+    private static byte[] readInputStream(InputStream in) throws IOException {
+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        try {
+            byte[] buffer = new byte[1024];
+            int size = in.read(buffer);
+            while (size > 0) {
+                outputStream.write(buffer, 0, size);
+                size = in.read(buffer);
+            }
+            return outputStream.toByteArray();
+        } finally {
+            outputStream.close();
+        }
+    }
+
+    // Array Concatenation
+
+    private static int[] concat(int[] source, int[] items) {
+        int[] concatenated = Arrays.copyOf(source, source.length + items.length);
+        System.arraycopy(items, 0, concatenated, source.length, items.length);
+        return concatenated;
+    }
+
+    private static <T> T[] concat(T[] source, T[] items) {
+        T[] concatenated = Arrays.copyOf(source, source.length + items.length);
+        System.arraycopy(items, 0, concatenated, source.length, items.length);
+        return concatenated;
     }
 }
diff --git a/libraries/device-collectors/src/main/platform-collectors/src/android/device/collectors/UiActionLatencyListener.java b/libraries/device-collectors/src/main/platform-collectors/src/android/device/collectors/UiActionLatencyListener.java
new file mode 100644
index 0000000..9128680
--- /dev/null
+++ b/libraries/device-collectors/src/main/platform-collectors/src/android/device/collectors/UiActionLatencyListener.java
@@ -0,0 +1,30 @@
+/*
+ * 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 com.android.helpers.UiActionLatencyHelper;
+
+/**
+ * A listener that captures latency for various system actions.
+ *
+ * <p>Do NOT throw exception anywhere in this class. We don't want to halt the test when metrics
+ * collection fails.
+ */
+public class UiActionLatencyListener extends BaseCollectionListener<StringBuilder> {
+    public UiActionLatencyListener() {
+        createHelperInstance(new UiActionLatencyHelper());
+    }
+}
diff --git a/libraries/device-collectors/src/main/platform-collectors/src/android/device/collectors/UiInteractionFrameInfoListener.java b/libraries/device-collectors/src/main/platform-collectors/src/android/device/collectors/UiInteractionFrameInfoListener.java
new file mode 100644
index 0000000..cad05f7
--- /dev/null
+++ b/libraries/device-collectors/src/main/platform-collectors/src/android/device/collectors/UiInteractionFrameInfoListener.java
@@ -0,0 +1,247 @@
+/*
+ * 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 static com.android.internal.jank.InteractionJankMonitor.PROP_NOTIFY_CUJ_EVENT;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+
+import com.android.helpers.MetricUtility;
+import com.android.helpers.UiInteractionFrameInfoHelper;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.jank.InteractionJankMonitor;
+
+import org.junit.runner.Description;
+import org.junit.runner.Result;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * A listener that captures jank for various system interactions.
+ *
+ * <p>Do NOT throw exception anywhere in this class. We don't want to halt the test when metrics
+ * collection fails.
+ */
+public class UiInteractionFrameInfoListener extends BaseCollectionListener<StringBuilder> {
+    private static final String TAG = UiInteractionFrameInfoListener.class.getSimpleName();
+    private static final long POLLING_INTERVAL_MS = 100;
+    private static final long POLLING_MAX_TIMES = 100;
+    private static final long DELAY_TOGGLE_PANEL_MS = 100;
+    private static final String CMD_ENABLE_NOTIFY =
+            String.format("setprop %s %d", PROP_NOTIFY_CUJ_EVENT, 1);
+    private static final String CMD_DISABLE_NOTIFY =
+            String.format("setprop %s %d", PROP_NOTIFY_CUJ_EVENT, 0);
+    private static final String CMD_TOGGLE_PANEL = "su shell service call statusbar 3";
+
+    private final MetricsLoggedReceiver mReceiver = new MetricsLoggedReceiver();
+    private final Object mLock = new Object();
+
+    @GuardedBy("mLock")
+    private final Set<String> mExpectedCujSet = new HashSet<>();
+
+    @GuardedBy("mLock")
+    private final Set<String> mLoggedCujSet = new HashSet<>();
+
+    @GuardedBy("mLock")
+    private TimestampRecord mCurrentTestTimestamp;
+
+    @GuardedBy("mLock")
+    private boolean mMetricsReady;
+
+    public UiInteractionFrameInfoListener() {
+        createHelperInstance(new UiInteractionFrameInfoHelper());
+    }
+
+    @Override
+    public void onTestRunStart(DataRecord runData, Description description) {
+        getInstrumentation().getUiAutomation().executeShellCommand(CMD_ENABLE_NOTIFY);
+        super.onTestRunStart(runData, description);
+    }
+
+    @Override
+    public void onTestRunEnd(DataRecord runData, Result result) {
+        super.onTestRunEnd(runData, result);
+        getInstrumentation().getUiAutomation().executeShellCommand(CMD_DISABLE_NOTIFY);
+    }
+
+    @Override
+    protected boolean onTestStartAlternative(DataRecord data) {
+        synchronized (mLock) {
+            mMetricsReady = false;
+            mCurrentTestTimestamp = new TimestampRecord();
+            mCurrentTestTimestamp.begin(System.nanoTime());
+        }
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(InteractionJankMonitor.ACTION_SESSION_BEGIN);
+        filter.addAction(InteractionJankMonitor.ACTION_METRICS_LOGGED);
+        filter.addAction(InteractionJankMonitor.ACTION_SESSION_CANCEL);
+        getInstrumentation().getContext().registerReceiver(mReceiver, filter);
+        mHelper.startCollecting();
+        return true;
+    }
+
+    @Override
+    protected boolean onTestEndAlternative(DataRecord data) {
+        try {
+            synchronized (mLock) {
+                mCurrentTestTimestamp.end(System.nanoTime());
+                if (mExpectedCujSet.size() > 0) {
+                    for (int i = 0; i < POLLING_MAX_TIMES && !mMetricsReady; i++) {
+                        flushSurfaceFlingerCallback();
+                        mLock.wait(POLLING_INTERVAL_MS);
+                    }
+                    if (mMetricsReady) {
+                        processMetrics(data);
+                    } else {
+                        throw new IllegalStateException("metrics not ready: ex="
+                                + mExpectedCujSet + ", log=" + mLoggedCujSet);
+                    }
+                } else {
+                    throw new IllegalStateException("No expected CUJ!");
+                }
+            }
+        } catch (InterruptedException e) {
+        } finally {
+            synchronized (mLock) {
+                mMetricsReady = false;
+                mExpectedCujSet.clear();
+                mLoggedCujSet.clear();
+            }
+            getInstrumentation().getContext().unregisterReceiver(mReceiver);
+            mHelper.stopCollecting();
+        }
+        return true;
+    }
+
+    private void flushSurfaceFlingerCallback() throws InterruptedException {
+        try {
+            getInstrumentation().getUiAutomation().executeShellCommand(CMD_TOGGLE_PANEL);
+            Thread.sleep(DELAY_TOGGLE_PANEL_MS);
+        } finally {
+            getInstrumentation().getUiAutomation().executeShellCommand(CMD_TOGGLE_PANEL);
+        }
+    }
+
+    private void reduceMetrics(DataRecord data, String key, String value) {
+        if (data == null || key.isEmpty() || value.isEmpty()) return;
+
+        double result = 0;
+        String[] tokens = value.split(MetricUtility.METRIC_SEPARATOR);
+        for (String token : tokens) {
+            if (key.endsWith(UiInteractionFrameInfoHelper.SUFFIX_MAX_FRAME_MS)) {
+                result = Double.max(result, Double.parseDouble(token));
+            } else {
+                result += Double.parseDouble(token);
+                result = UiInteractionFrameInfoHelper.makeLogFriendly(Math.floor(result));
+            }
+        }
+        data.addStringMetric(key, Double.toString(result));
+    }
+
+    private void processMetrics(DataRecord data) throws InterruptedException {
+        final Set<String> foundCujSet = new HashSet<>();
+        DataRecord metricsData = new DataRecord();
+        while (!(foundCujSet.size() == mLoggedCujSet.size())) {
+            super.collectMetrics(metricsData);
+            if (!metricsData.hasMetrics()) {
+                mLock.wait(POLLING_INTERVAL_MS);
+                continue;
+            }
+            Bundle bundle = metricsData.createBundleFromMetrics();
+            for (String key : bundle.keySet()) {
+                for (String cujName : mLoggedCujSet) {
+                    if (key.startsWith(cujName)) {
+                        reduceMetrics(data, key, bundle.getString(key));
+                        foundCujSet.add(cujName);
+                        break;
+                    }
+                }
+            }
+            metricsData.clear();
+        }
+    }
+
+    private static class TimestampRecord {
+        private long begin = Long.MAX_VALUE;
+        private long end = Long.MAX_VALUE;
+
+        void begin(long timestamp) {
+            begin = timestamp;
+        }
+
+        void end(long timestamp) {
+            end = timestamp;
+        }
+
+        boolean isInRange(long timestamp) {
+            return begin <= timestamp && timestamp <= end;
+        }
+    }
+
+    private class MetricsLoggedReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            String name = intent.getStringExtra(InteractionJankMonitor.BUNDLE_KEY_CUJ_NAME);
+            long timestamp = intent.getLongExtra(InteractionJankMonitor.BUNDLE_KEY_TIMESTAMP, -1L);
+
+            if (InteractionJankMonitor.ACTION_SESSION_BEGIN.equals(action)) {
+                handleSessionBegin(name, timestamp);
+            } else if (InteractionJankMonitor.ACTION_SESSION_CANCEL.equals(action)) {
+                handleSessionCancelled(name);
+            } else if (InteractionJankMonitor.ACTION_METRICS_LOGGED.equals(action)) {
+                handleMetricsLogged(name);
+            }
+        }
+
+        private void handleSessionBegin(String name, long timestamp) {
+            synchronized (mLock) {
+                if (mCurrentTestTimestamp.isInRange(timestamp)) {
+                    mExpectedCujSet.add(name);
+                }
+            }
+        }
+
+        private void handleSessionCancelled(String name) {
+            synchronized (mLock) {
+                boolean valid = mExpectedCujSet.remove(name);
+                if (valid && (mLoggedCujSet.size() == mExpectedCujSet.size())) {
+                    mMetricsReady = true;
+                    mLock.notifyAll();
+                }
+            }
+        }
+
+        private void handleMetricsLogged(String name) {
+            synchronized (mLock) {
+                if (mExpectedCujSet.contains(name)) {
+                    mLoggedCujSet.add(
+                            MetricUtility.constructKey(
+                                    UiInteractionFrameInfoHelper.KEY_PREFIX_CUJ, name));
+                    if (mLoggedCujSet.size() == mExpectedCujSet.size()) {
+                        mMetricsReady = true;
+                        mLock.notifyAll();
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/libraries/device-collectors/src/test/java/Android.bp b/libraries/device-collectors/src/test/java/Android.bp
index 47161d6..30ee9f3 100644
--- a/libraries/device-collectors/src/test/java/Android.bp
+++ b/libraries/device-collectors/src/test/java/Android.bp
@@ -27,11 +27,11 @@
     static_libs: [
         "androidx.test.ext.junit",
         "androidx.test.runner",
+        "androidx.test.uiautomator",
         "collector-device-lib",
         "junit",
         "mockito-target-minus-junit4",
         "perfetto-helper",
-        "ub-uiautomator",
     ],
 
     sdk_version: "current",
diff --git a/libraries/device-collectors/src/test/java/android/device/collectors/DumpsysServiceListenerTest.java b/libraries/device-collectors/src/test/java/android/device/collectors/DumpsysServiceListenerTest.java
new file mode 100644
index 0000000..82186a2
--- /dev/null
+++ b/libraries/device-collectors/src/test/java/android/device/collectors/DumpsysServiceListenerTest.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2021 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.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.app.Instrumentation;
+import android.os.Bundle;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.helpers.DumpsysServiceHelper;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.Description;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+/**
+ * Android Unit tests for {@link DumpsysServiceListener}.
+ *
+ * <p>To run: atest CollectorDeviceLibTest:android.device.collectors.DumpsysServiceListenerTest
+ */
+@RunWith(AndroidJUnit4.class)
+public class DumpsysServiceListenerTest {
+
+    @Mock private DumpsysServiceHelper mDumpsysServiceHelper;
+    @Mock private Instrumentation mInstrumentation;
+
+    private Description mRunDesc;
+
+    @Before
+    public void setup() {
+        initMocks(this);
+        mRunDesc = Description.createSuiteDescription("run");
+    }
+
+    @Test
+    public void testListener_noProcessNames() throws Exception {
+        DumpsysServiceListener listener = initListener(new Bundle(), mDumpsysServiceHelper);
+        listener.testRunStarted(mRunDesc);
+        verify(mDumpsysServiceHelper, never()).setUp(any());
+    }
+
+    @Test
+    public void testListener_withProcessNames() throws Exception {
+        Bundle bundle = new Bundle();
+        bundle.putString(
+                DumpsysServiceListener.SERVICE_NAMES_KEY,
+                String.join(DumpsysServiceListener.SERVICE_SEPARATOR, "service1", "service2"));
+        DumpsysServiceListener listener = initListener(bundle, mDumpsysServiceHelper);
+        listener.testRunStarted(mRunDesc);
+        verify(mDumpsysServiceHelper).setUp("service1", "service2");
+    }
+
+    private DumpsysServiceListener initListener(Bundle bundle, DumpsysServiceHelper helper) {
+        DumpsysServiceListener listener = new DumpsysServiceListener(bundle, helper);
+        listener.setInstrumentation(mInstrumentation);
+        return listener;
+    }
+}
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 2094015..2ed21cc 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
@@ -22,6 +22,7 @@
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.never;
@@ -129,7 +130,9 @@
         mListener.testStarted(mTest1Desc);
         verify(mPerfettoHelper, times(1)).startCollecting(anyString(), anyBoolean());
         mListener.onTestEnd(mDataRecord, mTest1Desc);
-        verify(mPerfettoHelper, times(1)).stopCollecting(anyLong(), anyString());
+        verify(mPerfettoHelper, times(1)).stopCollecting(anyLong(), eq(
+                "/sdcard/test_results/run_test1/PerfettoListener_1_Proxy/"
+                + "perfetto_run_test1-1.pb"));
 
     }
 
@@ -515,4 +518,30 @@
 
     }
 
+    /*
+     * Verify spaces in the test description are replaced with special character. Test description
+     * is used for creating the perfetto file, if the spaces are not replaced then tradefed content
+     * provider will throw an error for the files with spaces.
+     */
+    @Test
+    public void testPerfettoTestNameWithSpaces() throws Exception {
+        Bundle b = new Bundle();
+        mListener = initListener(b);
+        doReturn(true).when(mPerfettoHelper).startCollecting(anyString(), anyBoolean());
+        doReturn(true).when(mPerfettoHelper).stopCollecting(anyLong(), anyString());
+        // Test run start behavior
+        mListener.testRunStarted(mRunDesc);
+
+        Description mTest1DescWithSpaces = Description.createTestDescription("run  123",
+                "test1 456");
+        // Test test start behavior
+        mListener.testStarted(mTest1DescWithSpaces);
+        verify(mPerfettoHelper, times(1)).startCollecting(anyString(), anyBoolean());
+        mListener.onTestEnd(mDataRecord, mTest1DescWithSpaces);
+        verify(mPerfettoHelper, times(1)).stopCollecting(anyLong(), eq(
+                "/sdcard/test_results/run#123_test1#456/PerfettoListener_1_Proxy/"
+                + "perfetto_run#123_test1#456-1.pb"));
+
+    }
+
 }
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
index ceff8fd..664a294 100644
--- a/libraries/device-collectors/src/test/java/android/device/collectors/ScreenRecordCollectorTest.java
+++ b/libraries/device-collectors/src/test/java/android/device/collectors/ScreenRecordCollectorTest.java
@@ -31,7 +31,7 @@
 import android.device.collectors.util.SendToInstrumentation;
 import android.os.Bundle;
 import android.os.SystemClock;
-import android.support.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.UiDevice;
 import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.After;
diff --git a/libraries/device-collectors/src/test/java/android/device/collectors/SimpleperfListenerTest.java b/libraries/device-collectors/src/test/java/android/device/collectors/SimpleperfListenerTest.java
index 7473f71..085a7f9 100644
--- a/libraries/device-collectors/src/test/java/android/device/collectors/SimpleperfListenerTest.java
+++ b/libraries/device-collectors/src/test/java/android/device/collectors/SimpleperfListenerTest.java
@@ -88,14 +88,14 @@
     public void testSimpleperfPerTestSuccessFlow() throws Exception {
         Bundle b = new Bundle();
         mListener = initListener(b);
-        doReturn(true).when(mSimpleperfHelper).startCollecting();
+        doReturn(true).when(mSimpleperfHelper).startCollecting(anyString(), anyString());
         doReturn(true).when(mSimpleperfHelper).stopCollecting(anyString());
         // Test run start behavior
         mListener.testRunStarted(mRunDesc);
 
         // Test test start behavior
         mListener.testStarted(mTest1Desc);
-        verify(mSimpleperfHelper, times(1)).startCollecting();
+        verify(mSimpleperfHelper, times(1)).startCollecting(anyString(), anyString());
         mListener.onTestEnd(mDataRecord, mTest1Desc);
         verify(mSimpleperfHelper, times(1)).stopCollecting(anyString());
     }
@@ -110,14 +110,14 @@
         b.putString(SimpleperfListener.SKIP_TEST_FAILURE_METRICS, "false");
         mListener = initListener(b);
 
-        doReturn(true).when(mSimpleperfHelper).startCollecting();
+        doReturn(true).when(mSimpleperfHelper).startCollecting(anyString(), anyString());
         doReturn(true).when(mSimpleperfHelper).stopCollecting(anyString());
         // Test run start behavior
         mListener.testRunStarted(mRunDesc);
 
         // Test test start behavior
         mListener.testStarted(mTest1Desc);
-        verify(mSimpleperfHelper, times(1)).startCollecting();
+        verify(mSimpleperfHelper, times(1)).startCollecting(anyString(), anyString());
 
         // Test fail behaviour
         Failure failureDesc = new Failure(FAKE_TEST_DESCRIPTION, new Exception());
@@ -136,14 +136,14 @@
         b.putString(SimpleperfListener.SKIP_TEST_FAILURE_METRICS, "true");
         mListener = initListener(b);
 
-        doReturn(true).when(mSimpleperfHelper).startCollecting();
+        doReturn(true).when(mSimpleperfHelper).startCollecting(anyString(), anyString());
         doReturn(true).when(mSimpleperfHelper).stopSimpleperf();
         // Test run start behavior
         mListener.testRunStarted(mRunDesc);
 
         // Test test start behavior
         mListener.testStarted(mTest1Desc);
-        verify(mSimpleperfHelper, times(1)).startCollecting();
+        verify(mSimpleperfHelper, times(1)).startCollecting(anyString(), anyString());
 
         // Test fail behaviour
         Failure failureDesc = new Failure(FAKE_TEST_DESCRIPTION, new Exception());
@@ -161,14 +161,14 @@
         Bundle b = new Bundle();
         b.putString(SimpleperfListener.COLLECT_PER_RUN, "true");
         mListener = initListener(b);
-        doReturn(true).when(mSimpleperfHelper).startCollecting();
+        doReturn(true).when(mSimpleperfHelper).startCollecting(anyString(), anyString());
         doReturn(true).when(mSimpleperfHelper).stopCollecting(anyString());
 
         // Test run start behavior
         mListener.onTestRunStart(mListener.createDataRecord(), FAKE_DESCRIPTION);
-        verify(mSimpleperfHelper, times(1)).startCollecting();
+        verify(mSimpleperfHelper, times(1)).startCollecting(anyString(), anyString());
         mListener.testStarted(mTest1Desc);
-        verify(mSimpleperfHelper, times(1)).startCollecting();
+        verify(mSimpleperfHelper, times(1)).startCollecting(anyString(), anyString());
         mListener.onTestEnd(mDataRecord, mTest1Desc);
         verify(mSimpleperfHelper, times(0)).stopCollecting(anyString());
         mListener.onTestRunEnd(mListener.createDataRecord(), new Result());
@@ -183,11 +183,11 @@
         Bundle b = new Bundle();
         b.putString(SimpleperfListener.COLLECT_PER_RUN, "true");
         mListener = initListener(b);
-        doReturn(false).when(mSimpleperfHelper).startCollecting();
+        doReturn(false).when(mSimpleperfHelper).startCollecting(anyString(), anyString());
 
         // Test run start behavior
         mListener.onTestRunStart(mListener.createDataRecord(), FAKE_DESCRIPTION);
-        verify(mSimpleperfHelper, times(1)).startCollecting();
+        verify(mSimpleperfHelper, times(1)).startCollecting(anyString(), anyString());
         mListener.onTestRunEnd(mListener.createDataRecord(), new Result());
         verify(mSimpleperfHelper, times(0)).stopCollecting(anyString());
     }
@@ -199,14 +199,14 @@
     public void testSimpleperfStartFailureFlow() throws Exception {
         Bundle b = new Bundle();
         mListener = initListener(b);
-        doReturn(false).when(mSimpleperfHelper).startCollecting();
+        doReturn(false).when(mSimpleperfHelper).startCollecting(anyString(), anyString());
 
         // Test run start behavior
         mListener.testRunStarted(mRunDesc);
 
         // Test test start behavior
         mListener.testStarted(mTest1Desc);
-        verify(mSimpleperfHelper, times(1)).startCollecting();
+        verify(mSimpleperfHelper, times(1)).startCollecting(anyString(), anyString());
         mListener.onTestEnd(mDataRecord, mTest1Desc);
         verify(mSimpleperfHelper, times(0)).stopCollecting(anyString());
     }
@@ -219,7 +219,7 @@
     public void testSimpleperfInvocationCount() throws Exception {
         Bundle b = new Bundle();
         mListener = initListener(b);
-        doReturn(true).when(mSimpleperfHelper).startCollecting();
+        doReturn(true).when(mSimpleperfHelper).startCollecting(anyString(), anyString());
         doReturn(true).when(mSimpleperfHelper).stopCollecting(anyString());
 
         // Test run start behavior
@@ -227,19 +227,19 @@
 
         // Test1 invocation 1 start behavior
         mListener.testStarted(mTest1Desc);
-        verify(mSimpleperfHelper, times(1)).startCollecting();
+        verify(mSimpleperfHelper, times(1)).startCollecting(anyString(), anyString());
         mListener.onTestEnd(mDataRecord, mTest1Desc);
         verify(mSimpleperfHelper, times(1)).stopCollecting(anyString());
 
         // Test1 invocation 2 start behaviour
         mListener.testStarted(mTest1Desc);
-        verify(mSimpleperfHelper, times(2)).startCollecting();
+        verify(mSimpleperfHelper, times(2)).startCollecting(anyString(), anyString());
         mListener.onTestEnd(mDataRecord, mTest1Desc);
         verify(mSimpleperfHelper, times(2)).stopCollecting(anyString());
 
         // Test2 invocation 1 start behaviour
         mListener.testStarted(mTest2Desc);
-        verify(mSimpleperfHelper, times(3)).startCollecting();
+        verify(mSimpleperfHelper, times(3)).startCollecting(anyString(), anyString());
         mDataRecord = mListener.createDataRecord();
         mListener.onTestEnd(mDataRecord, mTest2Desc);
         verify(mSimpleperfHelper, times(3)).stopCollecting(anyString());
diff --git a/libraries/device-collectors/src/test/platform/Android.bp b/libraries/device-collectors/src/test/platform/Android.bp
index 547424c..d4e3501 100644
--- a/libraries/device-collectors/src/test/platform/Android.bp
+++ b/libraries/device-collectors/src/test/platform/Android.bp
@@ -30,8 +30,7 @@
         "junit",
         "mockito-target-minus-junit4",
         "perfetto-helper",
-        "platformprotoslite",
-        "ub-uiautomator",
+        "statsdprotonano",
     ],
     platform_apis: true,
     min_sdk_version: "26",
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 b134d17..4e0c0f6 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
@@ -32,11 +32,13 @@
 import android.content.res.AssetManager;
 import android.os.Bundle;
 
-import com.android.internal.os.StatsdConfigProto.StatsdConfig;
-import com.android.os.AtomsProto.Atom;
-import com.android.os.StatsLog.ConfigMetricsReportList;
-import com.android.os.StatsLog.ConfigMetricsReportList.ConfigKey;
+import com.android.internal.os.nano.StatsdConfigProto;
+import com.android.os.nano.AtomsProto;
+import com.android.os.nano.StatsLog;
+
 import com.google.common.collect.ImmutableMap;
+import com.google.protobuf.nano.CodedOutputByteBufferNano;
+import com.google.protobuf.nano.ExtendableMessageNano;
 
 import org.junit.Assert;
 import org.junit.Before;
@@ -48,9 +50,11 @@
 
 import java.io.ByteArrayInputStream;
 import java.io.File;
+import java.io.IOException;
 import java.nio.file.Paths;
 import java.util.Arrays;
 import java.util.Map;
+import java.util.stream.IntStream;
 
 /** Unit tests for {@link StatsdListener}. */
 public class StatsdListenerTest {
@@ -68,32 +72,47 @@
     private static final String TEST_METHOD_NAME_1 = "testMethodOne";
     private static final String TEST_METHOD_NAME_2 = "testMethodTwo";
 
-    private static final StatsdConfig CONFIG_1 =
-            StatsdConfig.newBuilder().setId(CONFIG_ID_1).build();
-    private static final StatsdConfig CONFIG_2 =
-            StatsdConfig.newBuilder().setId(CONFIG_ID_2).build();
+    private static final StatsdConfigProto.StatsdConfig CONFIG_1 =
+            new StatsdConfigProto.StatsdConfig();
 
-    private static final ConfigMetricsReportList REPORT_1 =
-            ConfigMetricsReportList.newBuilder()
-                    .setConfigKey(ConfigKey.newBuilder().setUid(0).setId(CONFIG_ID_1))
-                    .build();
-    private static final ConfigMetricsReportList REPORT_2 =
-            ConfigMetricsReportList.newBuilder()
-                    .setConfigKey(ConfigKey.newBuilder().setUid(0).setId(CONFIG_ID_2))
-                    .build();
+    private static final StatsdConfigProto.StatsdConfig CONFIG_2 =
+            new StatsdConfigProto.StatsdConfig();
 
-    private static final ImmutableMap<String, StatsdConfig> CONFIG_MAP =
+    private static final StatsLog.ConfigMetricsReportList REPORT_1 =
+            new StatsLog.ConfigMetricsReportList();
+
+    private static final StatsLog.ConfigMetricsReportList REPORT_2 =
+            new StatsLog.ConfigMetricsReportList();
+
+    private static final ImmutableMap<String, StatsdConfigProto.StatsdConfig> CONFIG_MAP =
             ImmutableMap.of(CONFIG_NAME_1, CONFIG_1, CONFIG_NAME_2, CONFIG_2);
 
     @Rule public ExpectedException mExpectedException = ExpectedException.none();
 
     @Before
     public void setUp() throws Exception {
+        CONFIG_1.id = CONFIG_ID_1;
+        CONFIG_2.id = CONFIG_ID_2;
+
+        // Config key for report 1
+        StatsLog.ConfigMetricsReportList.ConfigKey report1Key =
+                new StatsLog.ConfigMetricsReportList.ConfigKey();
+        report1Key.uid = 0;
+        report1Key.id = CONFIG_ID_1;
+        REPORT_1.configKey = report1Key;
+
+        // Config key for report 2
+        StatsLog.ConfigMetricsReportList.ConfigKey report2Key =
+                new StatsLog.ConfigMetricsReportList.ConfigKey();
+        report1Key.uid = 0;
+        report1Key.id = CONFIG_ID_2;
+        REPORT_2.configKey = report2Key;
+
         mListener = spy(new StatsdListener());
         // Stub the report collection to isolate collector from StatsManager.
         doNothing().when(mListener).addStatsConfig(anyLong(), any());
-        doReturn(REPORT_1.toByteArray()).when(mListener).getStatsReports(eq(CONFIG_ID_1));
-        doReturn(REPORT_2.toByteArray()).when(mListener).getStatsReports(eq(CONFIG_ID_2));
+        doReturn(serialize(REPORT_1)).when(mListener).getStatsReports(eq(CONFIG_ID_1));
+        doReturn(serialize(REPORT_2)).when(mListener).getStatsReports(eq(CONFIG_ID_2));
         doNothing().when(mListener).removeStatsConfig(anyLong());
         // Stub calls to permission APIs.
         doNothing().when(mListener).adoptShellPermissionIdentity();
@@ -119,8 +138,8 @@
         Description description = Description.createSuiteDescription("TestRun");
 
         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)).addStatsConfig(eq(CONFIG_ID_1), eq(serialize(CONFIG_1)));
+        verify(mListener, times(1)).addStatsConfig(eq(CONFIG_ID_2), eq(serialize(CONFIG_2)));
         verify(mListener, times(1)).logStart(eq(StatsdListener.RUN_EVENT_LABEL));
 
         mListener.onTestRunEnd(runData, new Result());
@@ -206,8 +225,8 @@
                 new DataRecord(), Description.createSuiteDescription("Placeholder"));
 
         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)).addStatsConfig(eq(CONFIG_ID_1), eq(serialize(CONFIG_1)));
+        verify(mListener, times(1)).addStatsConfig(eq(CONFIG_ID_2), eq(serialize(CONFIG_2)));
         verify(mListener, times(1)).logStart(eq(StatsdListener.TEST_EVENT_LABEL));
 
         mListener.onTestEnd(testData, description);
@@ -452,12 +471,12 @@
     @Test
     public void testParsingConfigFromArguments_validConfig() throws Exception {
         // Stub two configs for testing.
-        ByteArrayInputStream config1Stream = new ByteArrayInputStream(CONFIG_1.toByteArray());
+        ByteArrayInputStream config1Stream = new ByteArrayInputStream(serialize(CONFIG_1));
         doReturn(config1Stream)
                 .when(mListener)
                 .openConfigWithAssetManager(any(AssetManager.class), eq(CONFIG_NAME_1));
 
-        ByteArrayInputStream config2Stream = new ByteArrayInputStream(CONFIG_2.toByteArray());
+        ByteArrayInputStream config2Stream = new ByteArrayInputStream(serialize(CONFIG_2));
         doReturn(config2Stream)
                 .when(mListener)
                 .openConfigWithAssetManager(any(AssetManager.class), eq(CONFIG_NAME_2));
@@ -468,12 +487,12 @@
                 String.join(",", CONFIG_NAME_1, CONFIG_NAME_2));
         doReturn(args).when(mListener).getArguments();
 
-        Map<String, StatsdConfig> configs =
+        Map<String, StatsdConfigProto.StatsdConfig> configs =
                 mListener.getConfigsFromOption(StatsdListener.OPTION_CONFIGS_RUN_LEVEL);
         Assert.assertTrue(configs.containsKey(CONFIG_NAME_1));
-        Assert.assertEquals(configs.get(CONFIG_NAME_1).getId(), CONFIG_ID_1);
+        Assert.assertEquals(configs.get(CONFIG_NAME_1).id, CONFIG_ID_1);
         Assert.assertTrue(configs.containsKey(CONFIG_NAME_2));
-        Assert.assertEquals(configs.get(CONFIG_NAME_2).getId(), CONFIG_ID_2);
+        Assert.assertEquals(configs.get(CONFIG_NAME_2).id, CONFIG_ID_2);
         Assert.assertEquals(configs.size(), 2);
     }
 
@@ -491,7 +510,7 @@
         doReturn(args).when(mListener).getArguments();
 
         mExpectedException.expectMessage("Cannot parse");
-        Map<String, StatsdConfig> configs =
+        Map<String, StatsdConfigProto.StatsdConfig> configs =
                 mListener.getConfigsFromOption(StatsdListener.OPTION_CONFIGS_RUN_LEVEL);
     }
 
@@ -503,7 +522,7 @@
         doReturn(args).when(mListener).getArguments();
 
         mExpectedException.expectMessage("does not exist");
-        Map<String, StatsdConfig> configs =
+        Map<String, StatsdConfigProto.StatsdConfig> configs =
                 mListener.getConfigsFromOption(StatsdListener.OPTION_CONFIGS_RUN_LEVEL);
     }
 
@@ -560,7 +579,7 @@
     @Test
     public void testConfigsHavePermissionFixes() throws Exception {
         // Stub a config for testing.
-        ByteArrayInputStream configStream = new ByteArrayInputStream(CONFIG_1.toByteArray());
+        ByteArrayInputStream configStream = new ByteArrayInputStream(serialize(CONFIG_1));
         doReturn(configStream)
                 .when(mListener)
                 .openConfigWithAssetManager(any(AssetManager.class), eq(CONFIG_NAME_1));
@@ -569,18 +588,28 @@
         args.putString(StatsdListener.OPTION_CONFIGS_RUN_LEVEL, CONFIG_NAME_1);
         doReturn(args).when(mListener).getArguments();
 
-        Map<String, StatsdConfig> configs =
+        Map<String, StatsdConfigProto.StatsdConfig> configs =
                 mListener.getConfigsFromOption(StatsdListener.OPTION_CONFIGS_RUN_LEVEL);
         Assert.assertTrue(configs.containsKey(CONFIG_NAME_1));
+        int[] whitelistedAtomIds = configs.get(CONFIG_NAME_1).whitelistedAtomIds;
+        String[] defaultPullPackages = configs.get(CONFIG_NAME_1).defaultPullPackages;
         Assert.assertTrue(
-                configs.get(CONFIG_NAME_1)
-                        .getWhitelistedAtomIdsList()
-                        .stream()
-                        .anyMatch(id -> id == Atom.APP_BREADCRUMB_REPORTED_FIELD_NUMBER));
+                IntStream.of(whitelistedAtomIds)
+                        .anyMatch(
+                                id -> id == AtomsProto.Atom.APP_BREADCRUMB_REPORTED_FIELD_NUMBER));
         Assert.assertTrue(
-                configs.get(CONFIG_NAME_1)
-                        .getDefaultPullPackagesList()
+                Arrays.asList(defaultPullPackages)
                         .stream()
                         .anyMatch(name -> "AID_SYSTEM".equals(name)));
     }
+
+    // Some utilities for Nano protos.
+
+    private static <T extends ExtendableMessageNano<T>> byte[] serialize(
+            ExtendableMessageNano<T> message) throws IOException {
+        byte[] serialized = new byte[message.getSerializedSize()];
+        CodedOutputByteBufferNano buffer = CodedOutputByteBufferNano.newInstance(serialized);
+        message.writeTo(buffer);
+        return serialized;
+    }
 }
diff --git a/libraries/flicker/Android.bp b/libraries/flicker/Android.bp
index f172f9c..1348479 100644
--- a/libraries/flicker/Android.bp
+++ b/libraries/flicker/Android.bp
@@ -21,42 +21,65 @@
 java_test {
     name: "flickerlib",
     platform_apis: true,
-    srcs: ["src/**/*.java"],
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt"
+    ],
     static_libs: [
-        "androidx.test.janktesthelper",
-        "cts-wm-util",
-        "platformprotosnano",
-        "layersprotosnano",
-        "truth-prebuilt",
-        "sysui-helper",
-        "launcher-helper-lib",
+        "flickerlib-core",
+        "flickerlib-helpers"
     ],
 }
 
 java_library {
-    name: "flickerlib_without_helpers",
+    name: "flickerlib-core",
     platform_apis: true,
-    srcs: ["src/**/*.java"],
-    exclude_srcs: ["src/**/helpers/*.java"],
+    srcs: [
+        "src/com/android/server/wm/flicker/**/*.java",
+        "src/com/android/server/wm/flicker/**/*.kt"
+    ],
+    exclude_srcs: [
+        "**/helpers/*",
+    ],
     static_libs: [
-        "cts-wm-util",
-        "platformprotosnano",
-        "layersprotosnano",
+        "compatibility-device-util-axt",
+        "ub-uiautomator",
+        "androidx.test.uiautomator_uiautomator",
+        "androidx.test.ext.junit",
         "truth-prebuilt",
-        "app-helpers-core",
+        "launcher-helper-lib",
+        "wm-proto-parsers",
+        "platform-test-annotations",
+        "platform-test-core-rules",
     ],
 }
 
 java_library {
-    name: "flickerautomationhelperlib",
+    name: "flickerlib-helpers",
     sdk_version: "test_current",
     srcs: [
-        "src/com/android/server/wm/flicker/helpers/AutomationUtils.java",
-        "src/com/android/server/wm/flicker/WindowUtils.java",
+        "src/**/helpers/*.java",
+        "src/**/helpers/*.kt",
     ],
     static_libs: [
-        "sysui-helper",
-        "launcher-helper-lib",
         "compatibility-device-util-axt",
+        "app-helpers-core",
+        "launcher-helper-lib",
+        "wm-proto-parsers",
+    ],
+}
+
+java_library {
+    name: "wm-proto-parsers",
+    sdk_version: "test_current",
+    srcs: [
+        "src/com/android/server/wm/traces/**/*.java",
+        "src/com/android/server/wm/traces/**/*.kt",
+    ],
+    static_libs: [
+        "android-support-annotations",
+        "androidx.test.ext.junit",
+        "platformprotosnano",
+        "layersprotosnano",
     ],
 }
diff --git a/libraries/flicker/OWNERS b/libraries/flicker/OWNERS
index 6929b6e..ec25b44 100644
--- a/libraries/flicker/OWNERS
+++ b/libraries/flicker/OWNERS
@@ -1,3 +1,4 @@
 jjaggi@google.com
 roosa@google.com
 vishnun@google.com
+natanieljr@google.com
\ No newline at end of file
diff --git a/libraries/flicker/README.md b/libraries/flicker/README.md
new file mode 100644
index 0000000..97f79b9
--- /dev/null
+++ b/libraries/flicker/README.md
@@ -0,0 +1,160 @@
+# Flicker Test Library
+
+## Motivation
+Detect *flicker* &mdash; any discontinuous, or unpredictable behavior seen during UI transitions that is not due to performance.
+This is often the result of a logic error in the code and difficult to identify because the issue is transient and at times difficult to reproduce.
+This library helps create integration tests between `SurfaceFlinger`, `WindowManager` and `SystemUI` to identify flicker.
+
+Examples of flickers are:
+* Elements drawn in the wrong location (e.g., not rotating correctly)
+* Empty areas on the screen (e.g., not all areas of the screen covered during rotation)
+* System elements not appearing (e.g., nav and status bar or split screen divider)
+* Elements (dis)appearing at the wrong time (e.g., an element becomes invisible before being completely occluded)
+
+## Usage and Capabilities
+
+The library builds and runs UI transitions, captures Winscope [traces](https://source.android.com/devices/graphics/tracing-win-transitions) and exposes common assertions that can be tested against each trace.
+
+## Building a transition
+
+Start by defining common or error prone transitions using `TransitionRunner`.
+```java
+// Example: Build a transition that cold launches an app from launcher
+TransitionRunner transition = new TransitionBuilder()
+                // Specify a tag to identify the transition (optional)
+                .withTag("OpenAppCold_" + testApp.getLauncherName())
+
+                // Specify preconditions to setup the device
+                // Wake up device and go to home screen
+                .runBeforeAll(AutomationUtils::wakeUpAndGoToHomeScreen)
+
+                // Setup transition under test
+                // Press the home button and close the app to test a cold start
+                .runBefore(device::pressHome)
+                .runBefore(testApp::exit)
+
+                // Run the transition under test
+                // Open the app and wait for UI to be idle
+                // This is the part of the transition that will be tested.
+                .run(testApp::open)
+                .run(device::waitForIdle)
+
+                // Perform any tear downs
+                // Close the app
+                .runAfterAll(testApp::exit)
+
+                // Number of times to repeat the transition to catch any flaky issues
+                .repeat(5);
+```
+
+Run the transition to get a list of `TransitionResult` for each time the transition is repeated.
+```java
+    List<TransitionResult> results = transition.run();
+```
+`TransitionResult` contains paths to test artifacts such as Winscope traces and screen recordings.
+
+### Checking Assertions
+Each `TransitionResult` can be tested using an extension of the Google Truth library, `LayersTraceSubject` and `WmTraceSubject`.
+They try to balance test principles set out by Google Truth (not supporting nested assertions, keeping assertions simple) with providing support for common assertion use cases.
+
+Each trace can be represented as a ordered collection of trace entries, with an associated timestamp.
+Each trace entry has common assertion checks.
+The trace subjects expose methods to filter the range of entries and test for changing assertions.
+
+```java
+    TransitionResult result = results.get(0);
+    Rect displayBounds = getDisplayBounds();
+
+    // check all trace entries
+    assertThat(result).coversRegion(displayBounds).forAllEntries();
+
+    // check a range of entries
+    assertThat(result).coversRegion(displayBounds).forRange(startTime, endTime);
+
+    // check first entry
+    assertThat(result).coversRegion(displayBounds).inTheBeginning();
+
+    // check last entry
+    assertThat(result).coversRegion(displayBounds).atTheEnd();
+
+    // check a change in assertions, e.g. wallpaper window is visible,
+    // then wallpaper window becomes and stays invisible
+    assertThat(result)
+                .showsBelowAppWindow("wallpaper")
+                .then()
+                .hidesBelowAppWindow("wallpaper")
+                .forAllEntries();
+```
+
+All assertions return `Result` which contains a `success` flag, `assertionName` string identifier, and `reason` string to provide actionable details to the user.
+The `reason` string is build along the way with all the details as to why the assertions failed and any hints which might help the user determine the root cause.
+Failed assertion message will also contain a path to the trace that was tested.
+Example of a failed test:
+
+```
+    java.lang.AssertionError: Not true that <com.android.server.wm.traces.common.LayersTrace@65da4cc>
+    Layers Trace can be found in: /layers_trace_emptyregion.pb
+    Timestamp: 2308008331271
+    Assertion: coversRegion
+    Reason:   Region to test: Rect(0, 0 - 1440, 2880)
+    first empty point: 0, 99
+    visible regions:
+    StatusBar#0Rect(0, 0 - 1440, 98)
+    NavigationBar#0Rect(0, 2712 - 1440, 2880)
+    ScreenDecorOverlay#0Rect(0, 0 - 1440, 91)
+    ...
+        at com.google.common.truth.FailureStrategy.fail(FailureStrategy.java:24)
+        ...
+```
+
+---
+
+## Running Tests
+
+The tests can be run as any other Android JUnit tests. `frameworks/base/tests/FlickerTests` uses the library to test common UI transitions. Run `atest FlickerTest` to execute these tests.
+
+---
+
+## Other Topics
+### Monitors
+Monitors capture test artifacts for each transition run. They are started before each iteration of the test transition (after the `runBefore` calls) and stopped after the transition is completed. Each iteration will produce a new test artifact. The following monitors are available:
+
+#### LayersTraceMonitor
+Captures Layers trace. This monitor is started by default. Build a transition with `skipLayersTrace()` to disable this monitor.
+#### WindowManagerTraceMonitor
+Captures Window Manager trace. This monitor is started by default. Build a transition with `skipWindowManagerTrace()` to disable this monitor.
+#### WindowAnimationFrameStatsMonitor
+Captures WindowAnimationFrameStats for the transition. This monitor is started by default and is used to eliminate *janky* runs. If an iteration has skipped frames, as determined by WindowAnimationFrameStats, the results for the iteration is skipped. If the list of results is empty after all iterations are completed, then the test should fail. Build a transition with `includeJankyRuns()` to disable this monitor.
+#### ScreenRecorder
+Captures screen to a video file. This monitor is disabled by default. Build a transition with `recordEachRun()` to capture each transition or build with `recordAllRuns()` to capture every transition including setup and teardown.
+
+---
+
+### Extending Assertions
+
+To add a new assertion, add a function to one of the trace entry classes, `LayersTrace.Entry` or `WindowManagerTrace.Entry`.
+
+```java
+    // Example adds an assertion to the check if layer is hidden by parent.
+    Result isHiddenByParent(String layerName) {
+        // Result should contain a details if assertion fails for any reason
+        // such as if layer is not found or layer is not hidden by parent
+        // or layer has no parent.
+        // ...
+    }
+```
+Then add a function to the trace subject `LayersTraceSubject` or `WmTraceSubject` which will add the assertion for testing. When the assertion is evaluated, the trace will first be filtered then the assertion will be applied to the remaining entries.
+
+```java
+    public LayersTraceSubject isHiddenByParent(String layerName) {
+        mChecker.add(entry -> entry.isHiddenByParent(layerName),
+                "isHiddenByParent(" + layerName + ")");
+        return this;
+    }
+```
+
+To use the new assertion:
+```java
+    // Check if "Chrome" layer is hidden by parent in the first trace entry.
+    assertThat(result).isHiddenByParent("Chrome").inTheBeginning();
+```
\ No newline at end of file
diff --git a/libraries/flicker/TEST_MAPPING b/libraries/flicker/TEST_MAPPING
new file mode 100644
index 0000000..8e3f458
--- /dev/null
+++ b/libraries/flicker/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "FlickerLibTest"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/Assertions.java b/libraries/flicker/src/com/android/server/wm/flicker/Assertions.java
deleted file mode 100644
index 0723221..0000000
--- a/libraries/flicker/src/com/android/server/wm/flicker/Assertions.java
+++ /dev/null
@@ -1,196 +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;
-
-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
- * results. Assertions are functions that are applied over a single trace entry and returns a result
- * which includes a detailed reason if the assertion fails.
- */
-public class Assertions {
-    /**
-     * Checks assertion on a single trace entry.
-     *
-     * @param <T> trace entry type to perform the assertion on.
-     */
-    @FunctionalInterface
-    public interface TraceAssertion<T> extends Function<T, Result> {
-        /**
-         * Returns an assertion that represents the logical negation of this assertion.
-         *
-         * @return a assertion that represents the logical negation of this assertion
-         */
-        default TraceAssertion<T> negate() {
-            return (T t) -> apply(t).negate();
-        }
-    }
-
-    /** Checks assertion on a single layers trace entry. */
-    @FunctionalInterface
-    public interface LayersTraceAssertion extends TraceAssertion<LayersTrace.Entry> {}
-
-    /**
-     * 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> 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. */
-    public static class Result {
-        public static final String NEGATION_PREFIX = "!";
-        public final boolean success;
-        public final long timestamp;
-        public final String assertionName;
-        public final String reason;
-
-        public Result(boolean success, long timestamp, String assertionName, String reason) {
-            this.success = success;
-            this.timestamp = timestamp;
-            this.assertionName = assertionName;
-            this.reason = reason;
-        }
-
-        public Result(boolean success, String reason) {
-            this.success = success;
-            this.reason = reason;
-            this.assertionName = "";
-            this.timestamp = 0;
-        }
-
-        /** Returns the negated {@code Result} and adds a negation prefix to the assertion name. */
-        public Result negate() {
-            String negatedAssertionName;
-            if (this.assertionName.startsWith(NEGATION_PREFIX)) {
-                negatedAssertionName = this.assertionName.substring(NEGATION_PREFIX.length() + 1);
-            } else {
-                negatedAssertionName = NEGATION_PREFIX + this.assertionName;
-            }
-            return new Result(!this.success, this.timestamp, negatedAssertionName, this.reason);
-        }
-
-        public boolean passed() {
-            return this.success;
-        }
-
-        public boolean failed() {
-            return !this.success;
-        }
-
-        @Override
-        public String toString() {
-            return "Timestamp: "
-                    + prettyTimestamp(timestamp)
-                    + "\nAssertion: "
-                    + assertionName
-                    + "\nReason:   "
-                    + reason;
-        }
-
-        private String prettyTimestamp(long timestamp_ns) {
-            StringBuilder prettyTimestamp = new StringBuilder();
-            TimeUnit[] timeUnits = {
-                TimeUnit.DAYS,
-                TimeUnit.HOURS,
-                TimeUnit.MINUTES,
-                TimeUnit.SECONDS,
-                TimeUnit.MILLISECONDS
-            };
-            String[] unitSuffixes = {"d", "h", "m", "s", "ms"};
-
-            for (int i = 0; i < timeUnits.length; i++) {
-                long convertedTime = timeUnits[i].convert(timestamp_ns, TimeUnit.NANOSECONDS);
-                timestamp_ns -= TimeUnit.NANOSECONDS.convert(convertedTime, timeUnits[i]);
-                prettyTimestamp.append(convertedTime).append(unitSuffixes[i]);
-            }
-
-            return prettyTimestamp.toString();
-        }
-    }
-}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/AssertionsChecker.java b/libraries/flicker/src/com/android/server/wm/flicker/AssertionsChecker.java
deleted file mode 100644
index 68cc812..0000000
--- a/libraries/flicker/src/com/android/server/wm/flicker/AssertionsChecker.java
+++ /dev/null
@@ -1,231 +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;
-
-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 java.util.ArrayList;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.stream.Collectors;
-
-/**
- * Captures some of the common logic in {@link LayersTraceSubject} and {@link WmTraceSubject} used
- * to filter trace entries and combine multiple assertions.
- *
- * @param <T> trace entry type
- */
-public class AssertionsChecker<T extends ITraceEntry> {
-    private boolean mFilterEntriesByRange = false;
-    private long mFilterStartTime = 0;
-    private long mFilterEndTime = 0;
-    private boolean mSkipUntilFirstAssertion = false;
-    private AssertionOption mOption = AssertionOption.NONE;
-    private List<CompoundAssertion<T>> mAssertions = new LinkedList<>();
-
-    public void add(Assertions.TraceAssertion<T> assertion, String 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) {
-        mFilterEntriesByRange = true;
-        mFilterStartTime = startTime;
-        mFilterEndTime = endTime;
-    }
-
-    private void setOption(AssertionOption option) {
-        if (mOption != AssertionOption.NONE && option != mOption) {
-            throw new IllegalArgumentException(
-                    "Cannot use " + mOption + " option with " + option + " option.");
-        }
-        mOption = option;
-    }
-
-    public void checkFirstEntry() {
-        setOption(AssertionOption.CHECK_FIRST_ENTRY);
-    }
-
-    public void checkLastEntry() {
-        setOption(AssertionOption.CHECK_LAST_ENTRY);
-    }
-
-    public void checkChangingAssertions() {
-        setOption(AssertionOption.CHECK_CHANGING_ASSERTIONS);
-    }
-
-    /**
-     * Filters trace entries then runs assertions returning a list of failures.
-     *
-     * @param entries list of entries to perform assertions on
-     * @return list of failed assertion results
-     */
-    public List<Result> test(List<T> entries) {
-        List<T> filteredEntries;
-
-        if (mFilterEntriesByRange) {
-            filteredEntries =
-                    entries.stream()
-                            .filter(
-                                    e ->
-                                            ((e.getTimestamp() >= mFilterStartTime)
-                                                    && (e.getTimestamp() <= mFilterEndTime)))
-                            .collect(Collectors.toList());
-        } else {
-            filteredEntries = entries;
-        }
-
-        switch (mOption) {
-            case CHECK_CHANGING_ASSERTIONS:
-                return assertChanges(filteredEntries);
-            case CHECK_FIRST_ENTRY:
-                return assertEntry(filteredEntries.get(0));
-            case CHECK_LAST_ENTRY:
-                return assertEntry(filteredEntries.get(filteredEntries.size() - 1));
-        }
-        return assertAll(filteredEntries);
-    }
-
-    /**
-     * Steps through each trace entry checking if provided assertions are true in the order they are
-     * 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 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<>();
-        int entryIndex = 0;
-        int assertionIndex = 0;
-        int lastPassedAssertionIndex = -1;
-
-        if (mAssertions.size() == 0) {
-            return failures;
-        }
-
-        while (assertionIndex < mAssertions.size() && entryIndex < entries.size()) {
-            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;
-            }
-            assertionIndex++;
-
-            if (assertionIndex == mAssertions.size()) {
-                failures.add(result);
-                break;
-            }
-        }
-
-        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).getName()
-                                + " never became false";
-                reason +=
-                        "\nPassed assertions: "
-                                + mAssertions
-                                        .stream()
-                                        .limit(assertionIndex)
-                                        .map(NamedAssertion::getName)
-                                        .collect(Collectors.joining(","));
-                reason +=
-                        "\nUntested assertions: "
-                                + mAssertions
-                                        .stream()
-                                        .skip(assertionIndex + 1)
-                                        .map(NamedAssertion::getName)
-                                        .collect(Collectors.joining(","));
-
-                Result result =
-                        new Result(
-                                false /* success */,
-                                0 /* timestamp */,
-                                "assertChanges",
-                                "Not all assertions passed." + reason);
-                failures.add(result);
-            }
-        }
-        return failures;
-    }
-
-    private List<Result> assertEntry(T entry) {
-        List<Result> failures = new ArrayList<>();
-        for (NamedAssertion<T> assertion : mAssertions) {
-            Result result = assertion.apply(entry);
-            if (result.failed()) {
-                failures.add(result);
-            }
-        }
-        return failures;
-    }
-
-    private List<Result> assertAll(List<T> entries) {
-        return mAssertions
-                .stream()
-                .flatMap(assertion -> entries.stream().map(assertion).filter(Result::failed))
-                .collect(Collectors.toList());
-    }
-
-    private enum AssertionOption {
-        NONE,
-        CHECK_CHANGING_ASSERTIONS,
-        CHECK_FIRST_ENTRY,
-        CHECK_LAST_ENTRY,
-    }
-}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/Extensions.kt b/libraries/flicker/src/com/android/server/wm/flicker/Extensions.kt
new file mode 100644
index 0000000..97b69ca
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/Extensions.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.
+ */
+
+@file:JvmName("Extensions")
+package com.android.server.wm.flicker
+
+import java.nio.file.Paths
+
+internal const val FLICKER_TAG = "FLICKER"
+
+/**
+ * Gets the default flicker output dir.
+ * By default the data is stored in /sdcard/flicker instead of
+ * using the app's internal data directory to be accessible by
+ * other components (i.e. FilePuller)
+ */
+fun getDefaultFlickerOutputDir() =
+        Paths.get("/sdcard/flicker")
+
+internal fun String.containsAny(vararg values: String): Boolean {
+    return values.isEmpty() || values.any { search -> this.contains(search) }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/Flicker.kt b/libraries/flicker/src/com/android/server/wm/flicker/Flicker.kt
new file mode 100644
index 0000000..7bb2074
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/Flicker.kt
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2021 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
+
+import android.app.Instrumentation
+import android.support.test.launcherhelper.ILauncherStrategy
+import android.util.Log
+import androidx.test.uiautomator.UiDevice
+import com.android.server.wm.flicker.assertions.AssertionData
+import com.android.server.wm.flicker.monitor.ITransitionMonitor
+import com.android.server.wm.flicker.monitor.WindowAnimationFrameStatsMonitor
+import com.android.server.wm.traces.common.layers.LayersTrace
+import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace
+import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
+import java.nio.file.Path
+
+@DslMarker
+annotation class FlickerDslMarker
+
+/**
+ * Defines the runner for the flicker tests. This component is responsible for running the flicker
+ * tests and executing assertions on the traces to check for inconsistent behaviors on
+ * [WindowManagerTrace] and [LayersTrace]
+ */
+@FlickerDslMarker
+class Flicker(
+    /**
+     * Instrumentation to run the tests
+     */
+    @JvmField val instrumentation: Instrumentation,
+    /**
+     * Test automation component used to interact with the device
+     */
+    @JvmField val device: UiDevice,
+    /**
+     * Strategy used to interact with the launcher
+     */
+    @JvmField val launcherStrategy: ILauncherStrategy,
+    /**
+     * Output directory for test results
+     */
+    @JvmField val outputDir: Path,
+    /**
+     * Test name used to store the test results
+     */
+    @JvmField val testName: String,
+    /**
+     * Number of times the test should be executed
+     */
+    @JvmField var repetitions: Int,
+    /**
+     * Monitor for janky frames, when filtering out janky runs
+     */
+    @JvmField val frameStatsMonitor: WindowAnimationFrameStatsMonitor?,
+    /**
+     * Enabled tracing monitors
+     */
+    @JvmField val traceMonitors: List<ITransitionMonitor>,
+    /**
+     * Commands to be executed before each run
+     */
+    @JvmField val testSetup: List<Flicker.() -> Any>,
+    /**
+     * Commands to be executed before the test
+     */
+    @JvmField val runSetup: List<Flicker.() -> Any>,
+    /**
+     * Commands to be executed after the test
+     */
+    @JvmField val testTeardown: List<Flicker.() -> Any>,
+    /**
+     * Commands to be executed after the run
+     */
+    @JvmField val runTeardown: List<Flicker.() -> Any>,
+    /**
+     * Test commands
+     */
+    @JvmField val transitions: List<Flicker.() -> Any>,
+    /**
+     * Runner to execute the test transitions
+     */
+    @JvmField val runner: TransitionRunner,
+    /**
+     * Helper object for WM Synchronization
+     */
+    val wmHelper: WindowManagerStateHelper
+) {
+    var result: FlickerResult? = null
+        private set
+
+    /**
+     * Executes the test.
+     *
+     * @throws IllegalStateException If cannot execute the transition
+     */
+    fun execute(): Flicker = apply {
+        val result = runner.execute(this)
+        this.result = result
+        checkIsExecuted()
+    }
+
+    /**
+     * Asserts if the transition of this flicker test has ben executed
+     */
+    private fun checkIsExecuted() {
+        if (result == null) {
+            execute()
+        }
+        val error = result?.error
+        if (error != null) {
+            throw IllegalStateException("Unable to execute transition", error)
+        }
+    }
+
+    /**
+     * Run an assertion on the trace
+     *
+     * @param assertion Assertion to run
+     * @throws AssertionError If the assertions fail or the transition crashed
+     */
+    fun checkAssertion(assertion: AssertionData) {
+        checkIsExecuted()
+        val result = result
+        requireNotNull(result)
+
+        val failures = result.checkAssertions(listOf(assertion))
+        val failureMessage = failures.joinToString("\n") { it.message }
+
+        if (failureMessage.isNotEmpty()) {
+            throw AssertionError(failureMessage)
+        }
+    }
+
+    /**
+     * Deletes the traces files for successful assertions and clears the cached runner results
+     *
+     */
+    fun clear() {
+        Log.v(FLICKER_TAG, "Cleaning up spec $testName")
+        runner.cleanUp()
+        result?.cleanUp()
+        result = null
+    }
+
+    /**
+     * Runs a set of commands and, at the end, creates a tag containing the device state
+     *
+     * @param tag Identifier for the tag to be created
+     * @param commands Commands to execute before creating the tag
+     * @throws IllegalArgumentException If [tag] cannot be converted to a valid filename
+     */
+    fun withTag(tag: String, commands: Flicker.() -> Any) {
+        commands()
+        runner.createTag(this, tag)
+    }
+
+    fun createTag(tag: String) {
+        withTag(tag) {}
+    }
+
+    override fun toString(): String {
+        return this.testName
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/FlickerBlockJUnit4ClassRunner.kt b/libraries/flicker/src/com/android/server/wm/flicker/FlickerBlockJUnit4ClassRunner.kt
new file mode 100644
index 0000000..b43eab9
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/FlickerBlockJUnit4ClassRunner.kt
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2021 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
+
+import android.util.Log
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import org.junit.internal.runners.statements.RunAfters
+import org.junit.runner.notification.RunNotifier
+import org.junit.runners.model.FrameworkMethod
+import org.junit.runners.model.Statement
+import org.junit.runners.parameterized.BlockJUnit4ClassRunnerWithParameters
+import org.junit.runners.parameterized.TestWithParameters
+import java.lang.reflect.Modifier
+
+/**
+ * Implements the JUnit 4 standard test case class model, parsing from a flicker DSL.
+ *
+ * Supports both assertions in {@link org.junit.Test} and assertions defined in the DSL
+ */
+class FlickerBlockJUnit4ClassRunner @JvmOverloads constructor(
+    test: TestWithParameters,
+    private val parameters: Array<Any> = test.parameters.toTypedArray(),
+    private val flickerTestParameter: FlickerTestParameter? =
+        parameters.filterIsInstance(FlickerTestParameter::class.java).firstOrNull()
+) : BlockJUnit4ClassRunnerWithParameters(test) {
+    private var flickerMethod: FrameworkMethod? = null
+
+    /**
+     * {@inheritDoc}
+     */
+    override fun classBlock(notifier: RunNotifier): Statement {
+        val statement = childrenInvoker(notifier)
+        val cleanUpMethod = getFlickerCleanUpMethod()
+        val frameworkMethod = FrameworkMethod(cleanUpMethod)
+        return RunAfters(statement, listOf(frameworkMethod), flickerTestParameter)
+    }
+
+    /**
+     * Adds to `errors` for each method annotated with `@Test`that
+     * is not a public, void instance method with no arguments.
+     */
+    fun validateFlickerObject(errors: MutableList<Throwable>) {
+        val methods = testClass.getAnnotatedMethods(FlickerBuilderProvider::class.java)
+
+        if (methods.isEmpty() || methods.size > 1) {
+            val prefix = if (methods.isEmpty()) "One" else "Only one"
+            errors.add(Exception("$prefix object should be annotated with @FlickerObject"))
+        } else {
+            val method = methods.first()
+
+            if (Modifier.isStatic(method.method.modifiers)) {
+                errors.add(Exception("Method ${method.name}() show not be static"))
+            }
+            if (!Modifier.isPublic(method.method.modifiers)) {
+                errors.add(Exception("Method ${method.name}() should be public"))
+            }
+            if (method.returnType != FlickerBuilder::class.java) {
+                errors.add(Exception("Method ${method.name}() should return a " +
+                    "${FlickerBuilder::class.java.simpleName} object"))
+            }
+            if (method.method.parameterTypes.isNotEmpty()) {
+                errors.add(Exception("Method ${method.name} should have no parameters"))
+            }
+        }
+
+        if (errors.isEmpty()) {
+            flickerMethod = methods.first()
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    override fun createTest(): Any {
+        val test = super.createTest()
+        if (flickerTestParameter?.internalFlicker == null) {
+            Log.v(FLICKER_TAG, "Flicker object is not yet initialized")
+            injectFlickerOnTestParams(test)
+        }
+        return test
+    }
+
+    /**
+     * Builds a flicker object and assigns it to the test parameters
+     */
+    private fun injectFlickerOnTestParams(test: Any) {
+        val flickerTestParameter = flickerTestParameter
+        val flickerMethod = flickerMethod
+        if (flickerTestParameter != null && flickerMethod != null) {
+            Log.v(FLICKER_TAG, "Creating flicker object and adding it to test parameter")
+            val builder = flickerMethod.invokeExplosively(test) as FlickerBuilder
+            val testName = "${test::class.java.simpleName}_${flickerTestParameter.name}"
+            val flicker = builder.apply {
+                withTestName { testName }
+                repeat { flickerTestParameter.config.repetitions }
+            }.build(TransitionRunnerWithRules(flickerTestParameter.config))
+            flickerTestParameter.internalFlicker = flicker
+        } else {
+            Log.v(FLICKER_TAG, "Missing flicker builder provider method")
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    override fun validateInstanceMethods(errors: MutableList<Throwable>) {
+        validateFlickerObject(errors)
+        super.validateInstanceMethods(errors)
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    override fun validateConstructor(errors: MutableList<Throwable>) {
+        super.validateConstructor(errors)
+
+        if (errors.isEmpty()) {
+            // should have only one constructor, otherwise parent
+            // validator will create an exception
+            val ctor = testClass.javaClass.constructors.first()
+            if (ctor.parameterTypes.none { it == FlickerTestParameter::class.java }) {
+                errors.add(Exception("Constructor should have a parameter of type " +
+                    FlickerTestParameter::class.java.simpleName))
+            }
+        }
+    }
+
+    /**
+     * Obtains the method to clean up a flicker object cache,
+     * necessary to release memory after a configuration is executed
+     */
+    private fun getFlickerCleanUpMethod() = FlickerTestParameter::class.java.getMethod("clear")
+}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/FlickerBuilderProvider.kt b/libraries/flicker/src/com/android/server/wm/flicker/FlickerBuilderProvider.kt
new file mode 100644
index 0000000..acc0ff4
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/FlickerBuilderProvider.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2021 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
+
+@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
+@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER,
+    AnnotationTarget.PROPERTY_SETTER)
+annotation class FlickerBuilderProvider
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/FlickerParametersRunnerFactory.kt b/libraries/flicker/src/com/android/server/wm/flicker/FlickerParametersRunnerFactory.kt
new file mode 100644
index 0000000..2c50313
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/FlickerParametersRunnerFactory.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2021 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
+
+import org.junit.runner.Runner
+import org.junit.runners.parameterized.ParametersRunnerFactory
+import org.junit.runners.parameterized.TestWithParameters
+
+/**
+ * A {@code FlickerRunnerFactory} creates a runner for a single
+ * {@link TestWithParameters}. Parses and executes assertions from a flicker DSL
+ */
+class FlickerParametersRunnerFactory : ParametersRunnerFactory {
+    override fun createRunnerForTestWithParameters(test: TestWithParameters): Runner {
+        return FlickerBlockJUnit4ClassRunner(test)
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/FlickerResult.kt b/libraries/flicker/src/com/android/server/wm/flicker/FlickerResult.kt
new file mode 100644
index 0000000..006a153
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/FlickerResult.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2021 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
+
+import com.android.server.wm.flicker.assertions.AssertionData
+import com.android.server.wm.flicker.assertions.FlickerAssertionError
+import com.google.common.truth.Truth
+
+/**
+ * Result of a flicker run, including transitions, errors and create tags
+ */
+data class FlickerResult(
+    /**
+     * Result of each transition run
+     */
+    @JvmField val runs: List<FlickerRunResult> = listOf(),
+    /**
+     * List of test created during the execution
+     */
+    @JvmField val tags: Set<String> = setOf(),
+    /**
+     * Error which happened during the transition
+     */
+    @JvmField val error: Throwable? = null
+) {
+    /**
+     * List of failures during assertion
+     */
+    private val failures: MutableList<FlickerAssertionError> = mutableListOf()
+
+    /**
+     * Asserts if the transition of this flicker test has ben executed
+     */
+    private fun checkIsExecuted() {
+        Truth.assertWithMessage(error?.message).that(error).isNull()
+        Truth.assertWithMessage("Transition was not executed").that(runs).isNotEmpty()
+    }
+
+    /**
+     * Run the assertions on the trace
+     *
+     * @throws AssertionError If the assertions fail or the transition crashed
+     */
+    internal fun checkAssertions(
+        assertions: List<AssertionData>
+    ): List<FlickerAssertionError> {
+        checkIsExecuted()
+        val currFailures: List<FlickerAssertionError> = runs.flatMap { run ->
+            assertions.filter { it.tag == run.assertionTag }.mapNotNull { assertion ->
+                try {
+                    assertion.checkAssertion(run)
+                    null
+                } catch (error: Throwable) {
+                    FlickerAssertionError(error, assertion, run)
+                }
+            }
+        }
+        failures.addAll(currFailures)
+        return currFailures
+    }
+
+    /**
+     * Remove from the device the trace files associated with passed runs.
+     *
+     * If an test fails, or if the transition crashes, retain all traces related to
+     * that run.
+     */
+    fun cleanUp() {
+        if (error != null) {
+            return
+        }
+        runs.forEach {
+            if (it.canDelete(failures)) {
+                it.cleanUp()
+            }
+        }
+    }
+
+    fun isEmpty(): Boolean = error == null && runs.isEmpty()
+
+    fun isNotEmpty(): Boolean = !isEmpty()
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/FlickerRunResult.kt b/libraries/flicker/src/com/android/server/wm/flicker/FlickerRunResult.kt
new file mode 100644
index 0000000..5508a53
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/FlickerRunResult.kt
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2021 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
+
+import android.util.Log
+import androidx.annotation.VisibleForTesting
+import com.android.server.wm.flicker.assertions.FlickerAssertionError
+import com.android.server.wm.flicker.assertions.FlickerSubject
+import com.android.server.wm.flicker.dsl.AssertionTag
+import com.android.server.wm.flicker.traces.eventlog.EventLogSubject
+import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace
+import com.android.server.wm.flicker.traces.eventlog.FocusEvent
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
+import com.android.server.wm.traces.common.layers.LayerTraceEntry
+import com.android.server.wm.traces.common.layers.LayersTrace
+import com.android.server.wm.traces.common.windowmanager.WindowManagerState
+import com.android.server.wm.traces.parser.layers.LayersTraceParser
+import com.android.server.wm.traces.parser.windowmanager.WindowManagerTraceParser
+import java.io.IOException
+import java.nio.file.Files
+import java.nio.file.Path
+
+/**
+ * Defines the result of a flicker run
+ */
+class FlickerRunResult private constructor(
+    /**
+     * Run identifier
+     */
+    @JvmField val iteration: Int,
+    /**
+     * Path to the trace files associated with the result (incl. screen recording)
+     */
+    @JvmField val traceFiles: List<Path>,
+    /**
+     * Determines which assertions to run (e.g., start, end, all, or a custom tag)
+     */
+    @JvmField var assertionTag: String,
+    /**
+     * Truth subject that corresponds to a [WindowManagerTrace] or [WindowManagerState]
+     */
+    private val wmSubject: FlickerSubject?,
+    /**
+     * Truth subject that corresponds to a [LayersTrace] or [LayerTraceEntry]
+     */
+    private val layersSubject: FlickerSubject?,
+    /**
+     * Truth subject that corresponds to a list of [FocusEvent]
+     */
+    @VisibleForTesting
+    val eventLogSubject: EventLogSubject?
+) {
+    fun getSubjects(): List<FlickerSubject> {
+        val result = mutableListOf<FlickerSubject>()
+
+        wmSubject?.run { result.add(this.clone()) }
+        layersSubject?.run { result.add(this.clone()) }
+        eventLogSubject?.run { result.add(this.clone()) }
+
+        return result
+    }
+
+    private fun Path?.tryDelete() {
+        try {
+            this?.let { Files.deleteIfExists(it) }
+        } catch (e: IOException) {
+            Log.e(FLICKER_TAG, "Unable do delete $this", e)
+        }
+    }
+
+    fun canDelete(failures: List<FlickerAssertionError>): Boolean {
+        return failures.flatMap { it.traceFiles }.none { failureTrace ->
+            this.traceFiles.any { it == failureTrace }
+        }
+    }
+
+    /**
+     * Delete the trace files collected
+     */
+    fun cleanUp() {
+        this.traceFiles.forEach { it.tryDelete() }
+    }
+
+    class Builder @JvmOverloads constructor(private val iteration: Int = 0) {
+        /**
+         * Path to the WindowManager trace file, if collected
+         */
+        var wmTraceFile: Path? = null
+
+        /**
+         * Path to the SurfaceFlinger trace file, if collected
+         */
+        var layersTraceFile: Path? = null
+
+        /**
+         * Path to screen recording of the run, if collected
+         */
+        var screenRecording: Path? = null
+
+        /**
+         * List of focus events, if collected
+         */
+        var eventLog: List<FocusEvent>? = null
+
+        private fun getTraceFiles() = listOfNotNull(wmTraceFile, layersTraceFile, screenRecording)
+
+        private fun buildResult(
+            assertionTag: String,
+            wmSubject: FlickerSubject?,
+            layersSubject: FlickerSubject?,
+            eventLogSubject: EventLogSubject? = null
+        ): FlickerRunResult {
+            return FlickerRunResult(iteration,
+                getTraceFiles(),
+                assertionTag,
+                wmSubject,
+                layersSubject,
+                eventLogSubject
+            )
+        }
+
+        /**
+         * Builds a new [FlickerRunResult] for a trace
+         *
+         * @param assertionTag Tag to associate with the result
+         * @param wmTrace WindowManager trace
+         * @param layersTrace Layers trace
+         */
+        fun buildStateResult(
+            assertionTag: String,
+            wmTrace: WindowManagerTrace?,
+            layersTrace: LayersTrace?
+        ): FlickerRunResult {
+            val wmSubject = wmTrace?.let { WindowManagerTraceSubject.assertThat(it).first() }
+            val layersSubject = layersTrace?.let { LayersTraceSubject.assertThat(it).first() }
+            return buildResult(assertionTag, wmSubject, layersSubject)
+        }
+
+        @VisibleForTesting
+        fun buildEventLogResult(): FlickerRunResult {
+            val events = eventLog ?: emptyList()
+            return buildResult(
+                AssertionTag.ALL,
+                wmSubject = null,
+                layersSubject = null,
+                eventLogSubject = EventLogSubject.assertThat(events)
+            )
+        }
+
+        @VisibleForTesting
+        fun buildTraceResults(): List<FlickerRunResult> {
+            var wmTrace: WindowManagerTrace? = null
+            var layersTrace: LayersTrace? = null
+
+            if (wmTrace == null && wmTraceFile != null) {
+                Log.v(FLICKER_TAG, "Parsing WM trace")
+                wmTrace = wmTraceFile?.let {
+                    val traceData = Files.readAllBytes(it)
+                    WindowManagerTraceParser.parseFromTrace(traceData)
+                }
+            }
+
+            if (layersTrace == null && layersTraceFile != null) {
+                Log.v(FLICKER_TAG, "Parsing Layers trace")
+                layersTrace = layersTraceFile?.let {
+                    val traceData = Files.readAllBytes(it)
+                    LayersTraceParser.parseFromTrace(traceData)
+                }
+            }
+
+            val wmSubject = wmTrace?.let { WindowManagerTraceSubject.assertThat(it) }
+            val layersSubject = layersTrace?.let { LayersTraceSubject.assertThat(it) }
+
+            val traceResult = buildResult(
+                AssertionTag.ALL, wmSubject, layersSubject)
+            val initialStateResult = buildResult(
+                AssertionTag.START, wmSubject?.first(), layersSubject?.first())
+            val finalStateResult = buildResult(
+                AssertionTag.END, wmSubject?.last(), layersSubject?.last())
+
+            return listOf(initialStateResult, finalStateResult, traceResult)
+        }
+
+        fun buildAll(): List<FlickerRunResult> {
+            val result = buildTraceResults().toMutableList()
+            if (eventLog != null) {
+                result.add(buildEventLogResult())
+            }
+
+            return result
+        }
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/FlickerTestParameter.kt b/libraries/flicker/src/com/android/server/wm/flicker/FlickerTestParameter.kt
new file mode 100644
index 0000000..771088f
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/FlickerTestParameter.kt
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2021 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
+
+import android.view.Surface
+import android.view.WindowManagerPolicyConstants
+import com.android.server.wm.flicker.assertions.AssertionData
+import com.android.server.wm.flicker.assertions.FlickerSubject
+import com.android.server.wm.flicker.dsl.AssertionTag
+import com.android.server.wm.flicker.traces.eventlog.EventLogSubject
+import com.android.server.wm.flicker.traces.layers.LayerTraceEntrySubject
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerStateSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
+
+/**
+ * Specification of a flicker test for JUnit ParameterizedRunner class
+ */
+data class FlickerTestParameter(
+    @JvmField val config: MutableMap<String, Any?>,
+    @JvmField val name: String = defaultName(config)
+) {
+    internal var internalFlicker: Flicker? = null
+    internal val flicker: Flicker get() = internalFlicker ?: error("Flicker not initialized")
+    val isRotated: Boolean
+        get() = config.startRotation == Surface.ROTATION_90 ||
+            config.startRotation == Surface.ROTATION_270
+
+    fun clear() {
+        internalFlicker?.clear()
+    }
+
+    fun assertWmStart(assertion: WindowManagerStateSubject.() -> Unit) {
+        val assertionData = buildWmStartAssertion(assertion)
+        this.flicker.checkAssertion(assertionData)
+    }
+
+    fun assertWmEnd(assertion: WindowManagerStateSubject.() -> Unit) {
+        val assertionData = buildWmEndAssertion(assertion)
+        this.flicker.checkAssertion(assertionData)
+    }
+
+    fun assertWm(assertion: WindowManagerTraceSubject.() -> Unit) {
+        val assertionData = buildWMAssertion(assertion)
+        this.flicker.checkAssertion(assertionData)
+    }
+
+    fun assertWmTag(tag: String, assertion: WindowManagerStateSubject.() -> Unit) {
+        val assertionData = buildWMTagAssertion(tag, assertion)
+        this.flicker.checkAssertion(assertionData)
+    }
+
+    fun assertLayersStart(assertion: LayerTraceEntrySubject.() -> Unit) {
+        val assertionData = buildLayersStartAssertion(assertion)
+        this.flicker.checkAssertion(assertionData)
+    }
+
+    fun assertLayersEnd(assertion: LayerTraceEntrySubject.() -> Unit) {
+        val assertionData = buildLayersEndAssertion(assertion)
+        this.flicker.checkAssertion(assertionData)
+    }
+
+    fun assertLayers(assertion: LayersTraceSubject.() -> Unit) {
+        val assertionData = buildLayersAssertion(assertion)
+        this.flicker.checkAssertion(assertionData)
+    }
+
+    fun assertLayersTag(tag: String, assertion: LayerTraceEntrySubject.() -> Unit) {
+        val assertionData = buildLayersTagAssertion(tag, assertion)
+        this.flicker.checkAssertion(assertionData)
+    }
+
+    fun assertEventLog(assertion: EventLogSubject.() -> Unit) {
+        val assertionData = buildEventLogAssertion(assertion)
+        this.flicker.checkAssertion(assertionData)
+    }
+
+    override fun toString(): String = name
+
+    companion object {
+        fun defaultName(config: Map<String, Any?>) = buildString {
+            append(config.startRotationName)
+            if (config.endRotation != config.startRotation) {
+                append("_${config.endRotationName}")
+            }
+            if (config.navBarMode.isNotEmpty()) {
+                append("_${config.navBarModeName}")
+            }
+        }
+
+        @JvmStatic
+        fun buildWmStartAssertion(assertion: WindowManagerStateSubject.() -> Unit): AssertionData =
+            AssertionData(tag = AssertionTag.START,
+                expectedSubjectClass = WindowManagerStateSubject::class,
+                assertion = assertion as FlickerSubject.() -> Unit)
+
+        @JvmStatic
+        fun buildWmEndAssertion(assertion: WindowManagerStateSubject.() -> Unit): AssertionData =
+            AssertionData(tag = AssertionTag.END,
+                expectedSubjectClass = WindowManagerStateSubject::class,
+                assertion = assertion as FlickerSubject.() -> Unit)
+
+        @JvmStatic
+        fun buildWMAssertion(assertion: WindowManagerTraceSubject.() -> Unit): AssertionData {
+            val closedAssertion: WindowManagerTraceSubject.() -> Unit = {
+                assertion()
+                this.forAllEntries()
+            }
+            return AssertionData(tag = AssertionTag.ALL,
+                expectedSubjectClass = WindowManagerTraceSubject::class,
+                assertion = closedAssertion as FlickerSubject.() -> Unit)
+        }
+
+        @JvmStatic
+        fun buildWMTagAssertion(
+            tag: String,
+            assertion: WindowManagerStateSubject.() -> Unit
+        ): AssertionData = AssertionData(tag = tag,
+            expectedSubjectClass = WindowManagerStateSubject::class,
+            assertion = assertion as FlickerSubject.() -> Unit)
+
+        @JvmStatic
+        fun buildLayersStartAssertion(assertion: LayerTraceEntrySubject.() -> Unit): AssertionData =
+            AssertionData(tag = AssertionTag.START,
+                expectedSubjectClass = LayerTraceEntrySubject::class,
+                assertion = assertion as FlickerSubject.() -> Unit)
+
+        @JvmStatic
+        fun buildLayersEndAssertion(assertion: LayerTraceEntrySubject.() -> Unit): AssertionData =
+            AssertionData(tag = AssertionTag.END,
+                expectedSubjectClass = LayerTraceEntrySubject::class,
+                assertion = assertion as FlickerSubject.() -> Unit)
+
+        @JvmStatic
+        fun buildLayersAssertion(assertion: LayersTraceSubject.() -> Unit): AssertionData {
+            val closedAssertion: LayersTraceSubject.() -> Unit = {
+                assertion()
+                this.forAllEntries()
+            }
+
+            return AssertionData(tag = AssertionTag.ALL,
+                expectedSubjectClass = LayersTraceSubject::class,
+                assertion = closedAssertion as FlickerSubject.() -> Unit)
+        }
+
+        @JvmStatic
+        fun buildLayersTagAssertion(
+            tag: String,
+            assertion: LayerTraceEntrySubject.() -> Unit
+        ): AssertionData = AssertionData(tag = tag,
+            expectedSubjectClass = LayerTraceEntrySubject::class,
+            assertion = assertion as FlickerSubject.() -> Unit)
+
+        @JvmStatic
+        fun buildEventLogAssertion(assertion: EventLogSubject.() -> Unit): AssertionData =
+            AssertionData(tag = AssertionTag.ALL,
+                expectedSubjectClass = EventLogSubject::class,
+                assertion = assertion as FlickerSubject.() -> Unit)
+    }
+}
+
+internal const val REPETITIONS = "repetitions"
+internal const val START_ROTATION = "startRotation"
+internal const val END_ROTATION = "endRotation"
+internal const val NAV_BAR_MODE = "navBarMode"
+
+val Map<String, Any?>.repetitions: Int
+    get() = this.getOrDefault(REPETITIONS, 1) as Int
+
+val Map<String, Any?>.startRotation: Int
+    get() = this.getOrDefault(START_ROTATION, Surface.ROTATION_0) as Int
+
+val Map<String, Any?>.endRotation: Int
+    get() = this.getOrDefault(END_ROTATION, startRotation) as Int
+
+val Map<String, Any?>.startRotationName: String
+    get() = Surface.rotationToString(startRotation)
+
+val Map<String, Any?>.endRotationName: String
+    get() = Surface.rotationToString(endRotation)
+
+val Map<String, Any?>.navBarMode: String
+    get() = this.getOrDefault(NAV_BAR_MODE,
+        WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY) as String
+
+val Map<String, Any?>.navBarModeName
+    get() = when (this.navBarMode) {
+        WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY -> "3_BUTTON_NAV"
+        WindowManagerPolicyConstants.NAV_BAR_MODE_2BUTTON_OVERLAY -> "2_BUTTON_NAV"
+        WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY -> "GESTURAL_NAV"
+        else -> "UNKNOWN_NAV_BAR_MODE(${this.navBarMode}"
+    }
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/FlickerTestParameterFactory.kt b/libraries/flicker/src/com/android/server/wm/flicker/FlickerTestParameterFactory.kt
new file mode 100644
index 0000000..5883f9b
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/FlickerTestParameterFactory.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2021 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
+
+import android.view.Surface
+import android.view.WindowManagerPolicyConstants
+
+/**
+ * Factory for creating JUnit4 compatible tests based on the flicker DSL
+ *
+ * This class recreates behavior from JUnit5 TestFactory that is not available on JUnit4
+ */
+open class FlickerTestParameterFactory {
+    /**
+     * Gets a list of test configurations.
+     *
+     * Each configurations has a start orientation.
+     */
+    @JvmOverloads
+    open fun getConfigNonRotationTests(
+        supportedRotations: List<Int> = listOf(Surface.ROTATION_0, Surface.ROTATION_90),
+        supportedNavigationModes: List<String> = listOf(
+            WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY,
+            WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY),
+        repetitions: Int = 1
+    ): List<FlickerTestParameter> {
+        return supportedNavigationModes.flatMap { navBarMode ->
+            supportedRotations.map { rotation ->
+                createParam(repetitions, navBarMode, rotation)
+            }
+        }
+    }
+
+    /**
+     * Gets a list of test configurations.
+     *
+     * Each configurations has a start and end orientation.
+     */
+    @JvmOverloads
+    open fun getConfigRotationTests(
+        supportedRotations: List<Int> = listOf(Surface.ROTATION_0, Surface.ROTATION_90),
+        supportedNavigationModes: List<String> = listOf(
+            WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY,
+            WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY),
+        repetitions: Int = 1
+    ): List<FlickerTestParameter> {
+        return supportedNavigationModes.flatMap { navBarMode ->
+            supportedRotations
+            .flatMap { start -> supportedRotations.map { end -> start to end } }
+            .filter { (start, end) -> start != end }
+            .map { (start, end) ->
+                createParam(repetitions, navBarMode, start, end)
+            }
+        }
+    }
+
+    private fun createParam(
+        repetitions: Int,
+        navBarMode: String,
+        startRotation: Int,
+        endRotation: Int = startRotation
+    ) = FlickerTestParameter(
+        mutableMapOf(
+            NAV_BAR_MODE to navBarMode,
+            START_ROTATION to startRotation,
+            END_ROTATION to endRotation,
+            REPETITIONS to repetitions
+        )
+    )
+
+    companion object {
+        private lateinit var instance: FlickerTestParameterFactory
+
+        @JvmStatic
+        fun getInstance(): FlickerTestParameterFactory {
+            if (!::instance.isInitialized) {
+                instance = FlickerTestParameterFactory()
+            }
+
+            return instance
+        }
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/ITraceEntry.java b/libraries/flicker/src/com/android/server/wm/flicker/ITraceEntry.java
deleted file mode 100644
index 0737422..0000000
--- a/libraries/flicker/src/com/android/server/wm/flicker/ITraceEntry.java
+++ /dev/null
@@ -1,23 +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;
-
-/** Common interface for Layer and WindowManager trace entries. */
-public interface ITraceEntry {
-    /** @return timestamp of current entry */
-    long getTimestamp();
-}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/LayersTrace.java b/libraries/flicker/src/com/android/server/wm/flicker/LayersTrace.java
deleted file mode 100644
index f3eee72..0000000
--- a/libraries/flicker/src/com/android/server/wm/flicker/LayersTrace.java
+++ /dev/null
@@ -1,533 +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;
-
-import androidx.annotation.Nullable;
-
-import android.graphics.Rect;
-import android.surfaceflinger.nano.Layers.LayerProto;
-import android.surfaceflinger.nano.Layers.RectProto;
-import android.surfaceflinger.nano.Layers.RegionProto;
-import android.surfaceflinger.nano.Layerstrace.LayersTraceFileProto;
-import android.surfaceflinger.nano.Layerstrace.LayersTraceProto;
-import android.util.SparseArray;
-
-import com.android.server.wm.flicker.Assertions.Result;
-
-import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.Optional;
-import java.util.function.Consumer;
-import java.util.stream.Collectors;
-
-/**
- * Contains a collection of parsed Layers trace entries and assertions to apply over a single entry.
- *
- * <p>Each entry is parsed into a list of {@link LayersTrace.Entry} objects.
- */
-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, String sourceChecksum) {
-        this.mEntries = entries;
-        this.mSource = source;
-        this.mSourceChecksum = 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
-     * @param orphanLayerCallback a callback to handle any unexpected orphan layers
-     */
-    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 {
-            fileProto = LayersTraceFileProto.parseFrom(data);
-        } catch (Exception e) {
-            throw new RuntimeException(e);
-        }
-        for (LayersTraceProto traceProto : fileProto.entry) {
-            Entry entry =
-                    Entry.fromFlattenedLayers(
-                            traceProto.elapsedRealtimeNanos, traceProto.layers.layers,
-                            orphanLayerCallback);
-            entries.add(entry);
-        }
-        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 */);
-    }
-
-    /**
-     * 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) {
-        return parseFrom(data, source, null /* sourceChecksum */, null /* 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
-     */
-    public static LayersTrace parseFrom(byte[] data) {
-        return parseFrom(data, null /* source */);
-    }
-
-    public List<Entry> getEntries() {
-        return mEntries;
-    }
-
-    public Entry getEntry(long timestamp) {
-        Optional<Entry> entry =
-                mEntries.stream().filter(e -> e.getTimestamp() == timestamp).findFirst();
-        if (!entry.isPresent()) {
-            throw new RuntimeException("Entry does not exist for timestamp " + timestamp);
-        }
-        return entry.get();
-    }
-
-    public Optional<Path> getSource() {
-        return Optional.ofNullable(mSource);
-    }
-
-    public String getSourceChecksum() {
-        return mSourceChecksum;
-    }
-
-    /** Represents a single Layer trace entry. */
-    public static class Entry implements ITraceEntry {
-        private long mTimestamp;
-        private List<Layer> mRootLayers; // hierarchical representation of layers
-        private List<Layer> mFlattenedLayers = null;
-
-        private Entry(long timestamp, List<Layer> rootLayers) {
-            this.mTimestamp = timestamp;
-            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) {
-            SparseArray<Layer> layerMap = new SparseArray<>();
-            ArrayList<Layer> orphans = new ArrayList<>();
-            for (LayerProto proto : protos) {
-                int id = proto.id;
-                int parentId = proto.parent;
-
-                Layer newLayer = layerMap.get(id);
-                if (newLayer == null) {
-                    newLayer = new Layer(proto);
-                    layerMap.append(id, newLayer);
-                } else if (newLayer.mProto != null) {
-                    throw new RuntimeException("Duplicate layer id found:" + id);
-                } else {
-                    newLayer.mProto = proto;
-                    orphans.remove(newLayer);
-                }
-
-                // add parent placeholder
-                if (layerMap.get(parentId) == null) {
-                    Layer orphanLayer = new Layer(null);
-                    layerMap.append(parentId, orphanLayer);
-                    orphans.add(orphanLayer);
-                }
-                layerMap.get(parentId).addChild(newLayer);
-                newLayer.addParent(layerMap.get(parentId));
-            }
-
-            // 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 existence of an orphan layer
-                            orphanLayerCallback.accept(orphan);
-                            return;
-                        }
-                        String childNodes =
-                                orphan.mChildren
-                                        .stream()
-                                        .map(node -> Integer.toString(node.getId()))
-                                        .collect(Collectors.joining(", "));
-                        int orphanId = orphan.mChildren.get(0).getParentId();
-                        throw new RuntimeException(
-                                "Failed to parse layers trace. Found orphan layers with parent "
-                                        + "layer id:"
-                                        + orphanId
-                                        + " : "
-                                        + childNodes);
-                    });
-
-            return new Entry(timestamp, rootLayer.mChildren);
-        }
-
-        /** Extracts {@link Rect} from {@link RectProto}. */
-        private static Rect extract(RectProto proto) {
-            return new Rect(proto.left, proto.top, proto.right, proto.bottom);
-        }
-
-        /**
-         * Extracts {@link Rect} from {@link RegionProto} by returning a rect that encompasses all
-         * the rects making up the region.
-         */
-        private static Rect extract(RegionProto regionProto) {
-            Rect region = new Rect();
-            for (RectProto proto : regionProto.rect) {
-                region.union(proto.left, proto.top, proto.right, proto.bottom);
-            }
-            return region;
-        }
-
-        /** Checks if a region specified by {@code testRect} is covered by all visible layers. */
-        public Result coversRegion(Rect testRect) {
-            String assertionName = "coversRegion";
-            Collection<Layer> layers = asFlattenedLayers();
-
-            for (int x = testRect.left; x < testRect.right; x++) {
-                for (int y = testRect.top; y < testRect.bottom; y++) {
-                    boolean emptyRegionFound = true;
-                    for (Layer layer : layers) {
-                        if (layer.isInvisible() || layer.isHiddenByParent()) {
-                            continue;
-                        }
-                        for (RectProto rectProto : layer.mProto.visibleRegion.rect) {
-                            Rect r = extract(rectProto);
-                            if (r.contains(x, y)) {
-                                y = r.bottom;
-                                emptyRegionFound = false;
-                            }
-                        }
-                    }
-                    if (emptyRegionFound) {
-                        String reason =
-                                "Region to test: "
-                                        + testRect
-                                        + "\nfirst empty point: "
-                                        + x
-                                        + ", "
-                                        + y;
-                        reason += "\nvisible regions:";
-                        for (Layer layer : layers) {
-                            if (layer.isInvisible() || layer.isHiddenByParent()) {
-                                continue;
-                            }
-                            Rect r = extract(layer.mProto.visibleRegion);
-                            reason += "\n" + layer.mProto.name + r.toString();
-                        }
-                        return new Result(
-                                false /* success */, this.mTimestamp, assertionName, reason);
-                    }
-                }
-            }
-            String info = "Region covered: " + testRect;
-            return new Result(true /* success */, this.mTimestamp, assertionName, info);
-        }
-
-        /**
-         * Checks if a layer with name {@code layerName} has a visible region {@code
-         * expectedVisibleRegion}.
-         */
-        public Result hasVisibleRegion(String layerName, Rect expectedVisibleRegion) {
-            String assertionName = "hasVisibleRegion";
-            String reason = "Could not find " + layerName;
-            for (Layer layer : asFlattenedLayers()) {
-                if (layer.mProto.name.contains(layerName)) {
-                    if (layer.isHiddenByParent()) {
-                        reason = layer.getHiddenByParentReason();
-                        continue;
-                    }
-                    if (layer.isInvisible()) {
-                        reason = layer.getVisibilityReason();
-                        continue;
-                    }
-                    Rect visibleRegion = extract(layer.mProto.visibleRegion);
-                    if (visibleRegion.equals(expectedVisibleRegion)) {
-                        return new Result(
-                                true /* success */,
-                                this.mTimestamp,
-                                assertionName,
-                                layer.mProto.name + "has visible region " + expectedVisibleRegion);
-                    }
-                    reason =
-                            layer.mProto.name
-                                    + " has visible region:"
-                                    + visibleRegion
-                                    + " "
-                                    + "expected:"
-                                    + expectedVisibleRegion;
-                }
-            }
-            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";
-            String reason = "Could not find " + layerName;
-            for (Layer layer : asFlattenedLayers()) {
-                if (layer.mProto.name.contains(layerName)) {
-                    if (layer.isHiddenByParent()) {
-                        reason = layer.getHiddenByParentReason();
-                        continue;
-                    }
-                    if (layer.isInvisible()) {
-                        reason = layer.getVisibilityReason();
-                        continue;
-                    }
-                    return new Result(
-                            true /* success */,
-                            this.mTimestamp,
-                            assertionName,
-                            layer.mProto.name + " is visible");
-                }
-            }
-            return new Result(false /* success */, this.mTimestamp, assertionName, reason);
-        }
-
-        @Override
-        public long getTimestamp() {
-            return mTimestamp;
-        }
-
-        public List<Layer> getRootLayers() {
-            return mRootLayers;
-        }
-
-        /** Returns all layers as a flattened list using a depth first traversal. */
-        public List<Layer> asFlattenedLayers() {
-            if (mFlattenedLayers == null) {
-                mFlattenedLayers = new ArrayList<>();
-                ArrayList<Layer> pendingLayers = new ArrayList<>(this.mRootLayers);
-                while (!pendingLayers.isEmpty()) {
-                    Layer layer = pendingLayers.remove(0);
-                    mFlattenedLayers.add(layer);
-                    pendingLayers.addAll(layer.mChildren);
-                }
-            }
-            return mFlattenedLayers;
-        }
-
-        public Rect getVisibleBounds(String layerName) {
-            List<Layer> layers = asFlattenedLayers();
-            for (Layer layer : layers) {
-                if (layer.mProto.name.contains(layerName) && layer.isVisible()) {
-                    return extract(layer.mProto.visibleRegion);
-                }
-            }
-            return new Rect(0, 0, 0, 0);
-        }
-    }
-
-    /** Represents a single layer with links to its parent and child layers. */
-    public static class Layer {
-        @Nullable public LayerProto mProto;
-        public List<Layer> mChildren;
-        @Nullable public Layer mParent = null;
-
-        private Layer(LayerProto proto) {
-            this.mProto = proto;
-            this.mChildren = new ArrayList<>();
-        }
-
-        private void addChild(Layer childLayer) {
-            this.mChildren.add(childLayer);
-        }
-
-        private void addParent(Layer parentLayer) {
-            this.mParent = parentLayer;
-        }
-
-        public int getId() {
-            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
-                    || this.mProto.activeBuffer.width == 0;
-        }
-
-        public boolean isVisibleRegionEmpty() {
-            if (this.mProto.visibleRegion == null) {
-                return true;
-            }
-            Rect visibleRect = Entry.extract(this.mProto.visibleRegion);
-            return visibleRect.height() == 0 || visibleRect.width() == 0;
-        }
-
-        public boolean isHidden() {
-            return (this.mProto.flags & /* FLAG_HIDDEN */ 0x1) != 0x0;
-        }
-
-        public boolean isVisible() {
-            return (!isActiveBufferEmpty() || isColorLayer())
-                    && !isHidden()
-                    && this.mProto.color != null
-                    && this.mProto.color.a > 0
-                    && !isVisibleRegionEmpty();
-        }
-
-        public boolean isColorLayer() {
-            return this.mProto.type.equals("ColorLayer");
-        }
-
-        public boolean isRootLayer() {
-            return mParent != null && mParent.mProto == null;
-        }
-
-        public boolean isInvisible() {
-            return !isVisible();
-        }
-
-        public boolean isHiddenByParent() {
-            return !isRootLayer() && (mParent.isHidden() || mParent.isHiddenByParent());
-        }
-
-        public String getHiddenByParentReason() {
-            String reason = "Layer " + mProto.name;
-            if (isHiddenByParent()) {
-                reason += " is hidden by parent: " + mParent.mProto.name;
-            } else {
-                reason += " is not hidden by parent: " + mParent.mProto.name;
-            }
-            return reason;
-        }
-
-        public String getVisibilityReason() {
-            String reason = "Layer " + mProto.name;
-            if (isVisible()) {
-                reason += " is visible:";
-            } else {
-                reason += " is invisible:";
-                if (this.mProto.activeBuffer == null) {
-                    reason += " activeBuffer=null";
-                } else if (this.mProto.activeBuffer.height == 0) {
-                    reason += " activeBuffer.height=0";
-                } else if (this.mProto.activeBuffer.width == 0) {
-                    reason += " activeBuffer.width=0";
-                }
-                if (!isColorLayer()) {
-                    reason += " type != ColorLayer";
-                }
-                if (isHidden()) {
-                    reason += " flags=" + this.mProto.flags + " (FLAG_HIDDEN set)";
-                }
-                if (this.mProto.color == null || this.mProto.color.a == 0) {
-                    reason += " color.a=0";
-                }
-                if (isVisibleRegionEmpty()) {
-                    reason += " visible region is empty";
-                }
-            }
-            return reason;
-        }
-    }
-}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/LayersTraceSubject.java b/libraries/flicker/src/com/android/server/wm/flicker/LayersTraceSubject.java
deleted file mode 100644
index 008ac70..0000000
--- a/libraries/flicker/src/com/android/server/wm/flicker/LayersTraceSubject.java
+++ /dev/null
@@ -1,202 +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;
-
-import static com.google.common.truth.Truth.assertAbout;
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.graphics.Rect;
-import android.util.Log;
-
-import androidx.annotation.Nullable;
-
-import com.android.server.wm.flicker.Assertions.Result;
-import com.android.server.wm.flicker.LayersTrace.Entry;
-import com.android.server.wm.flicker.TransitionRunner.TransitionResult;
-
-import com.google.common.truth.FailureMetadata;
-import com.google.common.truth.Subject;
-
-import java.util.List;
-import java.util.Locale;
-import java.util.function.Consumer;
-import java.util.stream.Collectors;
-
-/** Truth subject for {@link LayersTrace} objects. */
-public class LayersTraceSubject extends Subject {
-    public static final String TAG = "FLICKER";
-
-    // Boiler-plate Subject.Factory for LayersTraceSubject
-    private static final Subject.Factory<LayersTraceSubject, LayersTrace> FACTORY =
-            LayersTraceSubject::new;
-
-    private AssertionsChecker<Entry> mChecker = new AssertionsChecker<>();
-    private LayersTrace mTrace;
-    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);
-        mTrace = subject;
-    }
-
-    // User-defined entry point
-    public static LayersTraceSubject assertThat(@Nullable LayersTrace entry) {
-        return assertAbout(FACTORY).that(entry);
-    }
-
-    // User-defined entry point. Ignores orphaned layers because of b/141326137
-    public static LayersTraceSubject assertThat(@Nullable TransitionResult result) {
-        Consumer<LayersTrace.Layer> orphanLayerCallback =
-                layer ->
-                        Log.w(
-                                TAG,
-                                String.format(
-                                        Locale.getDefault(), "Ignoring orphaned layer %s", layer));
-
-        return assertThat(result, orphanLayerCallback);
-    }
-
-    // User-defined entry point
-    public static LayersTraceSubject assertThat(@Nullable TransitionResult result,
-            Consumer<LayersTrace.Layer> orphanLayerCallback) {
-        LayersTrace entries =
-                LayersTrace.parseFrom(
-                        result.getLayersTrace(),
-                        result.getLayersTracePath(),
-                        result.getLayersTraceChecksum(),
-                        orphanLayerCallback);
-        return assertWithMessage(result.toString()).about(FACTORY).that(entries);
-    }
-
-    // Static method for getting the subject factory (for use with assertAbout())
-    public static Subject.Factory<LayersTraceSubject, LayersTrace> entries() {
-        return FACTORY;
-    }
-
-    public void forAllEntries() {
-        test();
-    }
-
-    public void forRange(long startTime, long endTime) {
-        mChecker.filterByRange(startTime, endTime);
-        test();
-    }
-
-    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 (mTrace.getEntries().isEmpty()) {
-            failWithActual("No entries found.", mTrace);
-        }
-        mChecker.checkFirstEntry();
-        test();
-    }
-
-    public void atTheEnd() {
-        if (mTrace.getEntries().isEmpty()) {
-            failWithActual("No entries found.", mTrace);
-        }
-        mChecker.checkLastEntry();
-        test();
-    }
-
-    private void test() {
-        List<Result> failures = mChecker.test(mTrace.getEntries());
-        if (!failures.isEmpty()) {
-            String failureLogs =
-                    failures.stream().map(Result::toString).collect(Collectors.joining("\n"));
-            String tracePath = "";
-            if (mTrace.getSource().isPresent()) {
-                tracePath =
-                        "\nLayers Trace can be found in: "
-                                + mTrace.getSource().get().toAbsolutePath()
-                                + "\nChecksum: "
-                                + mTrace.getSourceChecksum()
-                                + "\n";
-            }
-            failWithActual(tracePath + failureLogs, mTrace);
-        }
-    }
-
-    public LayersTraceSubject coversRegion(Rect rect) {
-        addAssertion(entry -> entry.coversRegion(rect), "coversRegion(" + rect + ")");
-        return this;
-    }
-
-    public LayersTraceSubject hasVisibleRegion(String layerName, Rect size) {
-        addAssertion(
-                entry -> entry.hasVisibleRegion(layerName, size),
-                "hasVisibleRegion(" + layerName + size + ")");
-        return this;
-    }
-
-    public LayersTraceSubject hasNotLayer(String layerName) {
-        addAssertion(entry -> entry.exists(layerName).negate(), "hasNotLayer(" + layerName + ")");
-        return this;
-    }
-
-    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/StandardAppHelper.java b/libraries/flicker/src/com/android/server/wm/flicker/StandardAppHelper.java
deleted file mode 100644
index 85ce307..0000000
--- a/libraries/flicker/src/com/android/server/wm/flicker/StandardAppHelper.java
+++ /dev/null
@@ -1,51 +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;
-
-import android.app.Instrumentation;
-import android.platform.helpers.AbstractStandardAppHelper;
-
-/**
- * Class to take advantage of {@code IAppHelper} interface so the same test can be run against first
- * party and third party apps.
- */
-public class StandardAppHelper extends AbstractStandardAppHelper {
-    private final String mPackageName;
-    private final String mLauncherName;
-
-    public StandardAppHelper(Instrumentation instr, String packageName, String launcherName) {
-        super(instr);
-        mPackageName = packageName;
-        mLauncherName = launcherName;
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public String getPackage() {
-        return mPackageName;
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public String getLauncherName() {
-        return mLauncherName;
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public void dismissInitialDialogs() {}
-}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/TransitionRunner.java b/libraries/flicker/src/com/android/server/wm/flicker/TransitionRunner.java
deleted file mode 100644
index 0b74a6c..0000000
--- a/libraries/flicker/src/com/android/server/wm/flicker/TransitionRunner.java
+++ /dev/null
@@ -1,472 +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;
-
-import static com.android.server.wm.flicker.monitor.TransitionMonitor.OUTPUT_DIR;
-
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-import androidx.test.InstrumentationRegistry;
-
-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 java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.ArrayList;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Locale;
-
-/**
- * Builds and runs UI transitions capturing test artifacts.
- *
- * <p>User can compose a transition from simpler steps, specifying setup and teardown steps. During
- * a transition, Layers trace, WindowManager trace, screen recordings and window animation frame
- * stats can be captured.
- *
- * <pre>
- * Transition builder options:
- *  {@link TransitionBuilder#run(Runnable)} run transition under test. Monitors will be started
- *  before the transition and stopped after the transition is completed.
- *  {@link TransitionBuilder#repeat(int)} repeat transitions under test multiple times recording
- *  result for each run.
- *  {@link TransitionBuilder#withTag(String)} specify a string identifier used to prefix logs and
- *  artifacts generated.
- *  {@link TransitionBuilder#runBeforeAll(Runnable)} run setup transitions once before all other
- *  transition are run to set up an initial state on device.
- *  {@link TransitionBuilder#runBefore(Runnable)} run setup transitions before each test transition
- *  run.
- *  {@link TransitionBuilder#runAfter(Runnable)} run teardown transitions after each test
- *  transition.
- *  {@link TransitionBuilder#runAfter(Runnable)} run teardown transitions once after all
- *  other transition  are run.
- *  {@link TransitionBuilder#includeJankyRuns()} disables {@link WindowAnimationFrameStatsMonitor}
- *  to monitor janky frames. If janky frames are detected, then the test run is skipped. This
- *  monitor is enabled by default.
- *  {@link TransitionBuilder#skipLayersTrace()} disables {@link LayersTraceMonitor} used to
- *  capture Layers trace during a transition. This monitor is enabled by default.
- *  {@link TransitionBuilder#skipWindowManagerTrace()} disables {@link WindowManagerTraceMonitor}
- *  used to capture WindowManager trace during a transition. This monitor is enabled by
- *  default.
- *  {@link TransitionBuilder#recordAllRuns()} records the screen contents and saves it to a file.
- *  All the runs including setup and teardown transitions are included in the recording. This
- *  monitor is used for debugging purposes.
- *  {@link TransitionBuilder#recordEachRun()} records the screen contents during test transitions
- *  and saves it to a file for each run. This monitor is used for debugging purposes.
- *
- * Example transition to capture WindowManager and Layers trace when opening a test app:
- * {@code
- * TransitionRunner.newBuilder()
- *      .withTag("OpenTestAppFast")
- *      .runBeforeAll(UiAutomationLib::wakeUp)
- *      .runBeforeAll(UiAutomationLib::UnlockDevice)
- *      .runBeforeAll(UiAutomationLib::openTestApp)
- *      .runBefore(UiAutomationLib::closeTestApp)
- *      .run(UiAutomationLib::openTestApp)
- *      .runAfterAll(UiAutomationLib::closeTestApp)
- *      .repeat(5)
- *      .build()
- *      .run();
- * }
- * </pre>
- */
-public class TransitionRunner {
-    private static final String TAG = "FLICKER";
-    private final ScreenRecorder mScreenRecorder;
-    private final WindowManagerTraceMonitor mWmTraceMonitor;
-    private final LayersTraceMonitor mLayersTraceMonitor;
-    private final WindowAnimationFrameStatsMonitor mFrameStatsMonitor;
-
-    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;
-    private final List<Runnable> mAfters;
-    private final List<Runnable> mAfterAlls;
-
-    private final int mIterations;
-    private final String mTestTag;
-
-    @Nullable private List<TransitionResult> mResults = null;
-
-    private TransitionRunner(TransitionBuilder builder) {
-        mScreenRecorder = builder.mScreenRecorder;
-        mWmTraceMonitor = builder.mWmTraceMonitor;
-        mLayersTraceMonitor = builder.mLayersTraceMonitor;
-        mFrameStatsMonitor = builder.mFrameStatsMonitor;
-
-        mAllRunsMonitors = builder.mAllRunsMonitors;
-        mPerRunMonitors = builder.mPerRunMonitors;
-        mBeforeAlls = builder.mBeforeAlls;
-        mBefores = builder.mBefores;
-        mTransitions = builder.mTransitions;
-        mAfters = builder.mAfters;
-        mAfterAlls = builder.mAfterAlls;
-
-        mIterations = builder.mIterations;
-        mTestTag = builder.mTestTag;
-    }
-
-    public static TransitionBuilder newBuilder(String outputDir) {
-        return new TransitionBuilder(Paths.get(outputDir));
-    }
-
-    public static TransitionBuilder newBuilder() {
-        return new TransitionBuilder();
-    }
-
-    /**
-     * Runs the composed transition and calls monitors at the appropriate stages. If jank monitor is
-     * enabled, transitions with jank are skipped.
-     *
-     * @return itself
-     */
-    public TransitionRunner run() {
-        mResults = new ArrayList<>();
-        mAllRunsMonitors.forEach(TransitionMonitor::start);
-        mBeforeAlls.forEach(Runnable::run);
-        for (int iteration = 0; iteration < mIterations; iteration++) {
-            mBefores.forEach(Runnable::run);
-            mPerRunMonitors.forEach(TransitionMonitor::start);
-            mTransitions.forEach(Runnable::run);
-            mPerRunMonitors.forEach(TransitionMonitor::stop);
-            mAfters.forEach(Runnable::run);
-            if (runJankFree() && mFrameStatsMonitor.jankyFramesDetected()) {
-                String msg =
-                        String.format(
-                                Locale.getDefault(),
-                                "Skipping iteration %d/%d for test %s due to jank. %s",
-                                iteration,
-                                mIterations - 1,
-                                mTestTag,
-                                mFrameStatsMonitor.toString());
-                Log.e(TAG, msg);
-                continue;
-            }
-            mResults.add(saveResult(iteration));
-        }
-        mAfterAlls.forEach(Runnable::run);
-        mAllRunsMonitors.forEach(
-                monitor -> {
-                    monitor.stop();
-                    monitor.save(mTestTag);
-                });
-        return this;
-    }
-
-    /**
-     * Returns a list of transition results.
-     *
-     * @return list of transition results.
-     */
-    public List<TransitionResult> getResults() {
-        if (mResults == null) {
-            throw new IllegalStateException("Results do not exist!");
-        }
-        return mResults;
-    }
-
-    /**
-     * Deletes all transition results that are not marked for saving.
-     *
-     * @return list of transition results.
-     */
-    public void deleteResults() {
-        if (mResults == null) {
-            return;
-        }
-        mResults.stream().filter(TransitionResult::canDelete).forEach(TransitionResult::delete);
-        mResults = null;
-    }
-
-    /**
-     * Saves monitor results to file.
-     *
-     * @return object containing paths to test artifacts
-     */
-    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,
-                layerTraceChecksum,
-                windowTrace,
-                windowTraceChecksum,
-                screenCaptureVideo,
-                screenCaptureVideoChecksum);
-    }
-
-    private boolean runJankFree() {
-        return mPerRunMonitors.contains(mFrameStatsMonitor);
-    }
-
-    public String getTestTag() {
-        return mTestTag;
-    }
-
-    /** Stores paths to all test artifacts. */
-    @VisibleForTesting
-    public static class TransitionResult {
-        @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,
-                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() {
-            flaggedForSaving = true;
-        }
-
-        public boolean canDelete() {
-            return !flaggedForSaving;
-        }
-
-        public boolean layersTraceExists() {
-            return layersTrace != null && layersTrace.toFile().exists();
-        }
-
-        public byte[] getLayersTrace() {
-            try {
-                return Files.readAllBytes(this.layersTrace);
-            } catch (Exception e) {
-                throw new RuntimeException(e);
-            }
-        }
-
-        public Path getLayersTracePath() {
-            return layersTrace;
-        }
-
-        public String getLayersTraceChecksum() {
-            return layersTraceChecksum;
-        }
-
-        public boolean windowManagerTraceExists() {
-            return windowManagerTrace != null && windowManagerTrace.toFile().exists();
-        }
-
-        public byte[] getWindowManagerTrace() {
-            try {
-                return Files.readAllBytes(this.windowManagerTrace);
-            } catch (Exception e) {
-                throw new RuntimeException(e);
-            }
-        }
-
-        public Path getWindowManagerTracePath() {
-            return windowManagerTrace;
-        }
-
-        public String getWindowManagerTraceChecksum() {
-            return windowManagerTraceChecksum;
-        }
-
-        public boolean screenCaptureVideoExists() {
-            return screenCaptureVideo != null && screenCaptureVideo.toFile().exists();
-        }
-
-        public Path screenCaptureVideoPath() {
-            return screenCaptureVideo;
-        }
-
-        public String getScreenCaptureVideoChecksum() {
-            return screenCaptureVideoChecksum;
-        }
-
-        public void delete() {
-            if (layersTraceExists()) layersTrace.toFile().delete();
-            if (windowManagerTraceExists()) windowManagerTrace.toFile().delete();
-            if (screenCaptureVideoExists()) screenCaptureVideo.toFile().delete();
-        }
-    }
-
-    /** Builds a {@link TransitionRunner} instance. */
-    public static class TransitionBuilder {
-        private ScreenRecorder mScreenRecorder;
-        private WindowManagerTraceMonitor mWmTraceMonitor;
-        private LayersTraceMonitor mLayersTraceMonitor;
-        private WindowAnimationFrameStatsMonitor mFrameStatsMonitor;
-
-        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<>();
-        private List<Runnable> mAfters = new LinkedList<>();
-        private List<Runnable> mAfterAlls = new LinkedList<>();
-
-        private boolean mRunJankFree = true;
-        private boolean mCaptureWindowManagerTrace = true;
-        private boolean mCaptureLayersTrace = true;
-        private boolean mRecordEachRun = false;
-        private int mIterations = 1;
-        private String mTestTag = "";
-
-        private boolean mRecordAllRuns = false;
-
-        private TransitionBuilder(@NonNull Path outputDir) {
-            mScreenRecorder = new ScreenRecorder();
-            mWmTraceMonitor = new WindowManagerTraceMonitor(outputDir);
-            mLayersTraceMonitor = new LayersTraceMonitor(outputDir);
-            mFrameStatsMonitor =
-                    new WindowAnimationFrameStatsMonitor(
-                            InstrumentationRegistry.getInstrumentation());
-        }
-
-        public TransitionBuilder() {
-            this(OUTPUT_DIR);
-        }
-
-        public TransitionRunner build() {
-            if (mCaptureWindowManagerTrace) {
-                mPerRunMonitors.add(mWmTraceMonitor);
-            }
-
-            if (mCaptureLayersTrace) {
-                mPerRunMonitors.add(mLayersTraceMonitor);
-            }
-
-            if (mRunJankFree) {
-                mPerRunMonitors.add(mFrameStatsMonitor);
-            }
-
-            if (mRecordAllRuns) {
-                mAllRunsMonitors.add(mScreenRecorder);
-            }
-
-            if (mRecordEachRun) {
-                mPerRunMonitors.add(mScreenRecorder);
-            }
-
-            return new TransitionRunner(this);
-        }
-
-        public TransitionBuilder runBeforeAll(Runnable runnable) {
-            mBeforeAlls.add(runnable);
-            return this;
-        }
-
-        public TransitionBuilder runBefore(Runnable runnable) {
-            mBefores.add(runnable);
-            return this;
-        }
-
-        public TransitionBuilder run(Runnable runnable) {
-            mTransitions.add(runnable);
-            return this;
-        }
-
-        public TransitionBuilder runAfter(Runnable runnable) {
-            mAfters.add(runnable);
-            return this;
-        }
-
-        public TransitionBuilder runAfterAll(Runnable runnable) {
-            mAfterAlls.add(runnable);
-            return this;
-        }
-
-        public TransitionBuilder repeat(int iterations) {
-            mIterations = iterations;
-            return this;
-        }
-
-        public TransitionBuilder skipWindowManagerTrace() {
-            mCaptureWindowManagerTrace = false;
-            return this;
-        }
-
-        public TransitionBuilder skipLayersTrace() {
-            mCaptureLayersTrace = false;
-            return this;
-        }
-
-        public TransitionBuilder includeJankyRuns() {
-            mRunJankFree = false;
-            return this;
-        }
-
-        public TransitionBuilder recordEachRun() {
-            if (mRecordAllRuns) {
-                throw new IllegalArgumentException("Invalid option with recordAllRuns");
-            }
-            mRecordEachRun = true;
-            return this;
-        }
-
-        public TransitionBuilder recordAllRuns() {
-            if (mRecordEachRun) {
-                throw new IllegalArgumentException("Invalid option with recordEachRun");
-            }
-            mRecordAllRuns = true;
-            return this;
-        }
-
-        public TransitionBuilder withTag(String testTag) {
-            if (testTag.contains(" ")) {
-                throw new IllegalArgumentException(
-                        "The test tag can not contain spaces since it "
-                                + "is a part of the file name");
-            }
-            mTestTag = testTag;
-            return this;
-        }
-    }
-}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/TransitionRunner.kt b/libraries/flicker/src/com/android/server/wm/flicker/TransitionRunner.kt
new file mode 100644
index 0000000..3f63df7
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/TransitionRunner.kt
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2021 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
+
+import android.util.Log
+import com.android.server.wm.flicker.monitor.ITransitionMonitor
+import com.android.server.wm.traces.parser.DeviceStateDump
+import com.android.server.wm.traces.parser.getCurrentState
+import java.io.IOException
+import java.nio.file.Files
+
+/**
+ * Runner to execute the transitions of a flicker test
+ *
+ * The commands are executed in the following order:
+ * 1) [Flicker.testSetup]
+ * 2) [Flicker.runSetup
+ * 3) Start monitors
+ * 4) [Flicker.transitions]
+ * 5) Stop monitors
+ * 6) [Flicker.runTeardown]
+ * 7) [Flicker.testTeardown]
+ *
+ * If the tests were already executed, reuse the previous results
+ *
+ */
+open class TransitionRunner {
+    /**
+     * Iteration identifier during test run
+     */
+    private var iteration = 0
+    private val tags = mutableSetOf<String>()
+    private var tagsResults = mutableListOf<FlickerRunResult>()
+
+    /**
+     * Executes the setup, transitions and teardown defined in [flicker]
+     *
+     * @param flicker test specification
+     * @throws IllegalArgumentException If the transitions are empty or repetitions is set to 0
+     */
+    open fun execute(flicker: Flicker): FlickerResult {
+        check(flicker)
+        return run(flicker)
+    }
+
+    /**
+     * Validate the [flicker] test specification before executing the transitions
+     *
+     * @param flicker test specification
+     * @throws IllegalArgumentException If the transitions are empty or repetitions is set to 0
+     */
+    protected fun check(flicker: Flicker) {
+        require(flicker.transitions.isNotEmpty()) {
+            "A flicker test must include transitions to run" }
+        require(flicker.repetitions > 0) {
+            "Number of repetitions must be greater than 0" }
+    }
+
+    open fun cleanUp() {
+        tags.clear()
+        tagsResults.clear()
+    }
+
+    /**
+     * Runs the actual setup, transitions and teardown defined in [flicker]
+     *
+     * @param flicker test specification
+     */
+    internal open fun run(flicker: Flicker): FlickerResult {
+        val runs = mutableListOf<FlickerRunResult>()
+        var executionError: Throwable? = null
+        try {
+            try {
+                flicker.testSetup.forEach { it.invoke(flicker) }
+                for (iteration in 0 until flicker.repetitions) {
+                    try {
+                        flicker.runSetup.forEach { it.invoke(flicker) }
+                        flicker.traceMonitors.forEach { it.start() }
+                        flicker.frameStatsMonitor?.run { start() }
+                        flicker.transitions.forEach { it.invoke(flicker) }
+                    } finally {
+                        flicker.traceMonitors.forEach { it.tryStop() }
+                        flicker.frameStatsMonitor?.run { tryStop() }
+                        flicker.runTeardown.forEach { it.invoke(flicker) }
+                    }
+                    if (flicker.frameStatsMonitor?.jankyFramesDetected() == true) {
+                        Log.e(FLICKER_TAG, "Skipping iteration " +
+                            "$iteration/${flicker.repetitions - 1} " +
+                            "for test ${flicker.testName} due to jank. $flicker.frameStatsMonitor")
+                        continue
+                    }
+                }
+            } finally {
+                val runResults = saveResult(flicker, iteration)
+                runs.addAll(runResults)
+                flicker.testTeardown.forEach { it.invoke(flicker) }
+            }
+        } catch (e: Throwable) {
+            executionError = e
+        }
+
+        runs.addAll(tagsResults)
+        val result = FlickerResult(runs.toList(), tags.toSet(), executionError)
+        cleanUp()
+        return result
+    }
+
+    private fun saveResult(flicker: Flicker, iteration: Int): List<FlickerRunResult> {
+        val resultBuilder = FlickerRunResult.Builder(iteration)
+        flicker.traceMonitors.forEach {
+            it.save(flicker.testName, iteration, resultBuilder)
+        }
+
+        return resultBuilder.buildAll()
+    }
+
+    private fun ITransitionMonitor.tryStop() {
+        this.run {
+            try {
+                stop()
+            } catch (e: Exception) {
+                Log.e(FLICKER_TAG, "Unable to stop $this", e)
+            }
+        }
+    }
+
+    private fun getTaggedFilePath(flicker: Flicker, tag: String, file: String) =
+        "${flicker.testName}_${iteration}_${tag}_$file"
+
+    /**
+     * Captures a snapshot of the device state and associates it with a new tag.
+     *
+     * This tag can be used to make assertions about the state of the device when the
+     * snapshot is collected.
+     *
+     * [tag] is used as part of the trace file name, thus, only valid letters and digits
+     * can be used
+     *
+     * @param flicker test specification
+     * @throws IllegalArgumentException If [tag] contains invalid characters
+     */
+    open fun createTag(flicker: Flicker, tag: String) {
+        require(!tag.contains(" ")) {
+            "The test tag $tag can not contain spaces since it is a part of the file name"
+        }
+        tags.add(tag)
+
+        val deviceStateBytes = getCurrentState(flicker.instrumentation.uiAutomation)
+        val deviceState = DeviceStateDump.fromDump(deviceStateBytes.first, deviceStateBytes.second)
+        try {
+            val wmTraceFile = flicker.outputDir.resolve(
+                getTaggedFilePath(flicker, tag, "wm_trace"))
+            Files.write(wmTraceFile, deviceStateBytes.first)
+
+            val layersTraceFile = flicker.outputDir.resolve(
+                getTaggedFilePath(flicker, tag, "layers_trace"))
+            Files.write(layersTraceFile, deviceStateBytes.second)
+
+            val builder = FlickerRunResult.Builder(iteration)
+            builder.wmTraceFile = wmTraceFile
+            builder.layersTraceFile = layersTraceFile
+
+            val result = builder.buildStateResult(
+                tag,
+                deviceState.wmTrace,
+                deviceState.layersTrace
+            )
+            tagsResults.add(result)
+        } catch (e: IOException) {
+            throw RuntimeException("Unable to create trace file: ${e.message}", e)
+        }
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/TransitionRunnerCached.kt b/libraries/flicker/src/com/android/server/wm/flicker/TransitionRunnerCached.kt
new file mode 100644
index 0000000..6cab2d1
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/TransitionRunnerCached.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2021 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
+
+/**
+ * Execute the transitions of a flicker test and caches the results.
+ *
+ * Return cached results instead of re-executing the transitions if possible.
+ *
+ * @param runner Actual runner to execute the test
+ */
+class TransitionRunnerCached @JvmOverloads constructor(
+    private val runner: TransitionRunner = TransitionRunner()
+) : TransitionRunner() {
+    private var result: FlickerResult? = null
+
+    /**
+     * {@inheritDoc}
+     */
+    override fun run(flicker: Flicker): FlickerResult {
+        if (result?.isEmpty() != false) {
+            result = runner.run(flicker)
+        }
+
+        return result ?: error("Result should not be empty")
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    override fun createTag(flicker: Flicker, tag: String) {
+        runner.createTag(flicker, tag)
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    override fun cleanUp() {
+        super.cleanUp()
+        result?.cleanUp()
+        result = null
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/TransitionRunnerWithRules.kt b/libraries/flicker/src/com/android/server/wm/flicker/TransitionRunnerWithRules.kt
new file mode 100644
index 0000000..fff00c5
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/TransitionRunnerWithRules.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2021 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
+
+import android.platform.test.rule.NavigationModeRule
+import android.platform.test.rule.PressHomeRule
+import android.platform.test.rule.UnlockScreenRule
+import com.android.server.wm.flicker.rules.ChangeDisplayOrientationRule
+import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule
+import org.junit.rules.RuleChain
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * Execute the transitions of a flicker test using JUnit rules and statements.
+ *
+ * Allow for easier reuse of test rules
+ */
+class TransitionRunnerWithRules(private val testConfig: Map<String, Any?>) : TransitionRunner() {
+    private var result: FlickerResult? = null
+
+    private fun buildDefaultSetupRules(): RuleChain {
+        return RuleChain.outerRule(ChangeDisplayOrientationRule(testConfig.startRotation))
+            .around(RemoveAllTasksButHomeRule())
+            .around(NavigationModeRule(testConfig.navBarMode))
+            .around(PressHomeRule())
+            .around(UnlockScreenRule())
+    }
+
+    private fun buildTransitionRule(flicker: Flicker): Statement {
+        return object : Statement() {
+                override fun evaluate() {
+                    result = runTransition(flicker)
+                }
+            }
+    }
+
+    private fun runTransition(flicker: Flicker): FlickerResult {
+        return super.run(flicker)
+    }
+
+    private fun buildTransitionChain(flicker: Flicker): Statement {
+        val setupRules = buildDefaultSetupRules()
+        val transitionRule = buildTransitionRule(flicker)
+        return setupRules.apply(transitionRule, Description.EMPTY)
+    }
+
+    override fun cleanUp() {
+        super.cleanUp()
+        result = null
+    }
+
+    /**
+     * Runs the actual setup, transitions and teardown defined in [flicker]
+     *
+     * @param flicker test specification
+     */
+    override fun run(flicker: Flicker): FlickerResult {
+        try {
+            val transitionChain = buildTransitionChain(flicker)
+            transitionChain.evaluate()
+            return result ?: error("Transition did not run")
+        } finally {
+            cleanUp()
+        }
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/WindowManagerTrace.java b/libraries/flicker/src/com/android/server/wm/flicker/WindowManagerTrace.java
deleted file mode 100644
index 1399a09..0000000
--- a/libraries/flicker/src/com/android/server/wm/flicker/WindowManagerTrace.java
+++ /dev/null
@@ -1,264 +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;
-
-import androidx.annotation.Nullable;
-
-import com.android.server.wm.flicker.Assertions.Result;
-import com.android.server.wm.nano.ActivityRecordProto;
-import com.android.server.wm.nano.TaskProto;
-import com.android.server.wm.nano.WindowManagerTraceFileProto;
-import com.android.server.wm.nano.WindowManagerTraceProto;
-import com.android.server.wm.nano.WindowStateProto;
-import com.android.server.wm.nano.WindowTokenProto;
-
-import com.google.protobuf.nano.InvalidProtocolBufferNanoException;
-
-import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Optional;
-
-/**
- * Contains a collection of parsed WindowManager trace entries and assertions to apply over a single
- * entry.
- *
- * <p>Each entry is parsed into a list of {@link WindowManagerTrace.Entry} objects.
- */
-public class WindowManagerTrace {
-    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, String sourceChecksum) {
-        this.mEntries = entries;
-        this.mSource = source;
-        this.mSourceChecksum = sourceChecksum;
-    }
-
-    /**
-     * Parses {@code WindowManagerTraceFileProto} from {@code data} and uses the proto to generates
-     * a list of trace entries.
-     *
-     * @param data binary proto data
-     * @param source Path to source of data for additional debug information
-     */
-    public static WindowManagerTrace parseFrom(byte[] data, Path source, String checksum) {
-        List<Entry> entries = new ArrayList<>();
-
-        WindowManagerTraceFileProto fileProto;
-        try {
-            fileProto = WindowManagerTraceFileProto.parseFrom(data);
-        } catch (InvalidProtocolBufferNanoException e) {
-            throw new RuntimeException(e);
-        }
-        for (WindowManagerTraceProto entryProto : fileProto.entry) {
-            entries.add(new Entry(entryProto));
-        }
-        return new WindowManagerTrace(entries, source, checksum);
-    }
-
-    public static WindowManagerTrace parseFrom(byte[] data) {
-        return parseFrom(data, null /* source */, null /* checksum */);
-    }
-
-    public List<Entry> getEntries() {
-        return mEntries;
-    }
-
-    public Entry getEntry(long timestamp) {
-        Optional<Entry> entry =
-                mEntries.stream().filter(e -> e.getTimestamp() == timestamp).findFirst();
-        if (!entry.isPresent()) {
-            throw new RuntimeException("Entry does not exist for timestamp " + timestamp);
-        }
-        return entry.get();
-    }
-
-    public Optional<Path> getSource() {
-        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;
-
-        public Entry(WindowManagerTraceProto proto) {
-            mProto = proto;
-        }
-
-        private static Result isWindowVisible(
-                String windowTitle, WindowTokenProto[] windowTokenProtos) {
-            boolean titleFound = false;
-            for (WindowTokenProto windowToken : windowTokenProtos) {
-                for (WindowStateProto windowState : windowToken.windows) {
-                    if (windowState.identifier.title.contains(windowTitle)) {
-                        titleFound = true;
-                        if (isVisible(windowState)) {
-                            return new Result(
-                                    true /* success */,
-                                    windowState.identifier.title + " is visible");
-                        }
-                    }
-                }
-            }
-
-            String reason;
-            if (!titleFound) {
-                reason = windowTitle + " cannot be found";
-            } else {
-                reason = windowTitle + " is invisible";
-            }
-            return new Result(false /* success */, reason);
-        }
-
-        private static boolean isVisible(WindowStateProto windowState) {
-            return windowState.windowContainer.visible;
-        }
-
-        @Override
-        public long getTimestamp() {
-            return mProto.elapsedRealtimeNanos;
-        }
-
-        /** Returns window title of the top most visible app window. */
-        private String getTopVisibleAppWindow() {
-            TaskProto[] tasks =
-                    mProto.windowManagerService
-                            .rootWindowContainer
-                            .displays[DEFAULT_DISPLAY]
-                            .tasks;
-            for (TaskProto rootTask : tasks) {
-                final String topVisible = getTopVisibleAppWindow(rootTask);
-                if (topVisible != null) return topVisible;
-            }
-
-            return "";
-        }
-
-        private String getTopVisibleAppWindow(TaskProto task) {
-            for (ActivityRecordProto activity : task.activities) {
-                for (WindowStateProto windowState : activity.windowToken.windows) {
-                    if (windowState.windowContainer.visible) {
-                        return task.activities[0].name;
-                    }
-                }
-            }
-
-            for (TaskProto childTask : task.tasks) {
-                final String topVisible = getTopVisibleAppWindow(childTask);
-                if (topVisible != null) return topVisible;
-            }
-            return null;
-        }
-
-        /** Checks if aboveAppWindow with {@code windowTitle} is visible. */
-        public Result isAboveAppWindowVisible(String windowTitle) {
-            WindowTokenProto[] windowTokenProtos =
-                    mProto.windowManagerService
-                            .rootWindowContainer
-                            .displays[DEFAULT_DISPLAY]
-                            .aboveAppWindows;
-            Result result = isWindowVisible(windowTitle, windowTokenProtos);
-            return new Result(result.success, getTimestamp(), "showsAboveAppWindow", result.reason);
-        }
-
-        /** Checks if belowAppWindow with {@code windowTitle} is visible. */
-        public Result isBelowAppWindowVisible(String windowTitle) {
-            WindowTokenProto[] windowTokenProtos =
-                    mProto.windowManagerService
-                            .rootWindowContainer
-                            .displays[DEFAULT_DISPLAY]
-                            .belowAppWindows;
-            Result result = isWindowVisible(windowTitle, windowTokenProtos);
-            return new Result(
-                    result.success, getTimestamp(), "isBelowAppWindowVisible", result.reason);
-        }
-
-        /** Checks if imeWindow with {@code windowTitle} is visible. */
-        public Result isImeWindowVisible(String windowTitle) {
-            WindowTokenProto[] windowTokenProtos =
-                    mProto.windowManagerService
-                            .rootWindowContainer
-                            .displays[DEFAULT_DISPLAY]
-                            .imeWindows;
-            Result result = isWindowVisible(windowTitle, windowTokenProtos);
-            return new Result(result.success, getTimestamp(), "isImeWindowVisible", result.reason);
-        }
-
-        /** Checks if app window with {@code windowTitle} is on top. */
-        public Result isVisibleAppWindowOnTop(String windowTitle) {
-            String topAppWindow = getTopVisibleAppWindow();
-            boolean success = topAppWindow.contains(windowTitle);
-            String reason = "wanted=" + windowTitle + " found=" + topAppWindow;
-            return new Result(success, getTimestamp(), "isAppWindowOnTop", reason);
-        }
-
-        /** Checks if app window with {@code windowTitle} is visible. */
-        public Result isAppWindowVisible(String windowTitle) {
-            final String assertionName = "isAppWindowVisible";
-            boolean[] titleFound = { false };
-            TaskProto[] tasks =
-                    mProto.windowManagerService
-                            .rootWindowContainer
-                            .displays[DEFAULT_DISPLAY]
-                            .tasks;
-            for (TaskProto task : tasks) {
-                final Result result = isAppWindowVisible(
-                        windowTitle, assertionName, titleFound, task);
-                if (result != null) return result;
-            }
-            String reason;
-            if (!titleFound[0]) {
-                reason = "Window " + windowTitle + " cannot be found";
-            } else {
-                reason = "Window " + windowTitle + " is invisible";
-            }
-            return new Result(false /* success */, getTimestamp(), assertionName, reason);
-        }
-
-        private Result isAppWindowVisible(String windowTitle, String assertionName,
-                boolean[] titleFound, TaskProto task) {
-            for (ActivityRecordProto activity : task.activities) {
-                if (activity.name.contains(windowTitle)) {
-                    titleFound[0] = true;
-                    for (WindowStateProto windowState : activity.windowToken.windows) {
-                        if (windowState.windowContainer.visible) {
-                            return new Result(
-                                    true /* success */,
-                                    getTimestamp(),
-                                    assertionName,
-                                    "Window " + activity.name + "is visible");
-                        }
-                    }
-                }
-            }
-
-            for (TaskProto childTask : task.tasks) {
-                final Result result = isAppWindowVisible(
-                        windowTitle, assertionName, titleFound, childTask);
-                if (result != null) return result;
-            }
-            return null;
-        }
-    }
-}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/WindowUtils.java b/libraries/flicker/src/com/android/server/wm/flicker/WindowUtils.java
deleted file mode 100644
index 1e77a42..0000000
--- a/libraries/flicker/src/com/android/server/wm/flicker/WindowUtils.java
+++ /dev/null
@@ -1,146 +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;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Point;
-import android.graphics.Rect;
-import android.view.Surface;
-import android.view.WindowManager;
-
-import androidx.test.InstrumentationRegistry;
-
-/** Helper functions to retrieve system window sizes and positions. */
-public class WindowUtils {
-
-    public static Rect getDisplayBounds() {
-        Point display = new Point();
-        WindowManager wm =
-                (WindowManager)
-                        InstrumentationRegistry.getContext()
-                                .getSystemService(Context.WINDOW_SERVICE);
-        wm.getDefaultDisplay().getRealSize(display);
-        return new Rect(0, 0, display.x, display.y);
-    }
-
-    private static int getCurrentRotation() {
-        WindowManager wm =
-                (WindowManager)
-                        InstrumentationRegistry.getContext()
-                                .getSystemService(Context.WINDOW_SERVICE);
-        return wm.getDefaultDisplay().getRotation();
-    }
-
-    public static Rect getDisplayBounds(int requestedRotation) {
-        Rect displayBounds = getDisplayBounds();
-        int currentDisplayRotation = getCurrentRotation();
-
-        boolean displayIsRotated =
-                (currentDisplayRotation == Surface.ROTATION_90
-                        || currentDisplayRotation == Surface.ROTATION_270);
-
-        boolean requestedDisplayIsRotated =
-                requestedRotation == Surface.ROTATION_90
-                        || requestedRotation == Surface.ROTATION_270;
-
-        // if the current orientation changes with the requested rotation,
-        // flip height and width of display bounds.
-        if (displayIsRotated != requestedDisplayIsRotated) {
-            return new Rect(0, 0, displayBounds.height(), displayBounds.width());
-        }
-
-        return new Rect(0, 0, displayBounds.width(), displayBounds.height());
-    }
-
-    public static Rect getAppPosition(int requestedRotation) {
-        Rect displayBounds = getDisplayBounds();
-        int currentDisplayRotation = getCurrentRotation();
-
-        boolean displayIsRotated =
-                currentDisplayRotation == Surface.ROTATION_90
-                        || currentDisplayRotation == Surface.ROTATION_270;
-
-        boolean requestedAppIsRotated =
-                requestedRotation == Surface.ROTATION_90
-                        || requestedRotation == Surface.ROTATION_270;
-
-        // display size will change if the display is reflected. Flip height and width of app if the
-        // requested rotation is different from the current rotation.
-        if (displayIsRotated != requestedAppIsRotated) {
-            return new Rect(0, 0, displayBounds.height(), displayBounds.width());
-        }
-
-        return new Rect(0, 0, displayBounds.width(), displayBounds.height());
-    }
-
-    public static Rect getStatusBarPosition(int requestedRotation) {
-        Resources resources = InstrumentationRegistry.getContext().getResources();
-        String resourceName;
-        Rect displayBounds = getDisplayBounds();
-        int width;
-        if (requestedRotation == Surface.ROTATION_0 || requestedRotation == Surface.ROTATION_180) {
-            resourceName = "status_bar_height_portrait";
-            width = Math.min(displayBounds.width(), displayBounds.height());
-        } else {
-            resourceName = "status_bar_height_landscape";
-            width = Math.max(displayBounds.width(), displayBounds.height());
-        }
-
-        int resourceId = resources.getIdentifier(resourceName, "dimen", "android");
-        int height = resources.getDimensionPixelSize(resourceId);
-
-        return new Rect(0, 0, width, height);
-    }
-
-    public static Rect getNavigationBarPosition(int requestedRotation) {
-        Resources resources = InstrumentationRegistry.getContext().getResources();
-        Rect displayBounds = getDisplayBounds();
-        int displayWidth = Math.min(displayBounds.width(), displayBounds.height());
-        int displayHeight = Math.max(displayBounds.width(), displayBounds.height());
-        int resourceId;
-        if (requestedRotation == Surface.ROTATION_0 || requestedRotation == Surface.ROTATION_180) {
-            resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android");
-            int height = resources.getDimensionPixelSize(resourceId);
-            return new Rect(0, displayHeight - height, displayWidth, displayHeight);
-        } else {
-            resourceId = resources.getIdentifier("navigation_bar_width", "dimen", "android");
-            int width = resources.getDimensionPixelSize(resourceId);
-            // swap display dimensions in landscape or seascape mode
-            int temp = displayHeight;
-            displayHeight = displayWidth;
-            displayWidth = temp;
-            if (requestedRotation == Surface.ROTATION_90) {
-                return new Rect(0, 0, width, displayHeight);
-            } else {
-                return new Rect(displayWidth - width, 0, displayWidth, displayHeight);
-            }
-        }
-    }
-
-    public static int getNavigationBarHeight() {
-        Resources resources = InstrumentationRegistry.getContext().getResources();
-        int resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android");
-        return resources.getDimensionPixelSize(resourceId);
-    }
-
-    public static int getDockedStackDividerInset() {
-        Resources resources = InstrumentationRegistry.getContext().getResources();
-        int resourceId = resources.getIdentifier("docked_stack_divider_insets", "dimen", "android");
-        return resources.getDimensionPixelSize(resourceId);
-    }
-}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/WmTraceSubject.java b/libraries/flicker/src/com/android/server/wm/flicker/WmTraceSubject.java
deleted file mode 100644
index eb3e67f..0000000
--- a/libraries/flicker/src/com/android/server/wm/flicker/WmTraceSubject.java
+++ /dev/null
@@ -1,228 +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;
-
-import static com.google.common.truth.Truth.assertAbout;
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import androidx.annotation.Nullable;
-
-import com.android.server.wm.flicker.Assertions.Result;
-import com.android.server.wm.flicker.TransitionRunner.TransitionResult;
-
-import com.google.common.truth.FailureMetadata;
-import com.google.common.truth.Subject;
-
-import java.nio.file.Path;
-import java.util.List;
-import java.util.Optional;
-import java.util.stream.Collectors;
-
-/** Truth subject for {@link WindowManagerTrace} objects. */
-public class WmTraceSubject extends Subject {
-    // Boiler-plate Subject.Factory for WmTraceSubject
-    private static final Subject.Factory<WmTraceSubject, WindowManagerTrace> FACTORY =
-            WmTraceSubject::new;
-
-    private AssertionsChecker<WindowManagerTrace.Entry> mChecker = new AssertionsChecker<>();
-    private WindowManagerTrace mTrace;
-    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);
-        mTrace = subject;
-    }
-
-    // User-defined entry point
-    public static WmTraceSubject assertThat(@Nullable WindowManagerTrace entry) {
-        return assertAbout(FACTORY).that(entry);
-    }
-
-    // User-defined entry point
-    public static WmTraceSubject assertThat(@Nullable TransitionResult result) {
-        WindowManagerTrace entries =
-                WindowManagerTrace.parseFrom(
-                        result.getWindowManagerTrace(),
-                        result.getWindowManagerTracePath(),
-                        result.getWindowManagerTraceChecksum());
-        return assertWithMessage(result.toString()).about(FACTORY).that(entries);
-    }
-
-    // Static method for getting the subject factory (for use with assertAbout())
-    public static Subject.Factory<WmTraceSubject, WindowManagerTrace> entries() {
-        return FACTORY;
-    }
-
-    public void forAllEntries() {
-        test();
-    }
-
-    public void forRange(long startTime, long endTime) {
-        mChecker.filterByRange(startTime, endTime);
-        test();
-    }
-
-    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 (mTrace.getEntries().isEmpty()) {
-            failWithActual("No entries found.", mTrace);
-        }
-        mChecker.checkFirstEntry();
-        test();
-    }
-
-    public void atTheEnd() {
-        if (mTrace.getEntries().isEmpty()) {
-            failWithActual("No entries found.", mTrace);
-        }
-        mChecker.checkLastEntry();
-        test();
-    }
-
-    private void test() {
-        List<Result> failures = mChecker.test(mTrace.getEntries());
-        if (!failures.isEmpty()) {
-            Optional<Path> failureTracePath = mTrace.getSource();
-            String failureLogs =
-                    failures.stream().map(Result::toString).collect(Collectors.joining("\n"));
-            String tracePath = "";
-            if (failureTracePath.isPresent()) {
-                tracePath =
-                        "\nWindowManager Trace can be found in: "
-                                + failureTracePath.get().toAbsolutePath()
-                                + "\nChecksum: "
-                                + mTrace.getSourceChecksum()
-                                + "\n";
-            }
-            failWithActual(tracePath + failureLogs, mTrace);
-        }
-    }
-
-    public WmTraceSubject showsAboveAppWindow(String partialWindowTitle) {
-        addAssertion(
-                entry -> entry.isAboveAppWindowVisible(partialWindowTitle),
-                "showsAboveAppWindow(" + partialWindowTitle + ")");
-        return this;
-    }
-
-    public WmTraceSubject hidesAboveAppWindow(String partialWindowTitle) {
-        addAssertion(
-                entry -> entry.isAboveAppWindowVisible(partialWindowTitle).negate(),
-                "hidesAboveAppWindow" + "(" + partialWindowTitle + ")");
-        return this;
-    }
-
-    public WmTraceSubject showsBelowAppWindow(String partialWindowTitle) {
-        addAssertion(
-                entry -> entry.isBelowAppWindowVisible(partialWindowTitle),
-                "showsBelowAppWindow(" + partialWindowTitle + ")");
-        return this;
-    }
-
-    public WmTraceSubject hidesBelowAppWindow(String partialWindowTitle) {
-        addAssertion(
-                entry -> entry.isBelowAppWindowVisible(partialWindowTitle).negate(),
-                "hidesBelowAppWindow" + "(" + partialWindowTitle + ")");
-        return this;
-    }
-
-    public WmTraceSubject showsImeWindow(String partialWindowTitle) {
-        addAssertion(
-                entry -> entry.isImeWindowVisible(partialWindowTitle),
-                "showsBelowAppWindow(" + partialWindowTitle + ")");
-        return this;
-    }
-
-    public WmTraceSubject hidesImeWindow(String partialWindowTitle) {
-        addAssertion(
-                entry -> entry.isImeWindowVisible(partialWindowTitle).negate(),
-                "hidesImeWindow" + "(" + partialWindowTitle + ")");
-        return this;
-    }
-
-    public WmTraceSubject showsAppWindowOnTop(String partialWindowTitle) {
-        addAssertion(
-                entry -> {
-                    Result result = entry.isAppWindowVisible(partialWindowTitle);
-                    if (result.passed()) {
-                        result = entry.isVisibleAppWindowOnTop(partialWindowTitle);
-                    }
-                    return result;
-                },
-                "showsAppWindowOnTop(" + partialWindowTitle + ")");
-        return this;
-    }
-
-    public WmTraceSubject hidesAppWindowOnTop(String partialWindowTitle) {
-        addAssertion(
-                entry -> {
-                    Result result = entry.isAppWindowVisible(partialWindowTitle).negate();
-                    if (result.failed()) {
-                        result = entry.isVisibleAppWindowOnTop(partialWindowTitle).negate();
-                    }
-                    return result;
-                },
-                "hidesAppWindowOnTop(" + partialWindowTitle + ")");
-        return this;
-    }
-
-    public WmTraceSubject showsAppWindow(String partialWindowTitle) {
-        addAssertion(
-                entry -> entry.isAppWindowVisible(partialWindowTitle),
-                "showsAppWindow(" + partialWindowTitle + ")");
-        return this;
-    }
-
-    public WmTraceSubject hidesAppWindow(String partialWindowTitle) {
-        addAssertion(
-                entry -> entry.isAppWindowVisible(partialWindowTitle).negate(),
-                "hidesAppWindow(" + partialWindowTitle + ")");
-        return this;
-    }
-}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/annotation/Group1.kt b/libraries/flicker/src/com/android/server/wm/flicker/annotation/Group1.kt
new file mode 100644
index 0000000..5258604
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/annotation/Group1.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2021 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.annotation
+
+/**
+ * The group annotations enable to run tests in parallel according to the arguments of test runner.
+ * By default, the test without group annotation are considered to be in this group.
+ */
+@Retention(AnnotationRetention.RUNTIME)
+@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
+annotation class Group1
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/annotation/Group2.kt b/libraries/flicker/src/com/android/server/wm/flicker/annotation/Group2.kt
new file mode 100644
index 0000000..79f6298
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/annotation/Group2.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2021 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.annotation
+
+/**
+ * The group annotations enable to run tests in parallel according to the arguments of test runner.
+ * By default, the test without group annotation are considered to be in this group.
+ */
+@Retention(AnnotationRetention.RUNTIME)
+@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
+annotation class Group2
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/annotation/Group3.kt b/libraries/flicker/src/com/android/server/wm/flicker/annotation/Group3.kt
new file mode 100644
index 0000000..e67fe07
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/annotation/Group3.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2021 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.annotation
+
+/**
+ * The group annotations enable to run tests in parallel according to the arguments of test runner.
+ * By default, the test without group annotation are considered to be in this group.
+ */
+@Retention(AnnotationRetention.RUNTIME)
+@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
+annotation class Group3
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/assertions/AssertionData.kt b/libraries/flicker/src/com/android/server/wm/flicker/assertions/AssertionData.kt
new file mode 100644
index 0000000..c384760
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/assertions/AssertionData.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.server.wm.flicker.assertions
+
+import com.android.server.wm.flicker.FlickerRunResult
+import kotlin.reflect.KClass
+
+/**
+ * Class containing basic data about a trace assertion for Flicker DSL
+ */
+data class AssertionData internal constructor(
+    /**
+     * Segment of the trace where the assertion will be applied (e.g., start, end).
+     */
+    @JvmField val tag: String,
+    /**
+     * Expected run result type
+     */
+    @JvmField val expectedSubjectClass: KClass<out FlickerSubject>,
+    /**
+     * Assertion command
+     */
+    @JvmField val assertion: FlickerSubject.() -> Unit
+) {
+    /**
+     * Extracts the data from the result and executes the assertion
+     *
+     * @param run Run to be asserted
+     */
+    fun checkAssertion(run: FlickerRunResult) {
+        val subjects = run.getSubjects().firstOrNull { expectedSubjectClass.isInstance(it) }
+        subjects?.run { assertion(this) }
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/assertions/Assertions.kt b/libraries/flicker/src/com/android/server/wm/flicker/assertions/Assertions.kt
new file mode 100644
index 0000000..26e4404
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/assertions/Assertions.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2021 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.assertions
+
+/**
+ * Checks assertion on a single trace entry.
+ *
+ * @param <T> trace entry type to perform the assertion on. </T>
+ */
+typealias Assertion<T> = (T) -> Unit
+
+/**
+ * Utility class to store assertions with an identifier to help generate more useful debug data
+ * when dealing with multiple assertions.
+ */
+open class NamedAssertion<T> (
+    private val assertion: Assertion<T>,
+    open val name: String
+) : Assertion<T> {
+    override fun invoke(target: T): Unit = assertion.invoke(target)
+
+    override fun toString(): String = "Assertion($name)"
+}
+
+/**
+ * Utility class to store assertions composed of multiple individual assertions
+ */
+class CompoundAssertion<T>(assertion: Assertion<T>, name: String) :
+    NamedAssertion<T>(assertion, name) {
+    private val assertions = mutableListOf<NamedAssertion<T>>()
+
+    init {
+        add(assertion, name)
+    }
+
+    override val name: String
+        get() = assertions.joinToString(" and ") { it.name }
+
+    /**
+     * Executes all [assertions] on [target]
+     */
+    override fun invoke(target: T) {
+        val failure = assertions.mapNotNull {
+            kotlin.runCatching { it.invoke(target) }.exceptionOrNull()
+        }.firstOrNull()
+        if (failure != null) {
+            throw failure
+        }
+    }
+
+    override fun toString(): String = name
+
+    /**
+     * Adds a new assertion to the list
+     */
+    fun add(assertion: Assertion<T>, name: String) {
+        assertions.add(NamedAssertion(assertion, name))
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/assertions/AssertionsChecker.kt b/libraries/flicker/src/com/android/server/wm/flicker/assertions/AssertionsChecker.kt
new file mode 100644
index 0000000..d3fb579
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/assertions/AssertionsChecker.kt
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2021 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.assertions
+
+import com.google.common.truth.Fact
+import kotlin.math.max
+
+/**
+ * Runs sequences of assertions on sequences of subjects.
+ *
+ * Starting at the first assertion and first trace entry, executes the assertions iteratively
+ * on the trace until all assertions and trace entries succeed.
+ *
+ * @param <T> trace entry type </T>
+ */
+class AssertionsChecker<T : FlickerSubject> {
+    private val assertions = mutableListOf<CompoundAssertion<T>>()
+    private var skipUntilFirstAssertion = false
+
+    fun add(name: String, assertion: Assertion<T>) {
+        assertions.add(CompoundAssertion(assertion, name))
+    }
+
+    /**
+     * Append [assertion] to the last existing set of assertions.
+     */
+    fun append(name: String, assertion: Assertion<T>) {
+        assertions.last().add(assertion, name)
+    }
+
+    /**
+     * Filters trace entries then runs assertions returning a list of failures.
+     *
+     * @param entries list of entries to perform assertions on
+     * @return list of failed assertion results
+     */
+    fun test(entries: List<T>): Unit = assertChanges(entries)
+
+    /**
+     * Steps through each trace entry checking if provided assertions are true in the order they are
+     * added. Each assertion must be true for at least a single trace entry.
+     *
+     *
+     * 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 B and
+     * back to A again.
+     *
+     *
+     * 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 fun assertChanges(entries: List<T>) {
+        if (assertions.isEmpty() || entries.isEmpty()) {
+            return
+        }
+
+        val failures = mutableListOf<Throwable>()
+        var entryIndex = 0
+        var assertionIndex = 0
+        var lastPassedAssertionIndex = -1
+        while (assertionIndex < assertions.size && entryIndex < entries.size) {
+            val currentAssertion = assertions[assertionIndex]
+            val currEntry = entries[entryIndex]
+            try {
+                currentAssertion.invoke(currEntry)
+                lastPassedAssertionIndex = assertionIndex
+                entryIndex++
+            } catch (e: Throwable) {
+                val ignoreFailure = skipUntilFirstAssertion && lastPassedAssertionIndex == -1
+                if (ignoreFailure) {
+                    entryIndex++
+                    continue
+                }
+                if (lastPassedAssertionIndex != assertionIndex) {
+                    val prevEntry = entries[max(entryIndex - 1, 0)]
+                    prevEntry.fail(e)
+                }
+                assertionIndex++
+                if (assertionIndex == assertions.size) {
+                    val prevEntry = entries[max(entryIndex - 1, 0)]
+                    prevEntry.fail(e)
+                }
+            }
+        }
+        if (lastPassedAssertionIndex == -1 && assertions.isNotEmpty() && failures.isEmpty()) {
+            entries.first().fail("Assertion never passed", assertions.first())
+        }
+
+        if (failures.isEmpty() && assertionIndex != assertions.lastIndex) {
+            val reason = listOf(
+                Fact.fact("Assertion never became false", assertions[assertionIndex]),
+                Fact.fact("Passed assertions", assertions.take(assertionIndex).joinToString(",")),
+                Fact.fact("Untested assertions",
+                    assertions.drop(assertionIndex + 1).joinToString(","))
+            )
+            entries.first().fail(reason)
+        }
+    }
+
+    /**
+     * 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
+     */
+    fun skipUntilFirstAssertion() {
+        skipUntilFirstAssertion = true
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/assertions/FlickerAssertionError.kt b/libraries/flicker/src/com/android/server/wm/flicker/assertions/FlickerAssertionError.kt
new file mode 100644
index 0000000..9061a6a
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/assertions/FlickerAssertionError.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2021 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.assertions
+
+import com.android.server.wm.flicker.FlickerRunResult
+import com.android.server.wm.flicker.traces.FlickerSubjectException
+import java.nio.file.Path
+import kotlin.AssertionError
+
+class FlickerAssertionError(
+    cause: Throwable,
+    @JvmField val assertion: AssertionData,
+    @JvmField val iteration: Int,
+    @JvmField val assertionTag: String,
+    @JvmField val traceFiles: List<Path>
+) : AssertionError(cause) {
+    constructor(cause: Throwable, assertion: AssertionData, run: FlickerRunResult)
+        : this(cause, assertion, run.iteration, run.assertionTag, run.traceFiles)
+
+    override val message: String
+        get() = buildString {
+            append("\n")
+            append("Test failed")
+            append("\n")
+            append("Iteration: ")
+            append(iteration)
+            append("\n")
+            append("Tag: ")
+            append(assertionTag)
+            append("\n")
+            append("Files: ")
+            append("\n")
+            traceFiles.forEach {
+                append("\t")
+                append(it)
+                append("\n")
+            }
+            // For subject exceptions, add the facts (layer/window/entry/etc)
+            // and the original cause of failure
+            if (cause is FlickerSubjectException) {
+                append(cause.facts)
+                append("\n")
+                cause.cause?.message?.let { append(it) }
+            } else {
+                cause?.message?.let { append(it) }
+            }
+        }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/assertions/FlickerSubject.kt b/libraries/flicker/src/com/android/server/wm/flicker/assertions/FlickerSubject.kt
new file mode 100644
index 0000000..d2f956f
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/assertions/FlickerSubject.kt
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2021 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.assertions
+
+import com.android.server.wm.flicker.traces.FlickerSubjectException
+import com.google.common.truth.Fact
+import com.google.common.truth.FailureMetadata
+import com.google.common.truth.StandardSubjectBuilder
+import com.google.common.truth.Subject
+
+/**
+ * Base subject for flicker assertions
+ */
+abstract class FlickerSubject(
+    protected val fm: FailureMetadata,
+    data: Any?
+) : Subject(fm, data) {
+    abstract val defaultFacts: String
+    abstract fun clone(): FlickerSubject
+
+    /**
+     * Fails an assertion on a subject
+     *
+     * @param reason for the failure
+     */
+    open fun fail(reason: List<Fact>): FlickerSubject = apply {
+        require(reason.isNotEmpty()) { "Failure should contain at least 1 fact" }
+        val facts = reason.drop(1).toTypedArray()
+        failWithoutActual(reason.first(), *facts)
+    }
+
+    fun fail(reason: Fact, vararg rest: Fact): FlickerSubject = apply {
+        val facts = mutableListOf(reason)
+            .also { it.addAll(rest) }
+        fail(facts)
+    }
+
+    /**
+     * Fails an assertion on a subject
+     *
+     * @param reason for the failure
+     */
+    fun fail(reason: Fact): FlickerSubject = apply {
+        fail(listOf(reason))
+    }
+
+    /**
+     * Fails an assertion on a subject
+     *
+     * @param reason for the failure
+     */
+    fun fail(reason: String): FlickerSubject = apply {
+        fail(Fact.fact("Reason", reason))
+    }
+
+    /**
+     * Fails an assertion on a subject
+     *
+     * @param reason for the failure
+     * @param value for the failure
+     */
+    fun fail(reason: String, value: Any): FlickerSubject = apply {
+        fail(Fact.fact(reason, value))
+    }
+
+    /**
+     * Fails an assertion on a subject
+     *
+     * @param reason for the failure
+     */
+    fun fail(reason: Throwable) {
+        if (reason is FlickerSubjectException) {
+            throw reason
+        } else {
+            throw FlickerSubjectException(this, reason)
+        }
+    }
+
+    /**
+     * Function to make external assertions using the subjects
+     * Necessary because check is protected and final in the Truth library
+     */
+    fun verify(message: String): StandardSubjectBuilder = check(message)
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/dsl/AssertionTag.kt b/libraries/flicker/src/com/android/server/wm/flicker/dsl/AssertionTag.kt
new file mode 100644
index 0000000..1d443bb
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/dsl/AssertionTag.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.server.wm.flicker.dsl
+
+/**
+ * Identify a trace location. By default all traces have: [START], [END] and [ALL] locations,
+ * representing inital, final and all trace states.
+ *
+ * In addition, it is possible to create custom trace locations (tags).
+ */
+object AssertionTag {
+    const val START = "start"
+    const val END = "end"
+    const val ALL = "all"
+}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/dsl/FlickerBuilder.kt b/libraries/flicker/src/com/android/server/wm/flicker/dsl/FlickerBuilder.kt
new file mode 100644
index 0000000..4330cfe
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/dsl/FlickerBuilder.kt
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2021 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.dsl
+
+import android.app.Instrumentation
+import android.support.test.launcherhelper.ILauncherStrategy
+import android.support.test.launcherhelper.LauncherStrategyFactory
+import androidx.test.uiautomator.UiDevice
+import com.android.server.wm.flicker.Flicker
+import com.android.server.wm.flicker.FlickerDslMarker
+import com.android.server.wm.flicker.TransitionRunner
+import com.android.server.wm.flicker.monitor.EventLogMonitor
+import com.android.server.wm.flicker.getDefaultFlickerOutputDir
+import com.android.server.wm.flicker.monitor.ITransitionMonitor
+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.android.server.wm.traces.common.layers.LayersTrace
+import com.android.server.wm.traces.common.layers.LayerTraceEntry
+import com.android.server.wm.traces.common.windowmanager.WindowManagerState
+import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace
+import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
+import java.nio.file.Path
+
+/**
+ * Build Flicker tests using Flicker DSL
+ */
+@FlickerDslMarker
+class FlickerBuilder private constructor(
+    private val instrumentation: Instrumentation,
+    private val launcherStrategy: ILauncherStrategy,
+    private val includeJankyRuns: Boolean,
+    private val outputDir: Path,
+    private val wmHelper: WindowManagerStateHelper,
+    private var testName: String,
+    private var iterations: Int,
+    private val setupCommands: TestCommandsBuilder,
+    private val teardownCommands: TestCommandsBuilder,
+    private val transitionCommands: MutableList<Flicker.() -> Any>,
+    val device: UiDevice,
+    private val traceMonitors: MutableList<ITransitionMonitor>
+) {
+    private val frameStatsMonitor: WindowAnimationFrameStatsMonitor? = if (includeJankyRuns) {
+        null
+    } else {
+        WindowAnimationFrameStatsMonitor(instrumentation)
+    }
+
+    @JvmOverloads
+    /**
+     * Default flicker builder constructor
+     */
+    constructor(
+        /**
+         * Instrumentation to run the tests
+         */
+        instrumentation: Instrumentation,
+        /**
+         * Strategy used to interact with the launcher
+         */
+        launcherStrategy: ILauncherStrategy = LauncherStrategyFactory
+                .getInstance(instrumentation).launcherStrategy,
+        /**
+         * Include or discard janky runs
+         */
+        includeJankyRuns: Boolean = true,
+        /**
+         * Output directory for the test results
+         */
+        outputDir: Path = getDefaultFlickerOutputDir(),
+        /**
+         * Helper object for WM Synchronization
+         */
+        wmHelper: WindowManagerStateHelper = WindowManagerStateHelper(instrumentation)
+    ) : this(
+        instrumentation,
+        launcherStrategy,
+        includeJankyRuns,
+        outputDir,
+        wmHelper,
+        testName = "",
+        iterations = 1,
+        setupCommands = TestCommandsBuilder(),
+        teardownCommands = TestCommandsBuilder(),
+        transitionCommands = mutableListOf(),
+        device = UiDevice.getInstance(instrumentation),
+        traceMonitors = mutableListOf<ITransitionMonitor>()
+            .also {
+                it.add(WindowManagerTraceMonitor(outputDir))
+                it.add(LayersTraceMonitor(outputDir))
+                it.add(ScreenRecorder(outputDir, instrumentation.targetContext))
+                it.add(EventLogMonitor())
+            }
+    )
+
+    /**
+     * Copy constructor
+     */
+    constructor(otherBuilder: FlickerBuilder): this(
+        otherBuilder.instrumentation,
+        otherBuilder.launcherStrategy,
+        otherBuilder.includeJankyRuns,
+        otherBuilder.outputDir.toAbsolutePath(),
+        otherBuilder.wmHelper,
+        otherBuilder.testName,
+        otherBuilder.iterations,
+        TestCommandsBuilder(otherBuilder.setupCommands),
+        TestCommandsBuilder(otherBuilder.teardownCommands),
+        otherBuilder.transitionCommands.toMutableList(),
+        UiDevice.getInstance(otherBuilder.instrumentation),
+        otherBuilder.traceMonitors.toMutableList()
+    )
+
+    /**
+     * Test name used to store the test results
+     *
+     * If reused throughout the test, only the last value is stored
+     */
+    fun withTestName(testName: () -> String) {
+        val name = testName()
+        require(!name.contains(" ")) {
+            "The test tag can not contain spaces since it is a part of the file name"
+        }
+        this.testName = name
+    }
+
+    /**
+     * Disable [WindowManagerTraceMonitor].
+     */
+    fun withoutWindowManagerTracing() {
+        withWindowManagerTracing { null }
+    }
+
+    /**
+     * Configure a [WindowManagerTraceMonitor] to obtain [WindowManagerTrace]
+     *
+     * By default the tracing is always active. To disable tracing return null
+     *
+     * If this tracing is disabled, the assertions for [WindowManagerTrace] and
+     * [WindowManagerState] will not be executed
+     */
+    fun withWindowManagerTracing(traceMonitor: (Path) -> WindowManagerTraceMonitor?) {
+        traceMonitors.removeIf { it is WindowManagerTraceMonitor }
+        val newMonitor = traceMonitor(outputDir)
+
+        if (newMonitor != null) {
+            traceMonitors.add(newMonitor)
+        }
+    }
+
+    /**
+     * Disable [LayersTraceMonitor].
+     */
+    fun withoutLayerTracing() {
+        withLayerTracing { null }
+    }
+
+    /**
+     * Configure a [LayersTraceMonitor] to obtain [LayersTrace].
+     *
+     * By default the tracing is always active. To disable tracing return null
+     *
+     * If this tracing is disabled, the assertions for [LayersTrace] and [LayerTraceEntry]
+     * will not be executed
+     */
+    fun withLayerTracing(traceMonitor: (Path) -> LayersTraceMonitor?) {
+        traceMonitors.removeIf { it is LayersTraceMonitor }
+        val newMonitor = traceMonitor(outputDir)
+
+        if (newMonitor != null) {
+            traceMonitors.add(newMonitor)
+        }
+    }
+
+    /**
+     * Configure a [ScreenRecorder].
+     *
+     * By default the tracing is always active. To disable tracing return null
+     */
+    fun withScreenRecorder(screenRecorder: (Path) -> ScreenRecorder?) {
+        traceMonitors.removeIf { it is ScreenRecorder }
+        val newMonitor = screenRecorder(outputDir)
+
+        if (newMonitor != null) {
+            traceMonitors.add(newMonitor)
+        }
+    }
+
+    /**
+     * Defines how many times the test run should be repeated
+     */
+    fun repeat(predicate: () -> Int) {
+        val repeat = predicate()
+        require(repeat >= 1) { "Number of repetitions should be greater or equal to 1" }
+        iterations = repeat
+    }
+
+    /**
+     * Defines the test ([TestCommandsBuilder.testCommands]) and run ([TestCommandsBuilder.runCommands])
+     * commands executed before the [transitions] to test
+     */
+    fun setup(commands: TestCommandsBuilder.() -> Unit) {
+        setupCommands.apply { commands() }
+    }
+
+    /**
+     * Defines the test ([TestCommandsBuilder.testCommands]) and run ([TestCommandsBuilder.runCommands])
+     * commands executed after the [transitions] to test
+     */
+    fun teardown(commands: TestCommandsBuilder.() -> Unit) {
+        teardownCommands.apply { commands() }
+    }
+
+    /**
+     * Defines the commands that trigger the behavior to test
+     */
+    fun transitions(command: Flicker.() -> Unit) {
+        transitionCommands.add(command)
+    }
+
+    /**
+     * Creates a new Flicker runner based on the current builder configuration
+     */
+    @JvmOverloads
+    fun build(runner: TransitionRunner = TransitionRunner()) = Flicker(
+        instrumentation,
+        device,
+        launcherStrategy,
+        outputDir,
+        testName,
+        iterations,
+        frameStatsMonitor,
+        traceMonitors,
+        setupCommands.buildTestCommands(),
+        setupCommands.buildRunCommands(),
+        teardownCommands.buildTestCommands(),
+        teardownCommands.buildRunCommands(),
+        transitionCommands,
+        runner,
+        wmHelper
+    )
+
+    /**
+     * Returns a copy of the current builder with the changes of [block] applied
+     */
+    fun copy(block: FlickerBuilder.() -> Unit) = FlickerBuilder(this).apply(block)
+}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/dsl/TestCommandsBuilder.kt b/libraries/flicker/src/com/android/server/wm/flicker/dsl/TestCommandsBuilder.kt
new file mode 100644
index 0000000..e12ab64
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/dsl/TestCommandsBuilder.kt
@@ -0,0 +1,78 @@
+/*
+ * 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.server.wm.flicker.dsl
+
+import com.android.server.wm.flicker.Flicker
+import com.android.server.wm.flicker.FlickerDslMarker
+
+/**
+ * Placeholder for test [Flicker.setup] and [Flicker.teardown] commands on the Flicker DSL
+ */
+@FlickerDslMarker
+class TestCommandsBuilder private constructor(
+    private val testCommands: MutableList<Flicker.() -> Any>,
+    private val runCommands: MutableList<Flicker.() -> Any>
+) {
+    constructor() : this(
+        testCommands = mutableListOf<Flicker.() -> Any>(),
+        runCommands = mutableListOf<Flicker.() -> Any>()
+    )
+
+    /**
+     * Copy constructor
+     */
+    constructor(otherCommands: TestCommandsBuilder) : this(
+        otherCommands.testCommands.toMutableList(),
+        otherCommands.runCommands.toMutableList()
+    )
+
+    /**
+     * Commands to execute once for the whole test
+     *
+     * If used on the context of if [Flicker.setup], the commands are executed before any traces
+     * are recorded and other commands are executed
+     *
+     * If used on the context of if [Flicker.teardown], the commands are executed after all traces
+     * are recorded and all other commands are executed.
+     *
+     * This command can be used multiple times, and the results are appended
+     */
+    fun test(command: Flicker.() -> Unit) {
+        testCommands.add(command)
+    }
+
+    /**
+     * Commands to execute once for each test repetition
+     *
+     * If used on the context of if [Flicker.setup], the commands are executed after those defined
+     * in [test] but before any traces are recorded and before the test commands [Flicker.execute]
+     * are executed
+     *
+     * If used on the context of if [Flicker.teardown], the commands are executed before those
+     * defined in [test] but after all traces are recorded and the test commands from [Flicker.execute]
+     * are executed.
+     *
+     * This command can be used multiple times, and the results are appended
+     */
+    fun eachRun(command: Flicker.() -> Unit) {
+        runCommands.add(command)
+    }
+
+    fun buildTestCommands(): List<Flicker.() -> Any> = testCommands
+
+    fun buildRunCommands(): List<Flicker.() -> Any> = runCommands
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/helpers/AutomationUtils.java b/libraries/flicker/src/com/android/server/wm/flicker/helpers/AutomationUtils.java
deleted file mode 100644
index 17f046b..0000000
--- a/libraries/flicker/src/com/android/server/wm/flicker/helpers/AutomationUtils.java
+++ /dev/null
@@ -1,302 +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.helpers;
-
-import static android.os.SystemClock.sleep;
-import static android.view.Surface.ROTATION_0;
-
-import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
-
-import static org.junit.Assert.assertNotNull;
-
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.graphics.Point;
-import android.graphics.Rect;
-import android.os.RemoteException;
-import android.support.test.uiautomator.By;
-import android.support.test.uiautomator.BySelector;
-import android.support.test.uiautomator.Configurator;
-import android.support.test.uiautomator.UiDevice;
-import android.support.test.uiautomator.UiObject2;
-import android.support.test.uiautomator.Until;
-import android.util.Log;
-import android.util.Rational;
-import android.view.Surface;
-import android.view.View;
-import android.view.ViewConfiguration;
-
-import androidx.test.InstrumentationRegistry;
-
-import com.android.server.wm.flicker.WindowUtils;
-
-import java.util.Locale;
-
-/** Collection of UI Automation helper functions. */
-public class AutomationUtils {
-    private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
-    private static final long FIND_TIMEOUT = 10000;
-    private static final long LONG_PRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout() * 2L;
-    private static final String TAG = "FLICKER";
-
-    public static void wakeUpAndGoToHomeScreen() {
-        UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
-        try {
-            device.wakeUp();
-        } catch (RemoteException e) {
-            throw new RuntimeException(e);
-        }
-        device.pressHome();
-    }
-
-    /**
-     * Sets {@link android.app.UiAutomation#waitForIdle(long, long)} global timeout to 0 causing the
-     * {@link android.app.UiAutomation#waitForIdle(long, long)} function to timeout instantly. This
-     * removes some delays when using the UIAutomator library required to create fast UI
-     * transitions.
-     */
-    public static void setFastWait() {
-        Configurator.getInstance().setWaitForIdleTimeout(0);
-    }
-
-    /** Reverts {@link android.app.UiAutomation#waitForIdle(long, long)} to default behavior. */
-    public static void setDefaultWait() {
-        Configurator.getInstance().setWaitForIdleTimeout(10000);
-    }
-
-    public static boolean isQuickstepEnabled(UiDevice device) {
-        boolean enabled = device.findObject(By.res(SYSTEMUI_PACKAGE, "recent_apps")) == null;
-        Log.d(TAG, "Quickstep enabled: " + enabled);
-        return enabled;
-    }
-
-    public static void openQuickstep(UiDevice device) {
-        if (isQuickstepEnabled(device)) {
-            int height = device.getDisplayHeight();
-            UiObject2 navBar = device.findObject(By.res(SYSTEMUI_PACKAGE, "navigation_bar_frame"));
-
-            Rect navBarVisibleBounds;
-
-            // TODO(vishnun) investigate why this object cannot be found.
-            if (navBar != null) {
-                navBarVisibleBounds = navBar.getVisibleBounds();
-            } else {
-                Log.e(TAG, "Could not find nav bar, infer location");
-                navBarVisibleBounds = WindowUtils.getNavigationBarPosition(ROTATION_0);
-            }
-
-            // Swipe from nav bar to 2/3rd down the screen.
-            device.swipe(
-                    navBarVisibleBounds.centerX(),
-                    navBarVisibleBounds.centerY(),
-                    navBarVisibleBounds.centerX(),
-                    height * 2 / 3,
-                    (navBarVisibleBounds.centerY() - height * 2 / 3) / 100); // 100 px/step
-        }
-
-        // use a long timeout to wait until recents populated
-        BySelector recentsSysUISelector = By.res(device.getLauncherPackageName(), "overview_panel");
-        UiObject2 recents = device.wait(Until.findObject(recentsSysUISelector), FIND_TIMEOUT);
-
-        // Quickstep detection is flaky on AOSP, UIDevice doesn't always find SysUI elements
-        // If it couldn't find, try pressing 'recent items' button
-        if (recents == null) {
-            try {
-                device.pressRecentApps();
-            } catch (RemoteException e) {
-                throw new RuntimeException(e);
-            }
-            recents = device.wait(Until.findObject(recentsSysUISelector), FIND_TIMEOUT);
-        }
-
-        assertNotNull("Recent items didn't appear", recents);
-        device.waitForIdle();
-    }
-
-    public static void clearRecents(UiDevice device) {
-        if (isQuickstepEnabled(device)) {
-            openQuickstep(device);
-
-            for (int i = 0; i < 10; i++) {
-                device.swipe(
-                        device.getDisplayWidth() / 2,
-                        device.getDisplayHeight() / 2,
-                        device.getDisplayWidth(),
-                        device.getDisplayHeight() / 2,
-                        5);
-
-                BySelector noRecentItemsSelector =
-                        getLauncherOverviewSelector(device).desc("No recent items");
-                UiObject2 noRecentItems = device.wait(Until.findObject(noRecentItemsSelector), 100);
-
-                // If "No recent items"  is displayed, there're no apps to remove
-                if (noRecentItems != null) {
-                    return;
-                }
-
-                // If "Clear all"  button appears, use it
-                BySelector clearAllSelector =
-                        By.res(device.getLauncherPackageName(), "clear_all_button");
-                UiObject2 clearAllButton = device.wait(Until.findObject(clearAllSelector), 100);
-                if (clearAllButton != null) {
-                    clearAllButton.click();
-                    return;
-                }
-            }
-        }
-    }
-
-    private static BySelector getLauncherOverviewSelector(UiDevice device) {
-        return By.res(device.getLauncherPackageName(), "overview_panel");
-    }
-
-    private static void longPressRecents(UiDevice device) {
-        BySelector recentsSelector = By.res(SYSTEMUI_PACKAGE, "recent_apps");
-        UiObject2 recentsButton = device.wait(Until.findObject(recentsSelector), FIND_TIMEOUT);
-        assertNotNull("Unable to find 'recent items' button", recentsButton);
-        recentsButton.click(LONG_PRESS_TIMEOUT);
-    }
-
-    public static void launchSplitScreen(UiDevice device) {
-        if (isQuickstepEnabled(device)) {
-            // Quickstep enabled
-            openQuickstep(device);
-        } else {
-            try {
-                device.pressRecentApps();
-            } catch (RemoteException e) {
-                Log.e(TAG, "launchSplitScreen", e);
-            }
-        }
-
-        BySelector overviewIconSelector =
-                By.res(device.getLauncherPackageName(), "icon").clazz(View.class);
-        UiObject2 overviewIcon = device.wait(Until.findObject(overviewIconSelector), FIND_TIMEOUT);
-        assertNotNull("Unable to find app icon in Overview", overviewIcon);
-        overviewIcon.click();
-
-        BySelector splitScreenButtonSelector = By.text("Split screen");
-        UiObject2 splitScreenButton =
-                device.wait(Until.findObject(splitScreenButtonSelector), FIND_TIMEOUT);
-        assertNotNull("Unable to find Split screen button in Overview", splitScreenButton);
-        splitScreenButton.click();
-
-        // Wait for animation to complete.
-        sleep(2000);
-
-        UiObject2 divider =
-                device.wait(Until.findObject(getSplitScreenDividerSelector()), FIND_TIMEOUT);
-        assertNotNull("Unable to find Split screen divider", divider);
-    }
-
-    private static BySelector getSplitScreenDividerSelector() {
-        return By.res(SYSTEMUI_PACKAGE, "docked_divider_handle");
-    }
-
-    public static void exitSplitScreen(UiDevice device) {
-        // Quickstep enabled
-        UiObject2 divider =
-                device.wait(Until.findObject(getSplitScreenDividerSelector()), FIND_TIMEOUT);
-        assertNotNull("Unable to find Split screen divider", divider);
-
-        // Drag the split screen divider to the top of the screen
-        int rotation = device.getDisplayRotation();
-        boolean isRotated = rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270;
-
-        Point dstPoint;
-        if (isRotated) {
-            dstPoint = new Point(0, device.getDisplayWidth() / 2);
-        } else {
-            dstPoint = new Point(device.getDisplayWidth() / 2, 0);
-        }
-        divider.drag(dstPoint, 400);
-        // Wait for animation to complete.
-        sleep(2000);
-    }
-
-    public static void resizeSplitScreen(UiDevice device, Rational windowHeightRatio) {
-        BySelector dividerSelector = getSplitScreenDividerSelector();
-        UiObject2 divider = device.wait(Until.findObject(dividerSelector), FIND_TIMEOUT);
-        assertNotNull("Unable to find Split screen divider", divider);
-
-        int destHeight =
-                (int) (WindowUtils.getDisplayBounds().height() * windowHeightRatio.floatValue());
-
-        // Drag the split screen divider to so that the ratio of top window height and bottom
-        // window height is windowHeightRatio
-        device.drag(
-                divider.getVisibleBounds().centerX(),
-                divider.getVisibleBounds().centerY(),
-                device.getDisplayWidth() / 2,
-                destHeight,
-                10);
-        // divider.drag(new Point(device.getDisplayWidth() / 2, destHeight), 400)
-        device.wait(Until.findObject(dividerSelector), FIND_TIMEOUT);
-
-        // Wait for animation to complete.
-        sleep(2000);
-    }
-
-    public static BySelector getPipWindowSelector() {
-        return By.res(SYSTEMUI_PACKAGE, "background");
-    }
-
-    public static void closePipWindow(UiDevice device) {
-        UiObject2 pipWindow = device.findObject(getPipWindowSelector());
-        assertNotNull("PIP window not found", pipWindow);
-
-        pipWindow.click();
-
-        UiObject2 exitPipObject = device.findObject(By.res(SYSTEMUI_PACKAGE, "dismiss"));
-        assertNotNull("PIP window dismiss button not found", pipWindow);
-
-        exitPipObject.click();
-        // Wait for animation to complete.
-        sleep(2000);
-    }
-
-    public static void expandPipWindow(UiDevice device) {
-        UiObject2 pipWindow = device.findObject(getPipWindowSelector());
-        assertNotNull("PIP window not found", pipWindow);
-        pipWindow.click();
-        pipWindow.click();
-    }
-
-    public static void stopPackage(Context context, String packageName) {
-        runShellCommand("am force-stop " + packageName);
-        int packageUid;
-        try {
-            packageUid = context.getPackageManager().getPackageUid(packageName, /* flags= */ 0);
-        } catch (PackageManager.NameNotFoundException e) {
-            return;
-        }
-        while (targetPackageIsRunning(packageUid)) {
-            try {
-                Thread.sleep(100);
-            } catch (InterruptedException e) {
-                // ignore
-            }
-        }
-    }
-
-    private static boolean targetPackageIsRunning(int uid) {
-        final String result =
-                runShellCommand(
-                        String.format(Locale.getDefault(), "cmd activity get-uid-state %d", uid));
-        return !result.contains("(NONEXISTENT)");
-    }
-}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/helpers/AutomationUtils.kt b/libraries/flicker/src/com/android/server/wm/flicker/helpers/AutomationUtils.kt
new file mode 100644
index 0000000..fe7058d
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/helpers/AutomationUtils.kt
@@ -0,0 +1,391 @@
+/*
+ * 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.server.wm.flicker.helpers
+
+import android.content.Context
+import android.content.pm.PackageManager
+import android.graphics.Point
+import android.graphics.Rect
+import android.os.RemoteException
+import android.os.SystemClock
+import android.util.Log
+import android.util.Rational
+import android.view.Surface
+import android.view.View
+import android.view.ViewConfiguration
+import androidx.annotation.VisibleForTesting
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.BySelector
+import androidx.test.uiautomator.Configurator
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.Until
+import com.android.compatibility.common.util.SystemUtil
+import com.android.server.wm.flicker.helpers.WindowUtils.displayBounds
+import com.android.server.wm.flicker.helpers.WindowUtils.getNavigationBarPosition
+import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
+import org.junit.Assert
+import org.junit.Assert.assertNotNull
+
+const val FIND_TIMEOUT: Long = 10000
+const val FAST_WAIT_TIMEOUT: Long = 0
+const val DOCKED_STACK_DIVIDER = "DockedStackDivider"
+const val IME_PACKAGE = "com.google.android.inputmethod.latin"
+@VisibleForTesting
+const val SYSTEMUI_PACKAGE = "com.android.systemui"
+private val LONG_PRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout() * 2L
+private const val TAG = "FLICKER"
+
+/**
+ * Sets [android.app.UiAutomation.waitForIdle] global timeout to 0 causing the
+ * [android.app.UiAutomation.waitForIdle] function to timeout instantly. This
+ * removes some delays when using the UIAutomator library required to create fast UI
+ * transitions.
+ */
+fun setFastWait() {
+    Configurator.getInstance().waitForIdleTimeout = FAST_WAIT_TIMEOUT
+}
+
+/**
+ * Reverts [android.app.UiAutomation.waitForIdle] to default behavior.
+ */
+fun setDefaultWait() {
+    Configurator.getInstance().waitForIdleTimeout = FIND_TIMEOUT
+}
+
+/**
+ * Checks if the device is running on gestural or 2-button navigation modes
+ */
+fun UiDevice.isQuickstepEnabled(): Boolean {
+    val enabled = this.findObject(By.res(SYSTEMUI_PACKAGE, "recent_apps")) == null
+    Log.d(TAG, "Quickstep enabled: $enabled")
+    return enabled
+}
+
+/**
+ * Checks if the display is rotated or not
+ */
+fun UiDevice.isRotated(): Boolean {
+    return this.displayRotation.isRotated()
+}
+
+/**
+ * Reopens the first device window from the list of recent apps (overview)
+ */
+fun UiDevice.reopenAppFromOverview(
+    wmHelper: WindowManagerStateHelper
+) {
+    val x = this.displayWidth / 2
+    val y = this.displayHeight / 2
+    this.click(x, y)
+
+    wmHelper.waitForAppTransitionIdle()
+}
+
+/**
+ * Shows quickstep
+ *
+ * @throws AssertionError When quickstep does not appear
+ */
+fun UiDevice.openQuickstep(
+    wmHelper: WindowManagerStateHelper
+) {
+    if (this.isQuickstepEnabled()) {
+        val navBar = this.findObject(By.res(SYSTEMUI_PACKAGE, "navigation_bar_frame"))
+        val navBarVisibleBounds: Rect
+
+        // TODO(vishnun) investigate why this object cannot be found.
+        navBarVisibleBounds = if (navBar != null) {
+            navBar.visibleBounds
+        } else {
+            Log.e(TAG, "Could not find nav bar, infer location")
+            getNavigationBarPosition(Surface.ROTATION_0).bounds
+        }
+
+        val startX = navBarVisibleBounds.centerX()
+        val startY = navBarVisibleBounds.centerY()
+        val endX: Int
+        val endY: Int
+        val height: Int
+        val steps: Int
+        if (this.isRotated()) {
+            height = this.displayWidth
+            endX = height * 2 / 3
+            endY = navBarVisibleBounds.centerY()
+            steps = (endX - startX) / 100 // 100 px/step
+        } else {
+            height = this.displayHeight
+            endX = navBarVisibleBounds.centerX()
+            endY = height * 2 / 3
+            steps = (startY - endY) / 100 // 100 px/step
+        }
+        // Swipe from nav bar to 2/3rd down the screen.
+        this.swipe(startX, startY, endX, endY, steps)
+    }
+
+    // use a long timeout to wait until recents populated
+    val recentsSysUISelector = By.res(this.launcherPackageName, "overview_panel")
+    var recents = this.wait(Until.findObject(recentsSysUISelector), FIND_TIMEOUT)
+
+    // Quickstep detection is flaky on AOSP, UIDevice doesn't always find SysUI elements
+    // If it couldn't find, try pressing 'recent items' button
+    if (recents == null) {
+        try {
+            this.pressRecentApps()
+        } catch (e: RemoteException) {
+            throw RuntimeException(e)
+        }
+        recents = this.wait(Until.findObject(recentsSysUISelector), FIND_TIMEOUT)
+    }
+    assertNotNull("Recent items didn't appear", recents)
+    wmHelper.waitForNavBarStatusBarVisible()
+    wmHelper.waitForAppTransitionIdle()
+}
+
+private fun getLauncherOverviewSelector(device: UiDevice): BySelector {
+    return By.res(device.launcherPackageName, "overview_panel")
+}
+
+private fun longPressRecents(device: UiDevice) {
+    val recentsSelector = By.res(SYSTEMUI_PACKAGE, "recent_apps")
+    val recentsButton = device.wait(Until.findObject(recentsSelector), FIND_TIMEOUT)
+    assertNotNull("Unable to find 'recent items' button", recentsButton)
+    recentsButton.click(LONG_PRESS_TIMEOUT)
+}
+
+/**
+ * Wait for any IME view to appear
+ */
+fun UiDevice.waitForIME(): Boolean {
+    val ime = this.wait(Until.findObject(By.pkg(IME_PACKAGE)), FIND_TIMEOUT)
+    return ime != null
+}
+
+private fun openQuickStepAndLongPressOverviewIcon(
+    device: UiDevice,
+    wmHelper: WindowManagerStateHelper
+) {
+    if (device.isQuickstepEnabled()) {
+        device.openQuickstep(wmHelper)
+    } else {
+        try {
+            device.pressRecentApps()
+        } catch (e: RemoteException) {
+            Log.e(TAG, "launchSplitScreen", e)
+        }
+    }
+    val overviewIconSelector = By.res(device.launcherPackageName, "icon")
+        .clazz(View::class.java)
+    val overviewIcon = device.wait(Until.findObject(overviewIconSelector), FIND_TIMEOUT)
+    assertNotNull("Unable to find app icon in Overview", overviewIcon)
+    overviewIcon.click()
+}
+
+fun UiDevice.openQuickStepAndClearRecentAppsFromOverview(
+    wmHelper: WindowManagerStateHelper
+) {
+    if (this.isQuickstepEnabled()) {
+        this.openQuickstep(wmHelper)
+    } else {
+        try {
+            this.pressRecentApps()
+        } catch (e: RemoteException) {
+            Log.e(TAG, "launchSplitScreen", e)
+        }
+    }
+    for (i in 0..9) {
+        this.swipe(
+                this.getDisplayWidth() / 2,
+                this.getDisplayHeight() / 2,
+                this.getDisplayWidth(),
+                this.getDisplayHeight() / 2,
+                5)
+        // If "Clear all"  button appears, use it
+        val clearAllSelector = By.res(this.getLauncherPackageName(), "clear_all")
+        val clearAllButton = this.wait(Until.findObject(clearAllSelector), FAST_WAIT_TIMEOUT)
+        if (clearAllButton != null) {
+            clearAllButton.click()
+        }
+    }
+    this.pressHome()
+}
+
+/**
+ * Opens quick step and puts the first app from the list of recently used apps into
+ * split-screen
+ *
+ * @throws AssertionError when unable to open the list of recently used apps, or when it does
+ * not contain a button to enter split screen mode
+ */
+fun UiDevice.launchSplitScreen(
+    wmHelper: WindowManagerStateHelper
+) {
+    openQuickStepAndLongPressOverviewIcon(this, wmHelper)
+    val splitScreenButtonSelector = By.text("Split screen")
+    val splitScreenButton =
+            this.wait(Until.findObject(splitScreenButtonSelector), FIND_TIMEOUT)
+    assertNotNull("Unable to find Split screen button in Overview", splitScreenButton)
+    splitScreenButton.click()
+
+    // Wait for animation to complete.
+    this.wait(Until.findObject(splitScreenDividerSelector), FIND_TIMEOUT)
+    wmHelper.waitForSurfaceAppeared(DOCKED_STACK_DIVIDER)
+
+    if (!this.isInSplitScreen()) {
+        Assert.fail("Unable to find Split screen divider")
+    }
+}
+
+/**
+ * Checks if the recent application is able to split screen(resizeable)
+ */
+fun UiDevice.canSplitScreen(
+    wmHelper: WindowManagerStateHelper
+): Boolean {
+    openQuickStepAndLongPressOverviewIcon(this, wmHelper)
+    val splitScreenButtonSelector = By.text("Split screen")
+    val canSplitScreen =
+            this.wait(Until.findObject(splitScreenButtonSelector), FIND_TIMEOUT) != null
+    this.pressHome()
+    return canSplitScreen
+}
+
+/**
+ * Checks if the device is in split screen by searching for the split screen divider
+ */
+fun UiDevice.isInSplitScreen(): Boolean {
+    return this.wait(Until.findObject(splitScreenDividerSelector), FIND_TIMEOUT) != null
+}
+
+fun UiDevice.waitSplitScreenGone(): Boolean {
+    return this.wait(Until.gone(splitScreenDividerSelector), FIND_TIMEOUT) != null
+}
+
+private val splitScreenDividerSelector: BySelector
+    get() = By.res(SYSTEMUI_PACKAGE, "docked_divider_handle")
+
+/**
+ * Drags the split screen divider to the top of the screen to close it
+ *
+ * @throws AssertionError when unable to find the split screen divider
+ */
+fun UiDevice.exitSplitScreen() {
+    // Quickstep enabled
+    val divider = this.wait(Until.findObject(splitScreenDividerSelector), FIND_TIMEOUT)
+    assertNotNull("Unable to find Split screen divider", divider)
+
+    // Drag the split screen divider to the top of the screen
+    val dstPoint = if (this.isRotated()) {
+        Point(0, this.displayWidth / 2)
+    } else {
+        Point(this.displayWidth / 2, 0)
+    }
+    divider.drag(dstPoint, 400)
+    // Wait for animation to complete.
+    SystemClock.sleep(2000)
+}
+
+/**
+ * Drags the split screen divider to the bottom of the screen to close it
+ *
+ * @throws AssertionError when unable to find the split screen divider
+ */
+fun UiDevice.exitSplitScreenFromBottom() {
+    // Quickstep enabled
+    val divider = this.wait(Until.findObject(splitScreenDividerSelector), FIND_TIMEOUT)
+    assertNotNull("Unable to find Split screen divider", divider)
+
+    // Drag the split screen divider to the bottom of the screen
+    val dstPoint = if (this.isRotated()) {
+        Point(this.displayWidth, this.displayWidth / 2)
+    } else {
+        Point(this.displayWidth / 2, this.displayHeight)
+    }
+    divider.drag(dstPoint, 400)
+    if (!this.waitSplitScreenGone()) {
+        Assert.fail("Split screen divider never disappeared")
+    }
+}
+
+/**
+ * Drags the split screen divider to resize the windows in split screen
+ *
+ * @throws AssertionError when unable to find the split screen divider
+ */
+fun UiDevice.resizeSplitScreen(windowHeightRatio: Rational) {
+    val dividerSelector = splitScreenDividerSelector
+    val divider = this.wait(Until.findObject(dividerSelector), FIND_TIMEOUT)
+    assertNotNull("Unable to find Split screen divider", divider)
+    val destHeight = (displayBounds.height() * windowHeightRatio.toFloat()).toInt()
+
+    // Drag the split screen divider to so that the ratio of top window height and bottom
+    // window height is windowHeightRatio
+    this.drag(
+            divider.visibleBounds.centerX(),
+            divider.visibleBounds.centerY(),
+            this.displayWidth / 2,
+            destHeight,
+            10)
+    this.wait(Until.findObject(dividerSelector), FIND_TIMEOUT)
+    // Wait for animation to complete.
+    SystemClock.sleep(2000)
+}
+
+/**
+ * Checks if the device has a window with the package name
+ */
+fun UiDevice.hasWindow(packageName: String): Boolean {
+    return this.wait(Until.findObject(By.pkg(packageName)), FIND_TIMEOUT) != null
+}
+
+/**
+ * Waits until the package with that name is gone
+ */
+fun UiDevice.waitUntilGone(packageName: String): Boolean {
+    return this.wait(Until.gone(By.pkg(packageName)), FIND_TIMEOUT) != null
+}
+
+fun stopPackage(context: Context, packageName: String) {
+    SystemUtil.runShellCommand("am force-stop $packageName")
+    val packageUid = try {
+        context.packageManager.getPackageUid(packageName, /* flags= */0)
+    } catch (e: PackageManager.NameNotFoundException) {
+        return
+    }
+    while (targetPackageIsRunning(packageUid)) {
+        try {
+            Thread.sleep(100)
+        } catch (e: InterruptedException) { // ignore
+        }
+    }
+}
+
+private fun targetPackageIsRunning(uid: Int): Boolean {
+    val result = SystemUtil.runShellCommand("cmd activity get-uid-state $uid")
+    return !result.contains("(NONEXISTENT)")
+}
+
+/**
+ * Turns on the device display and presses the home button to reach the launcher screen
+ */
+fun UiDevice.wakeUpAndGoToHomeScreen() {
+    try {
+        this.wakeUp()
+    } catch (e: RemoteException) {
+        throw RuntimeException(e)
+    }
+    this.pressHome()
+}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/helpers/StandardAppHelper.kt b/libraries/flicker/src/com/android/server/wm/flicker/helpers/StandardAppHelper.kt
new file mode 100644
index 0000000..71338f1
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/helpers/StandardAppHelper.kt
@@ -0,0 +1,200 @@
+/*
+ * 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.server.wm.flicker.helpers
+
+import android.app.ActivityManager
+import android.app.Instrumentation
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.platform.helpers.AbstractStandardAppHelper
+import android.support.test.launcherhelper.ILauncherStrategy
+import android.support.test.launcherhelper.LauncherStrategyFactory
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.BySelector
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.Until
+import com.android.server.wm.traces.common.windowmanager.WindowManagerState
+import com.android.server.wm.traces.parser.toActivityName
+import com.android.server.wm.traces.parser.toWindowName
+import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
+
+/**
+ * Class to take advantage of {@code IAppHelper} interface so the same test can be run against first
+ * party and third party apps.
+ */
+open class StandardAppHelper @JvmOverloads constructor(
+    instr: Instrumentation,
+    @JvmField val appName: String,
+    @JvmField val component: ComponentName,
+    protected val launcherStrategy: ILauncherStrategy =
+        LauncherStrategyFactory.getInstance(instr).launcherStrategy
+) : AbstractStandardAppHelper(instr) {
+    constructor(
+        instr: Instrumentation,
+        appName: String,
+        packageName: String,
+        activity: String,
+        launcherStrategy: ILauncherStrategy =
+            LauncherStrategyFactory.getInstance(instr).launcherStrategy
+    ): this(instr, appName,
+        ComponentName.createRelative(packageName, ".$activity"), launcherStrategy)
+
+    val windowName: String = component.toWindowName()
+    val activityName: String = component.toActivityName()
+
+    private val activityManager: ActivityManager?
+        get() = mInstrumentation.context.getSystemService(ActivityManager::class.java)
+
+    protected val context: Context
+        get() = mInstrumentation.context
+
+    protected val uiDevice: UiDevice = UiDevice.getInstance(mInstrumentation)
+
+    private fun getAppSelector(expectedPackageName: String): BySelector {
+        val expected = if (expectedPackageName.isNotEmpty()) {
+            expectedPackageName
+        } else {
+            component.packageName
+        }
+        return By.pkg(expected).depth(0)
+    }
+
+    override fun open() {
+        launcherStrategy.launch(appName, component.packageName)
+    }
+
+    /** {@inheritDoc}  */
+    override fun getPackage(): String {
+        return component.packageName
+    }
+
+    /** {@inheritDoc}  */
+    override fun getOpenAppIntent(): Intent {
+        val intent = Intent()
+        intent.addCategory(Intent.CATEGORY_LAUNCHER)
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+        intent.component = component
+        return intent
+    }
+
+    /** {@inheritDoc}  */
+    override fun getLauncherName(): String {
+        return appName
+    }
+
+    /** {@inheritDoc}  */
+    override fun dismissInitialDialogs() {}
+
+    /** {@inheritDoc}  */
+    override fun exit() {
+        // Ensure all testing components end up being closed.
+        activityManager?.forceStopPackage(component.packageName)
+    }
+
+    /**
+     * Exits the activity and wait for activity destroyed
+     */
+    @JvmOverloads
+    fun exit(
+        wmHelper: WindowManagerStateHelper
+    ) {
+        exit()
+        waitForActivityDestroyed(wmHelper)
+    }
+
+    /**
+     * Waits the activity until state change to {link WindowManagerState.STATE_DESTROYED}
+     */
+    private fun waitForActivityDestroyed(
+        wmHelper: WindowManagerStateHelper
+    ) {
+        val activityName = component.toActivityName()
+        wmHelper.waitFor("state of $activityName to be ${WindowManagerState.STATE_DESTROYED}") {
+            !it.wmState.containsActivity(activityName) ||
+                it.wmState.hasActivityState(activityName, WindowManagerState.STATE_DESTROYED)
+        }
+        wmHelper.waitForAppTransitionIdle()
+        // Ensure WindowManagerService wait until all animations have completed
+        mInstrumentation.uiAutomation.syncInputTransactions()
+    }
+
+    private fun launchAppViaIntent(
+        action: String? = null,
+        stringExtras: Map<String, String> = mapOf()
+    ) {
+        val intent = openAppIntent
+        intent.action = action
+        stringExtras.forEach {
+            intent.putExtra(it.key, it.value)
+        }
+        context.startActivity(intent)
+    }
+
+    /**
+     * Launches the app through an intent instead of interacting with the launcher.
+     *
+     * Uses UiAutomation to detect when the app is open
+     */
+    @JvmOverloads
+    open fun launchViaIntent(
+        expectedPackageName: String = "",
+        action: String? = null,
+        stringExtras: Map<String, String> = mapOf()
+    ) {
+        launchAppViaIntent(action, stringExtras)
+        val appSelector = getAppSelector(expectedPackageName)
+        uiDevice.wait(Until.hasObject(appSelector), APP_LAUNCH_WAIT_TIME_MS)
+    }
+
+    /**
+     * Launches the app through an intent instead of interacting with the launcher and waits
+     * until the app window is visible
+     */
+    @JvmOverloads
+    open fun launchViaIntent(
+        wmHelper: WindowManagerStateHelper,
+        expectedWindowName: String = "",
+        action: String? = null,
+        stringExtras: Map<String, String> = mapOf()
+    ) {
+        launchAppViaIntent(action, stringExtras)
+
+        val window = if (expectedWindowName.isNotEmpty()) {
+            expectedWindowName
+        } else {
+            windowName
+        }
+        wmHelper.waitFor("App is shown") {
+            it.wmState.isComplete() && it.wmState.isWindowVisible(window)
+        }
+
+        wmHelper.waitForAppTransitionIdle()
+        // During seamless rotation the app window is shown
+        val currWmState = wmHelper.currentState.wmState
+        if (currWmState.visibleWindows.none { it.isFullscreen }) {
+            wmHelper.waitForNavBarStatusBarVisible()
+        }
+
+        // Ensure WindowManagerService wait until all animations have completed
+        mInstrumentation.getUiAutomation().syncInputTransactions()
+    }
+
+    companion object {
+        private const val APP_LAUNCH_WAIT_TIME_MS = 10000L
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/helpers/WindowUtils.kt b/libraries/flicker/src/com/android/server/wm/flicker/helpers/WindowUtils.kt
new file mode 100644
index 0000000..2d1e443
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/helpers/WindowUtils.kt
@@ -0,0 +1,171 @@
+/*
+ * 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.server.wm.flicker.helpers
+
+import android.content.Context
+import android.graphics.Point
+import android.graphics.Rect
+import android.graphics.Region
+import android.view.Surface
+import android.view.WindowManager
+import androidx.test.platform.app.InstrumentationRegistry
+import kotlin.math.max
+import kotlin.math.min
+
+fun Int.isRotated() = this == Surface.ROTATION_90 || this == Surface.ROTATION_270
+
+object WindowUtils {
+    /**
+     * Helper functions to retrieve system window sizes and positions.
+     */
+    private val context
+        get() = InstrumentationRegistry.getInstrumentation().context
+
+    private val resources
+        get() = context.getResources()
+
+    /**
+     * Get the display bounds
+     */
+    val displayBounds: Rect
+        get() {
+            val display = Point()
+            val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
+            wm.defaultDisplay.getRealSize(display)
+            return Rect(0, 0, display.x, display.y)
+        }
+
+    /**
+     * Gets the current display rotation
+     */
+    val displayRotation: Int
+        get() {
+            val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
+            return wm.defaultDisplay.rotation
+        }
+
+    /**
+     * Get the display bounds when the device is at a specific rotation
+     *
+     * @param requestedRotation Device rotation
+     */
+    fun getDisplayBounds(requestedRotation: Int): Region {
+        val displayIsRotated = displayRotation.isRotated()
+        val requestedDisplayIsRotated = requestedRotation.isRotated()
+
+        // if the current orientation changes with the requested rotation,
+        // flip height and width of display bounds.
+        return if (displayIsRotated != requestedDisplayIsRotated) {
+            Region(0, 0, displayBounds.height(), displayBounds.width())
+        } else {
+            Region(0, 0, displayBounds.width(), displayBounds.height())
+        }
+    }
+
+    /**
+     * Gets the expected status bar position at a specific rotation
+     *
+     * @param requestedRotation Device rotation
+     */
+    fun getStatusBarPosition(requestedRotation: Int): Region {
+        val displayBounds = displayBounds
+        val resourceName: String
+        val width: Int
+        if (!requestedRotation.isRotated()) {
+            resourceName = "status_bar_height_portrait"
+            width = min(displayBounds.width(), displayBounds.height())
+        } else {
+            resourceName = "status_bar_height_landscape"
+            width = max(displayBounds.width(), displayBounds.height())
+        }
+        val resourceId = resources.getIdentifier(resourceName, "dimen", "android")
+        val height = resources.getDimensionPixelSize(resourceId)
+        return Region(0, 0, width, height)
+    }
+
+    /**
+     * Gets the expected navigation bar position at a specific rotation
+     *
+     * @param requestedRotation Device rotation
+     */
+    fun getNavigationBarPosition(requestedRotation: Int): Region {
+        val displayBounds = displayBounds
+        val displayWidth: Int
+        val displayHeight: Int
+        if (!requestedRotation.isRotated()) {
+            displayWidth = displayBounds.width()
+            displayHeight = displayBounds.height()
+        } else {
+            // swap display dimensions in landscape or seascape mode
+            displayWidth = displayBounds.height()
+            displayHeight = displayBounds.width()
+        }
+        val navBarWidth = getDimensionPixelSize("navigation_bar_width")
+        val navBarHeight = navigationBarHeight
+
+        return when {
+            // nav bar is at the bottom of the screen
+            requestedRotation in listOf(Surface.ROTATION_0, Surface.ROTATION_180) ||
+                isGesturalNavigationEnabled ->
+                Region(0, displayHeight - navBarHeight, displayWidth, displayHeight)
+            // nav bar is at the right side
+            requestedRotation == Surface.ROTATION_90 ->
+                Region(displayWidth - navBarWidth, 0, displayWidth, displayHeight)
+            // nav bar is at the left side
+            requestedRotation == Surface.ROTATION_270 ->
+                Region(0, 0, navBarWidth, displayHeight)
+            else -> error("Unknown rotation $requestedRotation")
+        }
+    }
+
+    /**
+     * Checks if the device uses gestural navigation
+     */
+    val isGesturalNavigationEnabled: Boolean
+        get() {
+            val resourceId = resources
+                    .getIdentifier("config_navBarInteractionMode", "integer", "android")
+            return resources.getInteger(resourceId) == 2 /* NAV_BAR_MODE_GESTURAL */
+        }
+
+    fun getDimensionPixelSize(resourceName: String): Int {
+        val resourceId = resources.getIdentifier(resourceName, "dimen", "android")
+        return resources.getDimensionPixelSize(resourceId)
+    }
+
+    /**
+     * Gets the navigation bar height
+     */
+    val navigationBarHeight: Int
+        get() {
+            var navBarHeight = getDimensionPixelSize("navigation_bar_height")
+            if (isGesturalNavigationEnabled) {
+                navBarHeight += getDimensionPixelSize("navigation_bar_gesture_height")
+            }
+            return navBarHeight
+        }
+
+    /**
+     * Split screen divider inset height
+     */
+    val dockedStackDividerInset: Int
+        get() {
+            val resourceId = resources
+                    .getIdentifier("docked_stack_divider_insets", "dimen", "android")
+            return resources.getDimensionPixelSize(resourceId)
+        }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/monitor/EventLogMonitor.kt b/libraries/flicker/src/com/android/server/wm/flicker/monitor/EventLogMonitor.kt
new file mode 100644
index 0000000..c1df425
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/monitor/EventLogMonitor.kt
@@ -0,0 +1,102 @@
+/*
+ * 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.server.wm.flicker.monitor
+
+import android.util.EventLog
+import android.util.EventLog.Event
+import com.android.server.wm.flicker.FlickerRunResult
+import com.android.server.wm.flicker.traces.eventlog.FocusEvent
+import com.android.server.wm.flicker.traces.eventlog.FocusEvent.Focus
+import java.io.IOException
+import java.util.UUID
+
+/**
+ * Collects event logs during transitions.
+ */
+open class EventLogMonitor : ITransitionMonitor {
+    private var _logs = listOf<Event>()
+    private lateinit var _logSeparator: String
+
+    /**
+     * Inserts a log separator so we can always find the starting point from where to evaluate
+     * following logs.
+     *
+     * @return Unique log separator.
+     */
+    private fun separateLogs(): String {
+        val logSeparator = UUID.randomUUID().toString()
+        EventLog.writeEvent(EVENT_LOG_SEPARATOR_TAG, logSeparator)
+        return logSeparator
+    }
+
+    private fun getEventLogs(vararg tags: Int): List<Event> {
+        val events = mutableListOf<Event>()
+        val searchTags = tags.copyOf(tags.size + 1)
+        searchTags[searchTags.size - 1] = EVENT_LOG_SEPARATOR_TAG
+        try {
+            EventLog.readEvents(searchTags, events)
+        } catch (e: IOException) {
+            throw RuntimeException(e)
+        }
+        return events.filter { it.data != null }.dropWhile {
+            it.tag != EVENT_LOG_SEPARATOR_TAG || it.data.toString() != _logSeparator
+        }.drop(1)
+    }
+
+    override fun start() {
+        // Insert event log marker
+        _logSeparator = separateLogs()
+    }
+
+    override fun stop() {
+        if (!::_logSeparator.isInitialized) {
+            _logs = emptyList()
+            return
+        }
+        // Read event log from log marker till end
+        _logs = getEventLogs(EVENT_LOG_INPUT_FOCUS_TAG)
+    }
+
+    override fun save(testTag: String, flickerRunResultBuilder: FlickerRunResult.Builder) {
+        flickerRunResultBuilder.eventLog = _logs.mapNotNull { event ->
+            val timestamp = event.timeNanos
+            val log = (event.data as Array<*>).map { it as String }
+            if (log.size != 2) {
+                throw RuntimeException("Error reading from eventlog $log")
+            }
+            val focusState =
+                    when {
+                        log[0].contains(" entering ") -> Focus.GAINED
+                        log[0].contains(" leaving ") -> Focus.LOST
+                        else -> Focus.REQUESTED
+                    }
+            // parse window from 'Focus [entering|leaving] [window name]'
+            // by dropping the first two words
+            var expectedWhiteSpace = 2
+            val window = log[0].dropWhile { !it.isWhitespace() || --expectedWhiteSpace > 0 }
+                    .drop(1)
+            val reason = log[1].removePrefix("reason=")
+            FocusEvent(timestamp, window, focusState, reason)
+                    .takeIf { focusState != Focus.REQUESTED }
+        }
+    }
+
+    private companion object {
+        const val EVENT_LOG_SEPARATOR_TAG = 42
+        const val EVENT_LOG_INPUT_FOCUS_TAG = 62001
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/monitor/Extensions.kt b/libraries/flicker/src/com/android/server/wm/flicker/monitor/Extensions.kt
new file mode 100644
index 0000000..6a87b51
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/monitor/Extensions.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+@file:JvmName("Extensions")
+package com.android.server.wm.flicker.monitor
+
+import com.android.server.wm.traces.parser.DeviceStateDump
+import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace
+import com.android.server.wm.flicker.getDefaultFlickerOutputDir
+import com.android.server.wm.traces.common.layers.LayersTrace
+import com.android.server.wm.traces.parser.layers.LayersTraceParser
+import com.android.server.wm.traces.parser.windowmanager.WindowManagerTraceParser
+import java.nio.file.Path
+
+/**
+ * Acquire the [WindowManagerTrace] with the device state changes that happen when executing
+ * the commands defined in the [predicate].
+ *
+ * @param outputDir Directory where to store the traces
+ * @param predicate Commands to execute
+ * @throws UnsupportedOperationException If tracing is already activated
+ */
+@JvmOverloads
+fun withWMTracing(
+    outputDir: Path = getDefaultFlickerOutputDir().resolve("withWMTracing"),
+    predicate: () -> Unit
+): WindowManagerTrace {
+    return WindowManagerTraceParser.parseFromTrace(
+        WindowManagerTraceMonitor(outputDir).withTracing(predicate))
+}
+
+/**
+ * Acquire the [LayersTrace] with the device state changes that happen when executing
+ * the commands defined in the [predicate].
+ *
+ * @param traceFlags Flags to indicate tracing level
+ * @param outputDir Directory where to store the traces
+ * @param predicate Commands to execute
+ * @throws UnsupportedOperationException If tracing is already activated
+ */
+@JvmOverloads
+fun withSFTracing(
+    traceFlags: Int = LayersTraceMonitor.TRACE_FLAGS,
+    outputDir: Path = getDefaultFlickerOutputDir().resolve("withSFTracing"),
+    predicate: () -> Unit
+): LayersTrace {
+    return LayersTraceParser.parseFromTrace(
+        LayersTraceMonitor(outputDir, traceFlags).withTracing(predicate))
+}
+
+/**
+ * Acquire the [WindowManagerTrace] and [LayersTrace] with the device state changes that happen
+ * when executing the commands defined in the [predicate].
+ *
+ * @param outputDir Directory where to store the traces
+ * @param predicate Commands to execute
+ * @throws UnsupportedOperationException If tracing is already activated
+ */
+@JvmOverloads
+fun withTracing(
+    outputDir: Path = getDefaultFlickerOutputDir(),
+    predicate: () -> Unit
+): DeviceStateDump {
+    val traces = recordTraces(outputDir, predicate)
+    val wmTraceData = traces.first
+    val layersTraceData = traces.second
+    return DeviceStateDump.fromTrace(wmTraceData, layersTraceData)
+}
+
+/**
+ * Acquire the [WindowManagerTrace] and [LayersTrace] with the device state changes that happen
+ * when executing the commands defined in the [predicate].
+ *
+ * @param outputDir Directory where to store the traces
+ * @param predicate Commands to execute
+ * @throws UnsupportedOperationException If tracing is already activated
+ * @return a pair containing the WM and SF traces
+ */
+@JvmOverloads
+fun recordTraces(
+    outputDir: Path = getDefaultFlickerOutputDir(),
+    predicate: () -> Unit
+): Pair<ByteArray, ByteArray> {
+    var wmTraceData = ByteArray(0)
+    val layersOutputDir = outputDir.resolve("withSFTracing")
+    val layersTraceData = LayersTraceMonitor(layersOutputDir).withTracing {
+        val wmOutputDir = outputDir.resolve("withWMTracing")
+        wmTraceData = WindowManagerTraceMonitor(wmOutputDir).withTracing(predicate)
+    }
+
+    return Pair(wmTraceData, layersTraceData)
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/monitor/ITransitionMonitor.kt b/libraries/flicker/src/com/android/server/wm/flicker/monitor/ITransitionMonitor.kt
new file mode 100644
index 0000000..8e5a716
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/monitor/ITransitionMonitor.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.server.wm.flicker.monitor
+
+import com.android.server.wm.flicker.FlickerRunResult
+
+/** Collects test artifacts during a UI transition.  */
+interface ITransitionMonitor {
+    /** Starts monitor.  */
+    fun start()
+
+    /** Stops monitor.  */
+    fun stop()
+
+    /**
+     * Saves any monitor artifacts to file adding `testTag` and `iteration` to the file
+     * name.
+     *
+     * @param testTag suffix added to artifact name
+     * @param iteration suffix added to artifact name
+     * @param flickerRunResultBuilder Flicker run results
+     * @return Path to saved artifact
+     */
+    fun save(testTag: String, iteration: Int, flickerRunResultBuilder: FlickerRunResult.Builder) =
+            save("${testTag}_$iteration", flickerRunResultBuilder)
+
+    /**
+     * Saves any monitor artifacts to file adding `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.
+     *
+     *
+     * 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
+     * @param flickerRunResultBuilder Flicker run results
+     */
+    fun save(testTag: String, flickerRunResultBuilder: FlickerRunResult.Builder) {
+        throw UnsupportedOperationException("Save not implemented for this monitor")
+    }
+
+    val checksum: String
+        get() = throw UnsupportedOperationException("Checksum 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
deleted file mode 100644
index 633ac84..0000000
--- a/libraries/flicker/src/com/android/server/wm/flicker/monitor/LayersTraceMonitor.java
+++ /dev/null
@@ -1,71 +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.RemoteException;
-import android.view.IWindowManager;
-import android.view.WindowManagerGlobal;
-
-import java.nio.file.Path;
-
-/** Captures Layers trace from SurfaceFlinger. */
-public class LayersTraceMonitor extends TraceMonitor {
-    private static final String TRACE_FILE = "layers_trace.pb";
-    private IWindowManager mWm = WindowManagerGlobal.getWindowManagerService();
-    private static final int TRACE_FLAGS = 0x7; // TRACE_CRITICAL|TRACE_INPUT|TRACE_COMPOSITION
-
-    public LayersTraceMonitor() {
-        this(OUTPUT_DIR);
-    }
-
-    public LayersTraceMonitor(Path outputDir) {
-        super(outputDir, TRACE_FILE);
-    }
-
-    @Override
-    public void start() {
-        try {
-            mWm.setLayerTracingFlags(TRACE_FLAGS);
-        } catch (RemoteException e) {
-            e.printStackTrace();
-        }
-        setEnabled(true);
-    }
-
-    @Override
-    public void stop() {
-        setEnabled(false);
-    }
-
-    @Override
-    public boolean isEnabled() {
-        try {
-            return mWm.isLayerTracing();
-        } catch (RemoteException e) {
-            e.printStackTrace();
-        }
-        return false;
-    }
-
-    private void setEnabled(boolean isEnabled) {
-        try {
-            mWm.setLayerTracing(isEnabled);
-        } catch (RemoteException e) {
-            e.printStackTrace();
-        }
-    }
-}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/monitor/LayersTraceMonitor.kt b/libraries/flicker/src/com/android/server/wm/flicker/monitor/LayersTraceMonitor.kt
new file mode 100644
index 0000000..74fa388
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/monitor/LayersTraceMonitor.kt
@@ -0,0 +1,69 @@
+/*
+ * 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.server.wm.flicker.monitor
+
+import android.os.RemoteException
+import android.view.WindowManagerGlobal
+import com.android.server.wm.flicker.FlickerRunResult
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.android.server.wm.traces.common.layers.LayersTrace
+import java.nio.file.Path
+
+/**
+ * Captures [LayersTrace] from SurfaceFlinger.
+ *
+ * Use [LayersTraceSubject.assertThat] to make assertions on the trace
+ */
+open class LayersTraceMonitor(
+    outputDir: Path,
+    private val traceFlags: Int
+) : TransitionMonitor(outputDir, "layers_trace.pb") {
+
+    constructor(outputDir: Path) : this(outputDir, TRACE_FLAGS)
+
+    private val windowManager = WindowManagerGlobal.getWindowManagerService()
+
+    override fun start() {
+        try {
+            windowManager.setLayerTracingFlags(traceFlags)
+            windowManager.isLayerTracing = true
+        } catch (e: RemoteException) {
+            throw RuntimeException("Could not start trace", e)
+        }
+    }
+
+    override fun stop() {
+        try {
+            windowManager.isLayerTracing = false
+        } catch (e: RemoteException) {
+            throw RuntimeException("Could not stop trace", e)
+        }
+    }
+
+    override val isEnabled: Boolean
+        get() = windowManager.isLayerTracing
+
+    override fun setResult(flickerRunResultBuilder: FlickerRunResult.Builder, traceFile: Path) {
+        flickerRunResultBuilder.layersTraceFile = traceFile
+    }
+
+    override fun getTracePath(builder: FlickerRunResult.Builder) = builder.layersTraceFile
+
+    companion object {
+        const val TRACE_FLAGS = 0x47 // TRACE_CRITICAL|TRACE_INPUT|TRACE_COMPOSITION|TRACE_SYNC
+    }
+}
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
deleted file mode 100644
index 3a7c012..0000000
--- a/libraries/flicker/src/com/android/server/wm/flicker/monitor/ScreenRecorder.java
+++ /dev/null
@@ -1,87 +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 static com.android.compatibility.common.util.SystemUtil.runShellCommand;
-
-import android.util.Log;
-
-import androidx.annotation.VisibleForTesting;
-
-import java.io.IOException;
-import java.nio.file.Path;
-import java.util.Locale;
-
-/** Captures screen contents and saves it as a mp4 video file. */
-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, OUTPUT_DIR, TRACE_FILE);
-    }
-
-    public ScreenRecorder(int width, int height, Path outputPath, String traceFile) {
-        super(outputPath, outputPath.resolve(traceFile));
-        mWidth = width;
-        mHeight = height;
-    }
-
-    @VisibleForTesting
-    public Path getPath() {
-        return mOutputPath;
-    }
-
-    @Override
-    public void start() {
-        mOutputPath.toFile().mkdirs();
-        String command =
-                String.format(
-                        Locale.getDefault(),
-                        "screenrecord --size %dx%d %s",
-                        mWidth,
-                        mHeight,
-                        mTraceFile);
-        mRecorderThread =
-                new Thread(
-                        () -> {
-                            try {
-                                Runtime.getRuntime().exec(command);
-                            } catch (IOException e) {
-                                Log.e(TAG, "Error executing " + command, e);
-                            }
-                        });
-        mRecorderThread.start();
-    }
-
-    @Override
-    public void stop() {
-        runShellCommand("killall -s 2 screenrecord");
-        try {
-            mRecorderThread.join();
-        } catch (InterruptedException e) {
-            // ignore
-        }
-    }
-
-    @Override
-    public boolean isEnabled() {
-        return mRecorderThread != null && mRecorderThread.isAlive();
-    }
-}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/monitor/ScreenRecorder.kt b/libraries/flicker/src/com/android/server/wm/flicker/monitor/ScreenRecorder.kt
new file mode 100644
index 0000000..9816aec
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/monitor/ScreenRecorder.kt
@@ -0,0 +1,154 @@
+/*
+ * 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.server.wm.flicker.monitor
+
+import android.content.Context
+import android.hardware.display.DisplayManager
+import android.hardware.display.VirtualDisplay
+import android.media.MediaCodecInfo
+import android.media.MediaRecorder
+import android.media.Metadata.VIDEO_FRAME_RATE
+import android.os.SystemClock
+import android.util.DisplayMetrics
+import android.util.Log
+import android.view.Surface
+import android.view.WindowManager
+import com.android.server.wm.flicker.FLICKER_TAG
+import com.android.server.wm.flicker.FlickerRunResult
+import java.nio.file.Files
+import java.nio.file.Path
+
+/** Captures screen contents and saves it as a mp4 video file.  */
+open class ScreenRecorder @JvmOverloads constructor(
+    outputPath: Path,
+    context: Context,
+    private val maxDurationMs: Int = MAX_DURATION_MS,
+    private val maxFileSizeBytes: Long = MAX_FILESIZE_BYTES,
+    private val width: Int = 720,
+    private val height: Int = 1280,
+    traceFile: String = "transition.mp4"
+) : TraceMonitor(outputPath, outputPath.resolve(traceFile)) {
+    private val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
+    private val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
+    private var mediaRecorder: MediaRecorder? = null
+    private var inputSurface: Surface? = null
+    private var virtualDisplay: VirtualDisplay? = null
+    private fun prepare() {
+        // Set up media recorder
+        val recorder = MediaRecorder()
+
+        // Set up audio source
+        recorder.setVideoSource(MediaRecorder.VideoSource.SURFACE)
+        recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
+
+        // Set up video
+        val metrics = DisplayMetrics()
+        windowManager.defaultDisplay.getRealMetrics(metrics)
+        val refreshRate = windowManager.defaultDisplay.refreshRate.toInt()
+        val vidBitRate = (width * height * refreshRate / VIDEO_FRAME_RATE
+            * VIDEO_FRAME_RATE_TO_RESOLUTION_RATIO)
+        recorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264)
+        recorder.setVideoEncodingProfileLevel(
+            MediaCodecInfo.CodecProfileLevel.AVCProfileHigh,
+            MediaCodecInfo.CodecProfileLevel.AVCLevel3)
+        recorder.setVideoSize(width, height)
+        recorder.setVideoFrameRate(refreshRate)
+        recorder.setVideoEncodingBitRate(vidBitRate)
+        recorder.setMaxDuration(maxDurationMs)
+        recorder.setMaxFileSize(maxFileSizeBytes)
+        recorder.setOutputFile(sourceTraceFilePath.toFile())
+        recorder.prepare()
+
+        // Create surface
+        inputSurface = recorder.surface
+        virtualDisplay = displayManager.createVirtualDisplay(
+            "Recording Display",
+            width,
+            height,
+            metrics.densityDpi,
+            inputSurface,
+            DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
+            null,
+            null)
+
+        this.mediaRecorder = recorder
+    }
+
+    override fun start() {
+        if (mediaRecorder != null) {
+            Log.i(FLICKER_TAG, "Screen recorder already running")
+            return
+        }
+        Files.deleteIfExists(sourceTraceFilePath)
+        require(!Files.exists(sourceTraceFilePath)) { "Could not delete old trace file" }
+        Files.createDirectories(sourceTraceFilePath.parent)
+
+        prepare()
+        Log.d(FLICKER_TAG, "Starting screen recording to file $sourceTraceFilePath")
+        mediaRecorder?.start()
+
+        var remainingTime = WAIT_TIMEOUT_MS
+        do {
+            SystemClock.sleep(WAIT_INTERVAL_MS)
+            remainingTime -= WAIT_INTERVAL_MS
+        } while (!Files.exists(sourceTraceFilePath) && remainingTime > 0)
+
+        require(Files.exists(sourceTraceFilePath)) {
+            "Screen recorder didn't start" }
+    }
+
+    override fun stop() {
+        if (mediaRecorder == null) {
+            Log.i(FLICKER_TAG, "Screen recorder was not started")
+            return
+        }
+
+        Log.d(FLICKER_TAG, "Stopping screen recording. Storing result in $sourceTraceFilePath")
+        try {
+            mediaRecorder?.stop()
+            mediaRecorder?.release()
+            inputSurface?.release()
+            virtualDisplay?.release()
+        } catch (e: Exception) {
+            throw IllegalStateException("Unable to stop screen recording", e)
+        } finally {
+            mediaRecorder = null
+        }
+    }
+
+    override val isEnabled: Boolean
+        get() = mediaRecorder != null
+
+    override fun setResult(flickerRunResultBuilder: FlickerRunResult.Builder, traceFile: Path) {
+        flickerRunResultBuilder.screenRecording = traceFile
+    }
+
+    override fun toString(): String {
+        return "ScreenRecorder($sourceTraceFilePath)"
+    }
+
+    companion object {
+        private const val WAIT_TIMEOUT_MS = 5000L
+        private const val WAIT_INTERVAL_MS = 500L
+        private const val MAX_DURATION_MS = 60 * 60 * 1000
+        private const val MAX_FILESIZE_BYTES = 100000000L
+        /**
+         * From [ScreenMediaRecorder#VIDEO_FRAME_RATE_TO_RESOLUTION_RATIO]
+         */
+        private const val VIDEO_FRAME_RATE_TO_RESOLUTION_RATIO = 6
+    }
+}
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
deleted file mode 100644
index 778db73..0000000
--- a/libraries/flicker/src/com/android/server/wm/flicker/monitor/TraceMonitor.java
+++ /dev/null
@@ -1,72 +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 static com.android.compatibility.common.util.SystemUtil.runShellCommand;
-
-import android.os.RemoteException;
-
-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 extends TransitionMonitor {
-    private static final Path TRACE_DIR = Paths.get("/data/misc/wmtrace/");
-
-    protected Path mTraceFile;
-
-    public abstract boolean isEnabled() throws RemoteException;
-
-    TraceMonitor(Path outputDir, Path traceFile) {
-        mTraceFile = traceFile;
-        mOutputPath = outputDir;
-    }
-
-    TraceMonitor(Path outputDir, String traceFileName) {
-        this(outputDir, TRACE_DIR.resolve(traceFileName));
-    }
-
-    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", src.toString(), dst.toString());
-        runShellCommand(copyCommand);
-        String removeCommand = String.format(Locale.getDefault(), "rm %s", src.toString());
-        runShellCommand(removeCommand);
-    }
-
-    @VisibleForTesting
-    public Path getOutputTraceFilePath(String testTag) {
-        return mOutputPath.resolve(testTag + "_" + mTraceFile.getFileName());
-    }
-}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/monitor/TraceMonitor.kt b/libraries/flicker/src/com/android/server/wm/flicker/monitor/TraceMonitor.kt
new file mode 100644
index 0000000..9240995
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/monitor/TraceMonitor.kt
@@ -0,0 +1,101 @@
+/*
+ * 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.server.wm.flicker.monitor
+
+import androidx.annotation.VisibleForTesting
+import com.android.compatibility.common.util.SystemUtil
+import com.android.server.wm.flicker.FlickerRunResult
+import com.google.common.io.BaseEncoding
+import java.io.FileInputStream
+import java.io.IOException
+import java.nio.ByteBuffer
+import java.nio.file.Files
+import java.nio.file.Path
+import java.security.MessageDigest
+import java.security.NoSuchAlgorithmException
+
+/**
+ * Base class for monitors containing common logic to read the trace as a byte array and save the
+ * trace to another location.
+ */
+abstract class TraceMonitor internal constructor(
+    @VisibleForTesting var outputPath: Path,
+    protected var sourceTraceFilePath: Path
+) : ITransitionMonitor {
+    override var checksum: String = ""
+        protected set
+
+    abstract val isEnabled: Boolean
+
+    abstract fun setResult(flickerRunResultBuilder: FlickerRunResult.Builder, traceFile: Path)
+
+    override fun save(testTag: String, flickerRunResultBuilder: FlickerRunResult.Builder) {
+        outputPath.toFile().mkdirs()
+        val savedTrace = outputPath.resolve("${testTag}_${sourceTraceFilePath.fileName}")
+        moveFile(sourceTraceFilePath, savedTrace)
+        require(Files.exists(savedTrace)) { "Unable to save trace file $savedTrace" }
+
+        setResult(flickerRunResultBuilder, savedTrace)
+
+        checksum = calculateChecksum(savedTrace)
+    }
+
+    fun save(testTag: String) {
+        outputPath.toFile().mkdirs()
+        val savedTrace = outputPath.resolve("${testTag}_${sourceTraceFilePath.fileName}")
+        moveFile(sourceTraceFilePath, savedTrace)
+        require(Files.exists(savedTrace)) { "Unable to save trace file $savedTrace" }
+
+        checksum = calculateChecksum(savedTrace)
+    }
+
+    private fun moveFile(src: Path, dst: Path) {
+        // 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
+        //       Moreover, the copied trace file may end up with different permissions, resulting
+        //       in b/162072200, to prevent this, ensure the files are readable after copying
+        SystemUtil.runShellCommand("cp $src $dst")
+        SystemUtil.runShellCommand("chmod a+r $dst")
+        SystemUtil.runShellCommand("rm $src")
+    }
+
+    companion object {
+        @VisibleForTesting
+        @JvmStatic
+        fun calculateChecksum(traceFile: Path): String {
+            return try {
+                val messageDigest = MessageDigest.getInstance("SHA-256")
+                val inputStream = FileInputStream(traceFile.toFile())
+                val channel = inputStream.channel
+                val buffer = ByteBuffer.allocate(2048)
+                while (channel.read(buffer) != -1) {
+                    buffer.flip()
+                    messageDigest.update(buffer)
+                    buffer.clear()
+                }
+                val hash = messageDigest.digest()
+                BaseEncoding.base16().encode(hash).toLowerCase()
+            } catch (e: NoSuchAlgorithmException) {
+                throw IllegalArgumentException("Checksum algorithm SHA-256 not found", e)
+            } catch (e: IOException) {
+                throw IllegalArgumentException("File not found", e)
+            }
+        }
+    }
+}
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
deleted file mode 100644
index ea64d33..0000000
--- a/libraries/flicker/src/com/android/server/wm/flicker/monitor/TransitionMonitor.java
+++ /dev/null
@@ -1,99 +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 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/TransitionMonitor.kt b/libraries/flicker/src/com/android/server/wm/flicker/monitor/TransitionMonitor.kt
new file mode 100644
index 0000000..3424aa2
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/monitor/TransitionMonitor.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.server.wm.flicker.monitor
+
+import com.android.server.wm.flicker.FlickerRunResult
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.Paths
+
+abstract class TransitionMonitor(
+    outputPath: Path,
+    sourceTraceFilePath: Path
+) : TraceMonitor(outputPath, sourceTraceFilePath) {
+
+    internal constructor(
+        outputDir: Path,
+        traceFileName: String
+    ) : this(outputDir, TRACE_DIR.resolve(traceFileName))
+
+    protected abstract fun getTracePath(builder: FlickerRunResult.Builder): Path?
+
+    /**
+     * Acquires the trace generated when executing the commands defined in the [predicate].
+     *
+     * @param predicate Commands to execute
+     * @throws UnsupportedOperationException If tracing is already activated
+     */
+    fun withTracing(predicate: () -> Unit): ByteArray {
+        if (this.isEnabled) {
+            throw UnsupportedOperationException("Chained 'withTracing' calls are not supported")
+        }
+        try {
+            this.start()
+            predicate()
+        } finally {
+            this.stop()
+        }
+
+        val builder = FlickerRunResult.Builder()
+        this.save("withTracing", builder)
+        val path = this.getTracePath(builder)
+
+        return path?.let {
+            Files.readAllBytes(it).also { _ -> Files.delete(it) }
+        } ?: error("Unable to acquire trace")
+    }
+
+    companion object {
+        private val TRACE_DIR = Paths.get("/data/misc/wmtrace/")
+    }
+}
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
deleted file mode 100644
index c1fab77..0000000
--- a/libraries/flicker/src/com/android/server/wm/flicker/monitor/WindowAnimationFrameStatsMonitor.java
+++ /dev/null
@@ -1,100 +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 static android.view.FrameStats.UNDEFINED_TIME_NANO;
-
-import android.app.Instrumentation;
-import android.util.Log;
-import android.view.FrameStats;
-
-/**
- * Monitors {@link android.view.WindowAnimationFrameStats} to detect janky frames.
- *
- * <p>Adapted from {@link androidx.test.jank.internal.WindowAnimationFrameStatsMonitorImpl} using
- * the same threshold to determine jank.
- */
-public class WindowAnimationFrameStatsMonitor extends TransitionMonitor {
-
-    private static final String TAG = "FLICKER";
-    // Maximum normalized error in frame duration before the frame is considered janky
-    private static final double MAX_ERROR = 0.5f;
-    // Maximum normalized frame duration before the frame is considered a pause
-    private static final double PAUSE_THRESHOLD = 15.0f;
-    private Instrumentation mInstrumentation;
-    private FrameStats mStats;
-    private int mNumJankyFrames;
-    private long mLongestFrameNano = 0L;
-
-    /** Constructs a WindowAnimationFrameStatsMonitor instance. */
-    public WindowAnimationFrameStatsMonitor(Instrumentation instrumentation) {
-        mInstrumentation = instrumentation;
-    }
-
-    private void analyze() {
-        int frameCount = mStats.getFrameCount();
-        long refreshPeriodNano = mStats.getRefreshPeriodNano();
-
-        // Skip first frame
-        for (int i = 2; i < frameCount; i++) {
-            // Handle frames that have not been presented.
-            if (mStats.getFramePresentedTimeNano(i) == UNDEFINED_TIME_NANO) {
-                // The animation must not have completed. Warn and break out of the loop.
-                Log.w(TAG, "Skipping fenced frame.");
-                break;
-            }
-            long frameDurationNano =
-                    mStats.getFramePresentedTimeNano(i) - mStats.getFramePresentedTimeNano(i - 1);
-            double normalized = (double) frameDurationNano / refreshPeriodNano;
-            if (normalized < PAUSE_THRESHOLD) {
-                if (normalized > 1.0f + MAX_ERROR) {
-                    mNumJankyFrames++;
-                }
-                mLongestFrameNano = Math.max(mLongestFrameNano, frameDurationNano);
-            }
-        }
-    }
-
-    @Override
-    public void start() {
-        // Clear out any previous data
-        mNumJankyFrames = 0;
-        mLongestFrameNano = 0;
-        mInstrumentation.getUiAutomation().clearWindowAnimationFrameStats();
-    }
-
-    @Override
-    public void stop() {
-        mStats = mInstrumentation.getUiAutomation().getWindowAnimationFrameStats();
-        analyze();
-    }
-
-    public boolean jankyFramesDetected() {
-        return mStats.getFrameCount() > 0 && mNumJankyFrames > 0;
-    }
-
-    @Override
-    public String toString() {
-        return mStats.toString()
-                + " RefreshPeriodNano:"
-                + mStats.getRefreshPeriodNano()
-                + " NumJankyFrames:"
-                + mNumJankyFrames
-                + " LongestFrameNano:"
-                + mLongestFrameNano;
-    }
-}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/monitor/WindowAnimationFrameStatsMonitor.kt b/libraries/flicker/src/com/android/server/wm/flicker/monitor/WindowAnimationFrameStatsMonitor.kt
new file mode 100644
index 0000000..787f6b8
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/monitor/WindowAnimationFrameStatsMonitor.kt
@@ -0,0 +1,94 @@
+/*
+ * 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.server.wm.flicker.monitor
+
+import android.app.Instrumentation
+import android.util.Log
+import android.view.FrameStats
+import com.android.server.wm.flicker.FLICKER_TAG
+import kotlin.math.max
+
+/**
+ * Monitors [android.view.WindowAnimationFrameStats] to detect janky frames.
+ *
+ *
+ * Adapted from [androidx.test.jank.internal.WindowAnimationFrameStatsMonitorImpl] using
+ * the same threshold to determine jank.
+ */
+open class WindowAnimationFrameStatsMonitor(
+    private val instrumentation: Instrumentation
+) : ITransitionMonitor {
+    private var frameStats: FrameStats? = null
+    private var numJankyFrames = 0
+    private var longestFrameNano = 0L
+
+    private fun analyze() {
+        val stats = frameStats ?: throw IllegalStateException("Frame status are only available " +
+                "once the monitor has been stopped")
+
+        val frameCount = stats.frameCount
+        val refreshPeriodNano = stats.refreshPeriodNano
+
+        // Skip first frame
+        for (i in 2 until frameCount) {
+            // Handle frames that have not been presented.
+            if (stats.getFramePresentedTimeNano(i) == FrameStats.UNDEFINED_TIME_NANO) {
+                // The animation must not have completed. Warn and break out of the loop.
+                Log.w(FLICKER_TAG, "Skipping fenced frame.")
+                break
+            }
+
+            val frameDurationNano =
+                    stats.getFramePresentedTimeNano(i) - stats.getFramePresentedTimeNano(i - 1)
+            val normalized = frameDurationNano.toDouble() / refreshPeriodNano
+            if (normalized < PAUSE_THRESHOLD) {
+                if (normalized > 1.0f + MAX_ERROR) {
+                    numJankyFrames++
+                }
+                longestFrameNano = max(longestFrameNano, frameDurationNano)
+            }
+        }
+    }
+
+    override fun start() {
+        // Clear out any previous data
+        frameStats = null
+        numJankyFrames = 0
+        longestFrameNano = 0
+        instrumentation.uiAutomation.clearWindowAnimationFrameStats()
+    }
+
+    override fun stop() {
+        frameStats = instrumentation.uiAutomation.windowAnimationFrameStats
+        analyze()
+    }
+
+    fun jankyFramesDetected(): Boolean = frameStats?.frameCount ?: 0 > 0 && numJankyFrames > 0
+
+    override fun toString(): String = """$frameStats
+        RefreshPeriodNano: ${frameStats?.refreshPeriodNano}
+        NumJankyFrames: $numJankyFrames
+        LongestFrameNano: $longestFrameNano""".trimIndent()
+
+    companion object {
+        // Maximum normalized error in frame duration before the frame is considered janky
+        private const val MAX_ERROR = 0.5
+
+        // Maximum normalized frame duration before the frame is considered a pause
+        private const val PAUSE_THRESHOLD = 15.0
+    }
+}
\ No newline at end of file
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
deleted file mode 100644
index c216170..0000000
--- a/libraries/flicker/src/com/android/server/wm/flicker/monitor/WindowManagerTraceMonitor.java
+++ /dev/null
@@ -1,60 +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.RemoteException;
-import android.view.IWindowManager;
-import android.view.WindowManagerGlobal;
-
-import java.nio.file.Path;
-
-/** 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() {
-        this(OUTPUT_DIR);
-    }
-
-    public WindowManagerTraceMonitor(Path outputDir) {
-        super(outputDir, TRACE_FILE);
-    }
-
-    @Override
-    public void start() {
-        try {
-            mWm.startWindowTrace();
-        } catch (RemoteException e) {
-            throw new RuntimeException("Could not start trace", e);
-        }
-    }
-
-    @Override
-    public void stop() {
-        try {
-            mWm.stopWindowTrace();
-        } catch (RemoteException e) {
-            throw new RuntimeException("Could not stop trace", e);
-        }
-    }
-
-    @Override
-    public boolean isEnabled() throws RemoteException {
-        return mWm.isWindowTraceEnabled();
-    }
-}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/monitor/WindowManagerTraceMonitor.kt b/libraries/flicker/src/com/android/server/wm/flicker/monitor/WindowManagerTraceMonitor.kt
new file mode 100644
index 0000000..0e46c81
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/monitor/WindowManagerTraceMonitor.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.server.wm.flicker.monitor
+
+import android.os.RemoteException
+import android.view.WindowManagerGlobal
+import com.android.server.wm.flicker.FlickerRunResult
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
+import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace
+import java.nio.file.Path
+
+/**
+ * Captures [WindowManagerTrace] from WindowManager.
+ *
+ * Use [WindowManagerTraceSubject.assertThat] to make assertions on the trace
+ */
+open class WindowManagerTraceMonitor(
+    outputDir: Path
+) : TransitionMonitor(outputDir, "wm_trace.pb") {
+    private val windowManager = WindowManagerGlobal.getWindowManagerService()
+    override fun start() {
+        try {
+            windowManager.startWindowTrace()
+        } catch (e: RemoteException) {
+            throw RuntimeException("Could not start trace", e)
+        }
+    }
+
+    override fun stop() {
+        try {
+            windowManager.stopWindowTrace()
+        } catch (e: RemoteException) {
+            throw RuntimeException("Could not stop trace", e)
+        }
+    }
+
+    override val isEnabled: Boolean
+        get() = windowManager.isWindowTraceEnabled
+
+    override fun setResult(flickerRunResultBuilder: FlickerRunResult.Builder, traceFile: Path) {
+        flickerRunResultBuilder.wmTraceFile = traceFile
+    }
+
+    override fun getTracePath(builder: FlickerRunResult.Builder) = builder.wmTraceFile
+}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/rules/ChangeDisplayOrientationRule.kt b/libraries/flicker/src/com/android/server/wm/flicker/rules/ChangeDisplayOrientationRule.kt
new file mode 100644
index 0000000..d7d7b8a
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/rules/ChangeDisplayOrientationRule.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2021 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.rules
+
+import android.app.Instrumentation
+import android.content.Context
+import android.os.RemoteException
+import android.view.Surface
+import android.view.WindowManager
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
+
+data class ChangeDisplayOrientationRule @JvmOverloads constructor(
+    private val targetOrientation: Int,
+    private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+) : TestWatcher() {
+    private var initialOrientation = -1
+
+    override fun starting(description: Description?) {
+        val wm = instrumentation.context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
+        initialOrientation = wm.defaultDisplay.rotation
+        setRotation(targetOrientation, instrumentation)
+    }
+
+    override fun finished(description: Description?) {
+        setRotation(initialOrientation, instrumentation)
+    }
+
+    companion object {
+        @JvmOverloads
+        fun setRotation(
+            rotation: Int,
+            instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation(),
+            wmHelper: WindowManagerStateHelper = WindowManagerStateHelper(instrumentation)
+        ) {
+            val device: UiDevice = UiDevice.getInstance(instrumentation)
+
+            try {
+                when (rotation) {
+                    Surface.ROTATION_270 -> device.setOrientationRight()
+                    Surface.ROTATION_90 -> device.setOrientationLeft()
+                    Surface.ROTATION_0 -> device.setOrientationNatural()
+                    else -> device.setOrientationNatural()
+                }
+
+                wmHelper.waitForRotation(rotation)
+                // During seamless rotation the app window is shown
+                val currWmState = wmHelper.currentState.wmState
+                if (currWmState.visibleWindows.none { it.isFullscreen }) {
+                    wmHelper.waitForNavBarStatusBarVisible()
+                }
+                wmHelper.waitForAppTransitionIdle()
+
+                // Ensure WindowManagerService wait until all animations have completed
+                instrumentation.uiAutomation.syncInputTransactions()
+            } catch (e: RemoteException) {
+                throw RuntimeException(e)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/rules/RemoveAllTasksButHomeRule.kt b/libraries/flicker/src/com/android/server/wm/flicker/rules/RemoveAllTasksButHomeRule.kt
new file mode 100644
index 0000000..e6c66db
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/rules/RemoveAllTasksButHomeRule.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2021 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.rules
+
+import android.app.ActivityTaskManager
+import android.app.WindowConfiguration
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
+
+/**
+ * Test rule to ensure no tasks as running before executing the test
+ */
+class RemoveAllTasksButHomeRule : TestWatcher() {
+    override fun starting(description: Description?) {
+        removeAllTasksButHome()
+    }
+
+    companion object {
+        @JvmStatic
+        fun removeAllTasksButHome() {
+            val atm = ActivityTaskManager.getService()
+            atm.removeRootTasksWithActivityTypes(ALL_ACTIVITY_TYPE_BUT_HOME)
+        }
+
+        private val ALL_ACTIVITY_TYPE_BUT_HOME = intArrayOf(
+            WindowConfiguration.ACTIVITY_TYPE_STANDARD,
+            WindowConfiguration.ACTIVITY_TYPE_ASSISTANT,
+            WindowConfiguration.ACTIVITY_TYPE_RECENTS,
+            WindowConfiguration.ACTIVITY_TYPE_UNDEFINED)
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/traces/FlickerFailureStrategy.kt b/libraries/flicker/src/com/android/server/wm/flicker/traces/FlickerFailureStrategy.kt
new file mode 100644
index 0000000..c41bc4b
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/traces/FlickerFailureStrategy.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2021 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.traces
+
+import com.android.server.wm.flicker.assertions.FlickerSubject
+import com.google.common.truth.FailureStrategy
+
+class FlickerFailureStrategy : FailureStrategy {
+    private lateinit var subject: FlickerSubject
+
+    fun init(subject: FlickerSubject) = apply {
+        this.subject = subject
+    }
+
+    override fun fail(error: AssertionError) {
+        require(::subject.isInitialized) { "Failure strategy is not initialized" }
+        throw FlickerSubjectException(subject, error)
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/traces/FlickerSubjectException.kt b/libraries/flicker/src/com/android/server/wm/flicker/traces/FlickerSubjectException.kt
new file mode 100644
index 0000000..7a5ea0c
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/traces/FlickerSubjectException.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2021 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.traces
+
+import com.android.server.wm.flicker.assertions.FlickerSubject
+
+/**
+ * Exception thrown by [FlickerSubject]s
+ */
+class FlickerSubjectException(
+    flickerSubject: FlickerSubject,
+    cause: Throwable
+) : AssertionError(flickerSubject.defaultFacts, cause) {
+    internal val facts = flickerSubject.defaultFacts
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/traces/FlickerTraceSubject.kt b/libraries/flicker/src/com/android/server/wm/flicker/traces/FlickerTraceSubject.kt
new file mode 100644
index 0000000..b6d2607
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/traces/FlickerTraceSubject.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2021 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.traces
+
+import com.android.server.wm.flicker.assertions.Assertion
+import com.android.server.wm.flicker.assertions.AssertionsChecker
+import com.android.server.wm.flicker.assertions.FlickerSubject
+import com.google.common.truth.FailureMetadata
+
+/**
+ * Base subject for flicker trace assertions
+ */
+abstract class FlickerTraceSubject<EntrySubject : FlickerSubject>(
+    fm: FailureMetadata,
+    data: Any?
+) : FlickerSubject(fm, data) {
+    protected val assertionsChecker = AssertionsChecker<EntrySubject>()
+    private var newAssertionBlock = true
+
+    abstract val subjects: List<EntrySubject>
+
+    protected fun addAssertion(name: String, assertion: Assertion<EntrySubject>) {
+        if (newAssertionBlock) {
+            assertionsChecker.add(name, assertion)
+        } else {
+            assertionsChecker.append(name, assertion)
+        }
+        newAssertionBlock = false
+    }
+
+    /**
+     * Run the assertions for all trace entries
+     */
+    fun forAllEntries() {
+        assertionsChecker.test(subjects)
+    }
+
+    /**
+     * User-defined entry point for the first trace entry
+     */
+    fun first(): EntrySubject = subjects.first()
+
+    /**
+     * User-defined entry point for the last trace entry
+     */
+    fun last(): EntrySubject = subjects.last()
+
+    /**
+     * Signal that the last assertion set is complete. The next assertion added will start a new
+     * set of assertions.
+     *
+     * E.g.: checkA().then().checkB()
+     *
+     * Will produce two sets of assertions (checkA) and (checkB) and checkB will only be checked
+     * after checkA passes.
+     */
+    protected fun startAssertionBlock() {
+        newAssertionBlock = true
+    }
+
+    /**
+     * Checks whether all the trace entries on the list are visible for more than one consecutive
+     * entry
+     *
+     * @param [visibleEntries] a list of all the entries with their name and index
+     */
+    protected fun visibleEntriesShownMoreThanOneConsecutiveTime(
+        visibleEntriesProvider: (EntrySubject) -> Set<String>
+    ) {
+        var lastVisible = visibleEntriesProvider(subjects.first())
+        val lastNew = lastVisible.toMutableSet()
+
+        subjects.drop(1).forEachIndexed { index, entrySubject ->
+            val currentVisible = visibleEntriesProvider(entrySubject)
+            val newVisible = currentVisible.filter { it !in lastVisible }
+            lastNew.removeAll(currentVisible)
+
+            if (lastNew.isNotEmpty()) {
+                val prevEntry = subjects[index]
+                prevEntry.fail("$lastNew is not visible for 2 entries")
+            }
+            lastNew.addAll(newVisible)
+            lastVisible = currentVisible
+        }
+
+        if (lastNew.isNotEmpty()) {
+            val lastEntry = subjects.last()
+            lastEntry.fail("$lastNew is not visible for 2 entries")
+        }
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/traces/RegionSubject.kt b/libraries/flicker/src/com/android/server/wm/flicker/traces/RegionSubject.kt
new file mode 100644
index 0000000..6c3f81b
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/traces/RegionSubject.kt
@@ -0,0 +1,591 @@
+/*
+ * Copyright (C) 2021 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.traces
+
+import androidx.annotation.VisibleForTesting
+import com.android.server.wm.flicker.assertions.FlickerSubject
+import com.android.server.wm.traces.common.Rect
+import com.android.server.wm.traces.common.RectF
+import com.android.server.wm.traces.common.Region
+import com.android.server.wm.traces.parser.toAndroidRect
+import com.android.server.wm.traces.parser.toAndroidRegion
+import com.google.common.truth.Fact
+import com.google.common.truth.FailureMetadata
+import com.google.common.truth.StandardSubjectBuilder
+
+/**
+ * Truth subject for [Rect] objects, used to make assertions over behaviors that occur on a
+ * rectangle.
+ */
+class RegionSubject(
+    fm: FailureMetadata,
+    private val subjects: List<FlickerSubject>,
+    val region: android.graphics.Region
+) : FlickerSubject(fm, region) {
+    private val topPositionSubject
+        get() = check(MSG_ERROR_TOP_POSITION).that(region.bounds.top)
+    private val bottomPositionSubject
+        get() = check(MSG_ERROR_BOTTOM_POSITION).that(region.bounds.bottom)
+    private val leftPositionSubject
+        get() = check(MSG_ERROR_LEFT_POSITION).that(region.bounds.left)
+    private val rightPositionSubject
+        get() = check(MSG_ERROR_RIGHT_POSITION).that(region.bounds.right)
+    private val areaSubject
+        get() = check(MSG_ERROR_AREA).that(region.bounds.area)
+
+    private val android.graphics.Rect.area get() = this.width() * this.height()
+    private val Rect.area get() = this.width * this.height
+
+    override val defaultFacts: String = buildString {
+        subjects.forEach { subject -> appendln(subject.defaultFacts) }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    override fun clone(): FlickerSubject {
+        return RegionSubject(fm, subjects, region)
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    override fun fail(reason: List<Fact>): FlickerSubject {
+        val newReason = reason.toMutableList()
+        return super.fail(newReason)
+    }
+
+    private fun assertLeftRightAndAreaEquals(other: android.graphics.Region) {
+        leftPositionSubject.isEqualTo(other.bounds.left)
+        rightPositionSubject.isEqualTo(other.bounds.right)
+        areaSubject.isEqualTo(other.bounds.area)
+    }
+
+    /**
+     * Asserts that the top and bottom coordinates of [RegionSubject.region] are smaller
+     * or equal to those of [region].
+     *
+     * Also checks that the left and right positions, as well as area, don't change
+     */
+    fun isHigherOrEqual(subject: RegionSubject): RegionSubject = apply {
+        isHigherOrEqual(subject.region)
+    }
+
+    /**
+     * Asserts that the top and bottom coordinates of [other] are smaller or equal to
+     * those of [region].
+     *
+     * Also checks that the left and right positions, as well as area, don't change
+     */
+    fun isHigherOrEqual(other: Rect): RegionSubject = apply {
+        isHigherOrEqual(other.toAndroidRect())
+    }
+
+    /**
+     * Asserts that the top and bottom coordinates of [other] are smaller or equal to
+     * those of [region].
+     *
+     * Also checks that the left and right positions, as well as area, don't change
+     */
+    fun isHigherOrEqual(other: Region): RegionSubject = apply {
+        isHigherOrEqual(other.toAndroidRegion())
+    }
+
+    /**
+     * Asserts that the top and bottom coordinates of [other] are smaller or equal to
+     * those of [region].
+     *
+     * Also checks that the left and right positions, as well as area, don't change
+     */
+    fun isHigherOrEqual(other: android.graphics.Rect): RegionSubject = apply {
+        isHigherOrEqual(android.graphics.Region(other))
+    }
+
+    /**
+     * Asserts that the top and bottom coordinates of [other] are smaller or equal to
+     * those of [region].
+     *
+     * Also checks that the left and right positions, as well as area, don't change
+     */
+    fun isHigherOrEqual(other: android.graphics.Region): RegionSubject = apply {
+        assertLeftRightAndAreaEquals(other)
+        topPositionSubject.isAtMost(other.bounds.top)
+        bottomPositionSubject.isAtMost(other.bounds.bottom)
+    }
+
+    /**
+     * Asserts that the top and bottom coordinates of [RegionSubject.region] are greater
+     * or equal to those of [region].
+     *
+     * Also checks that the left and right positions, as well as area, don't change
+     */
+    fun isLowerOrEqual(subject: RegionSubject): RegionSubject = apply {
+        isLowerOrEqual(subject.region)
+    }
+
+    /**
+     * Asserts that the top and bottom coordinates of [other] are greater or equal to
+     * those of [region].
+     *
+     * Also checks that the left and right positions, as well as area, don't change
+     */
+    fun isLowerOrEqual(other: Rect): RegionSubject = apply {
+        isLowerOrEqual(other.toAndroidRect())
+    }
+
+    /**
+     * Asserts that the top and bottom coordinates of [other] are greater or equal to
+     * those of [region].
+     *
+     * Also checks that the left and right positions, as well as area, don't change
+     */
+    fun isLowerOrEqual(other: android.graphics.Rect): RegionSubject = apply {
+        isLowerOrEqual(android.graphics.Region(other))
+    }
+
+    /**
+     * Asserts that the top and bottom coordinates of [other] are greater or equal to
+     * those of [region].
+     *
+     * Also checks that the left and right positions, as well as area, don't change
+     */
+    fun isLowerOrEqual(other: android.graphics.Region): RegionSubject = apply {
+        assertLeftRightAndAreaEquals(other)
+        topPositionSubject.isAtLeast(other.bounds.top)
+        bottomPositionSubject.isAtLeast(other.bounds.bottom)
+    }
+
+    /**
+     * Asserts that the top and bottom coordinates of [RegionSubject.region] are smaller than
+     * those of [region].
+     *
+     * Also checks that the left and right positions, as well as area, don't change
+     */
+    fun isHigher(subject: RegionSubject): RegionSubject = apply {
+        isHigher(subject.region)
+    }
+
+    /**
+     * Asserts that the top and bottom coordinates of [other] are smaller than those of [region].
+     *
+     * Also checks that the left and right positions, as well as area, don't change
+     */
+    fun isHigher(other: Rect): RegionSubject = apply {
+        isHigher(other.toAndroidRect())
+    }
+
+    /**
+     * Asserts that the top and bottom coordinates of [other] are smaller than those of [region].
+     *
+     * Also checks that the left and right positions, as well as area, don't change
+     */
+    fun isHigher(other: android.graphics.Rect): RegionSubject = apply {
+        isHigher(android.graphics.Region(other))
+    }
+
+    /**
+     * Asserts that the top and bottom coordinates of [other] are smaller than those of [region].
+     *
+     * Also checks that the left and right positions, as well as area, don't change
+     */
+    fun isHigher(other: android.graphics.Region): RegionSubject = apply {
+        assertLeftRightAndAreaEquals(other)
+        topPositionSubject.isLessThan(other.bounds.top)
+        bottomPositionSubject.isLessThan(other.bounds.bottom)
+    }
+
+    /**
+     * Asserts that the top and bottom coordinates of [RegionSubject.region] are greater than
+     * those of [region].
+     *
+     * Also checks that the left and right positions, as well as area, don't change
+     */
+    fun isLower(subject: RegionSubject): RegionSubject = apply {
+        isLower(subject.region)
+    }
+
+    /**
+     * Asserts that the top and bottom coordinates of [other] are greater than those of [region].
+     *
+     * Also checks that the left and right positions, as well as area, don't change
+     */
+    fun isLower(other: Rect): RegionSubject = apply {
+        isLower(other.toAndroidRect())
+    }
+
+    /**
+     * Asserts that the top and bottom coordinates of [other] are greater than those of [region].
+     *
+     * Also checks that the left and right positions, as well as area, don't change
+     */
+    fun isLower(other: android.graphics.Rect): RegionSubject = apply {
+        isLower(android.graphics.Region(other))
+    }
+
+    /**
+     * Asserts that the top and bottom coordinates of [other] are greater than those of [region].
+     *
+     * Also checks that the left and right positions, as well as area, don't change
+     */
+    fun isLower(other: android.graphics.Region): RegionSubject = apply {
+        assertLeftRightAndAreaEquals(other)
+        topPositionSubject.isGreaterThan(other.bounds.top)
+        bottomPositionSubject.isGreaterThan(other.bounds.bottom)
+    }
+
+    /**
+     * Asserts that [region] covers at most [testRegion], that is, its area doesn't cover any
+     * point outside of [testRegion].
+     *
+     * @param testRegion Expected covered area
+     */
+    fun coversAtMost(testRegion: android.graphics.Region): RegionSubject = apply {
+        val testRect = testRegion.bounds
+        val intersection = android.graphics.Region(region)
+        val covers = intersection.op(testRect, android.graphics.Region.Op.INTERSECT) &&
+            !intersection.op(region, android.graphics.Region.Op.XOR)
+
+        if (!covers) {
+            fail(Fact.fact("Region to test", testRegion),
+                Fact.fact("Covered region", region),
+                Fact.fact("Out-of-bounds region", intersection))
+        }
+    }
+
+    /**
+     * Asserts that [region] covers at most [testRegion], that is, its area doesn't cover any
+     * point outside of [testRegion].
+     *
+     * @param testRegion Expected covered area
+     */
+    fun coversAtMost(testRegion: Region): RegionSubject = apply {
+        coversAtMost(testRegion.toAndroidRegion())
+    }
+
+    /**
+     * Asserts that [region] covers at most [testRect], that is, its area doesn't cover any
+     * point outside of [testRect].
+     *
+     * @param testRect Expected covered area
+     */
+    fun coversAtMost(testRect: Rect): RegionSubject = apply {
+        coversAtMost(Region(testRect))
+    }
+
+    /**
+     * Asserts that [region] covers at most [testRect], that is, its area doesn't cover any
+     * point outside of [testRect].
+     *
+     * @param testRect Expected covered area
+     */
+    fun coversAtMost(testRect: android.graphics.Rect): RegionSubject = apply {
+        coversAtMost(android.graphics.Region(testRect))
+    }
+
+    /**
+     * Asserts that [region] covers at least [testRegion], that is, its area covers each point
+     * in the region
+     *
+     * @param testRegion Expected covered area
+     */
+    fun coversAtLeast(testRegion: android.graphics.Region): RegionSubject = apply {
+        val intersection = android.graphics.Region(region)
+        val covers = intersection.op(testRegion, android.graphics.Region.Op.INTERSECT) &&
+            !intersection.op(testRegion, android.graphics.Region.Op.XOR)
+
+        if (!covers) {
+            fail(Fact.fact("Region to test", testRegion),
+                Fact.fact("Covered region", region),
+                Fact.fact("Uncovered region", intersection))
+        }
+    }
+
+    /**
+     * Asserts that [region] covers at least [testRegion], that is, its area covers each point
+     * in the region
+     *
+     * @param testRegion Expected covered area
+     */
+    fun coversAtLeast(testRegion: Region): RegionSubject = apply {
+        coversAtLeast(testRegion.toAndroidRegion())
+    }
+
+    /**
+     * Asserts that [region] covers at least [testRect], that is, its area covers each point
+     * in the region
+     *
+     * @param testRect Expected covered area
+     */
+    fun coversAtLeast(testRect: Rect): RegionSubject = apply {
+        coversAtLeast(Region(testRect))
+    }
+
+    /**
+     * Asserts that [region] covers at least [testRect], that is, its area covers each point
+     * in the region
+     *
+     * @param testRect Expected covered area
+     */
+    fun coversAtLeast(testRect: android.graphics.Rect): RegionSubject = apply {
+        coversAtLeast(android.graphics.Region(testRect))
+    }
+
+    /**
+     * Asserts that [region] covers at exactly [testRegion]
+     *
+     * @param testRegion Expected covered area
+     */
+    fun coversExactly(testRegion: android.graphics.Region): RegionSubject = apply {
+        val intersection = android.graphics.Region(region)
+        val isNotEmpty = intersection.op(testRegion, android.graphics.Region.Op.XOR)
+
+        if (isNotEmpty) {
+            fail(Fact.fact("Region to test", testRegion),
+                Fact.fact("Covered region", region),
+                Fact.fact("Uncovered region", intersection))
+        }
+    }
+
+    /**
+     * Asserts that [region] covers at exactly [testRegion]
+     *
+     * @param testRegion Expected covered area
+     */
+    fun coversExactly(testRegion: Region): RegionSubject = apply {
+        coversExactly(testRegion.toAndroidRegion())
+    }
+
+    /**
+     * Asserts that [region] covers at exactly [testRect]
+     *
+     * @param testRect Expected covered area
+     */
+    fun coversExactly(testRect: Rect): RegionSubject = apply {
+        coversExactly(testRect.toAndroidRect())
+    }
+
+    /**
+     * Asserts that [region] covers at exactly [testRect]
+     *
+     * @param testRect Expected covered area
+     */
+    fun coversExactly(testRect: android.graphics.Rect): RegionSubject = apply {
+        coversExactly(android.graphics.Region(testRect))
+    }
+
+    /**
+     * Asserts that [region] and [testRegion] overlap
+     *
+     * @param testRegion Other area
+     */
+    fun overlaps(testRegion: android.graphics.Region): RegionSubject = apply {
+        val intersection = android.graphics.Region(region)
+        val isEmpty = !intersection.op(testRegion, android.graphics.Region.Op.INTERSECT)
+
+        if (isEmpty) {
+            fail(Fact.fact("Region to test", testRegion),
+                Fact.fact("Covered region", region),
+                Fact.fact("Overlap region", intersection))
+        }
+    }
+
+    /**
+     * Asserts that [region] and [testRegion] overlap
+     *
+     * @param testRegion Other area
+     */
+    fun overlaps(testRegion: Region): RegionSubject = apply {
+        overlaps(testRegion.toAndroidRegion())
+    }
+
+    /**
+     * Asserts that [region] and [testRect] overlap
+     *
+     * @param testRect Other area
+     */
+    fun overlaps(testRect: android.graphics.Rect): RegionSubject = apply {
+        overlaps(android.graphics.Region(testRect))
+    }
+
+    /**
+     * Asserts that [region] and [testRect] overlap
+     *
+     * @param testRect Other area
+     */
+    fun overlaps(testRect: Rect): RegionSubject = apply {
+        overlaps(testRect.toAndroidRect())
+    }
+
+    /**
+     * Asserts that [region] and [testRegion] don't overlap
+     *
+     * @param testRegion Other area
+     */
+    fun notOverlaps(testRegion: android.graphics.Region): RegionSubject = apply {
+        val intersection = android.graphics.Region(region)
+        val isEmpty = !intersection.op(testRegion, android.graphics.Region.Op.INTERSECT)
+
+        if (!isEmpty) {
+            fail(Fact.fact("Region to test", testRegion),
+                Fact.fact("Covered region", region),
+                Fact.fact("Overlap region", intersection))
+        }
+    }
+
+    /**
+     * Asserts that [region] and [testRegion] don't overlap
+     *
+     * @param testRegion Other area
+     */
+    fun notOverlaps(testRegion: Region): RegionSubject = apply {
+        notOverlaps(testRegion.toAndroidRegion())
+    }
+
+    /**
+     * Asserts that [region] and [testRect] don't overlap
+     *
+     * @param testRect Other area
+     */
+    fun notOverlaps(testRect: android.graphics.Rect): RegionSubject = apply {
+        notOverlaps(android.graphics.Region(testRect))
+    }
+
+    /**
+     * Asserts that [region] and [testRect] don't overlap
+     *
+     * @param testRect Other area
+     */
+    fun notOverlaps(testRect: Rect): RegionSubject = apply {
+        notOverlaps(testRect.toAndroidRect())
+    }
+
+    companion object {
+        @VisibleForTesting
+        const val MSG_ERROR_TOP_POSITION = "Incorrect top position"
+
+        @VisibleForTesting
+        const val MSG_ERROR_BOTTOM_POSITION = "Incorrect top position"
+
+        @VisibleForTesting
+        const val MSG_ERROR_LEFT_POSITION = "Incorrect left position"
+
+        @VisibleForTesting
+        const val MSG_ERROR_RIGHT_POSITION = "Incorrect right position"
+
+        @VisibleForTesting
+        const val MSG_ERROR_AREA = "Incorrect rect area"
+
+        private fun mergeRegions(regions: Array<Region>): android.graphics.Region {
+            val result = android.graphics.Region()
+            regions.forEach { region ->
+                region.rects.forEach { rect ->
+                    result.op(rect.toAndroidRect(), android.graphics.Region.Op.UNION)
+                }
+            }
+            return result
+        }
+
+        /**
+         * Boiler-plate Subject.Factory for RectSubject
+         */
+        @JvmStatic
+        @JvmOverloads
+        fun getFactory(
+            flickerSubjects: List<FlickerSubject> = emptyList()
+        ) = Factory { fm: FailureMetadata, region: android.graphics.Region? ->
+            val subjectRegion = region ?: android.graphics.Region()
+            RegionSubject(fm, flickerSubjects, subjectRegion)
+        }
+
+        /**
+         * User-defined entry point for existing android regions
+         */
+        @JvmStatic
+        @JvmOverloads
+        fun assertThat(
+            region: android.graphics.Region?,
+            flickerSubjects: List<FlickerSubject> = emptyList()
+        ): RegionSubject {
+            val strategy = FlickerFailureStrategy()
+            val subject = StandardSubjectBuilder.forCustomFailureStrategy(strategy)
+                .about(getFactory(flickerSubjects))
+                .that(region ?: android.graphics.Region()) as RegionSubject
+            strategy.init(subject)
+            return subject
+        }
+
+        /**
+         * User-defined entry point for existing rects
+         */
+        @JvmStatic
+        @JvmOverloads
+        fun assertThat(
+            rect: Array<Rect>,
+            flickerSubjects: List<FlickerSubject> = emptyList()
+        ): RegionSubject = assertThat(Region(rect), flickerSubjects)
+
+        /**
+         * User-defined entry point for existing rects
+         */
+        @JvmStatic
+        @JvmOverloads
+        fun assertThat(
+            rect: Rect?,
+            flickerSubjects: List<FlickerSubject> = emptyList()
+        ): RegionSubject = assertThat(Region(rect), flickerSubjects)
+
+        /**
+         * User-defined entry point for existing rects
+         */
+        @JvmStatic
+        fun assertThat(
+            rect: RectF?,
+            flickerSubjects: List<FlickerSubject> = emptyList()
+        ): RegionSubject = assertThat(rect?.toRect(), flickerSubjects)
+
+        /**
+         * User-defined entry point for existing rects
+         */
+        @JvmStatic
+        fun assertThat(
+            rect: Array<RectF>,
+            flickerSubjects: List<FlickerSubject> = emptyList()
+        ): RegionSubject = assertThat(
+            mergeRegions(rect.map { Region(it.toRect()) }.toTypedArray()),
+            flickerSubjects)
+
+        /**
+         * User-defined entry point for existing regions
+         */
+        @JvmStatic
+        @JvmOverloads
+        fun assertThat(
+            regions: Array<Region>,
+            flickerSubjects: List<FlickerSubject> = emptyList()
+        ): RegionSubject = assertThat(mergeRegions(regions), flickerSubjects)
+
+        /**
+         * User-defined entry point for existing regions
+         */
+        @JvmStatic
+        @JvmOverloads
+        fun assertThat(
+            region: Region?,
+            flickerSubjects: List<FlickerSubject> = emptyList()
+        ): RegionSubject = assertThat(region?.toAndroidRegion(), flickerSubjects)
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/traces/eventlog/EventLogSubject.kt b/libraries/flicker/src/com/android/server/wm/flicker/traces/eventlog/EventLogSubject.kt
new file mode 100644
index 0000000..704e46b
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/traces/eventlog/EventLogSubject.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2021 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.traces.eventlog
+
+import com.android.server.wm.flicker.assertions.AssertionsChecker
+import com.android.server.wm.flicker.assertions.FlickerSubject
+import com.google.common.truth.Fact
+import com.google.common.truth.FailureMetadata
+import com.google.common.truth.Subject.Factory
+import com.google.common.truth.Truth
+
+/**
+ * Truth subject for [FocusEvent] objects.
+ */
+class EventLogSubject private constructor(
+    failureMetadata: FailureMetadata,
+    private val trace: List<FocusEvent>
+) : FlickerSubject(failureMetadata, trace) {
+    override val defaultFacts: String by lazy {
+        val first = subjects.first().defaultFacts
+        val last = subjects.last().defaultFacts
+        "EventLogSubject($first, $last)"
+    }
+
+    /** {@inheritDoc} */
+    override fun clone(): FlickerSubject {
+        return EventLogSubject(fm, trace)
+    }
+
+    private val subjects by lazy {
+        trace.map { FocusEventSubject.assertThat(it, this) }
+    }
+
+    private val assertionsChecker = AssertionsChecker<FocusEventSubject>()
+    private val _focusChanges by lazy {
+        val focusList = mutableListOf<String>()
+        trace.firstOrNull { !it.hasFocus() }?.let { focusList.add(it.window) }
+        focusList + trace.filter { it.hasFocus() }.map { it.window }
+    }
+
+    fun focusChanges(windows: Array<out String>) = apply {
+        if (windows.isNotEmpty()) {
+            val focusChanges = _focusChanges
+                .dropWhile { !it.contains(windows.first()) }
+                .take(windows.size)
+            val success = windows.size <= focusChanges.size &&
+                focusChanges.zip(windows).all { (focus, search) -> focus.contains(search) }
+
+            if (!success) {
+                fail(Fact.fact("Expected", windows.joinToString(",")),
+                    Fact.fact("Found", focusChanges.joinToString(",")))
+            }
+        }
+    }
+
+    fun focusDoesNotChange() = apply {
+        check("Focus should not change")
+            .that(_focusChanges)
+            .isEmpty()
+    }
+
+    companion object {
+        /**
+         * Boiler-plate Subject.Factory for EventLogSubject
+         */
+        private val FACTORY = Factory { fm: FailureMetadata, subject: List<FocusEvent> ->
+            EventLogSubject(fm, subject)
+        }
+
+        /**
+         * User-defined entry point
+         */
+        fun assertThat(entry: List<FocusEvent>) =
+                Truth.assertAbout(FACTORY).that(entry) as EventLogSubject
+
+        /**
+         * Static method for getting the subject factory (for use with assertAbout())
+         */
+        fun entries(): Factory<EventLogSubject, List<FocusEvent>> {
+            return FACTORY
+        }
+    }
+
+    /**
+     * Run the assertions.
+     */
+    fun forAllEntries(): Unit = assertionsChecker.test(subjects)
+}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/traces/eventlog/FocusEvent.kt b/libraries/flicker/src/com/android/server/wm/flicker/traces/eventlog/FocusEvent.kt
new file mode 100644
index 0000000..94a6ec8
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/traces/eventlog/FocusEvent.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.server.wm.flicker.traces.eventlog
+
+import com.android.server.wm.traces.common.ITraceEntry
+
+class FocusEvent(
+    override val timestamp: Long,
+    val window: String,
+    val focus: Focus,
+    val reason: String
+) : ITraceEntry {
+    enum class Focus { GAINED, LOST, REQUESTED }
+
+    override fun toString(): String {
+        return "$timestamp: Focus ${focus.name} $window Reason=$reason"
+    }
+
+    fun hasFocus(): Boolean {
+        return this.focus == Focus.GAINED
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/traces/eventlog/FocusEventSubject.kt b/libraries/flicker/src/com/android/server/wm/flicker/traces/eventlog/FocusEventSubject.kt
new file mode 100644
index 0000000..fb6745d
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/traces/eventlog/FocusEventSubject.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2021 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.traces.eventlog
+
+import com.android.server.wm.flicker.assertions.FlickerSubject
+import com.android.server.wm.flicker.traces.FlickerFailureStrategy
+import com.google.common.truth.FailureMetadata
+import com.google.common.truth.StandardSubjectBuilder
+
+class FocusEventSubject(
+    fm: FailureMetadata,
+    val event: FocusEvent,
+    val trace: EventLogSubject?
+) : FlickerSubject(fm, event) {
+    override val defaultFacts by lazy { event.toString() }
+
+    /** {@inheritDoc} */
+    override fun clone(): FlickerSubject {
+        return FocusEventSubject(fm, event, trace)
+    }
+
+    fun hasFocus() {
+        check("Does not have focus")
+            .that(event.hasFocus())
+            .isTrue()
+    }
+
+    fun hasNotFocus() {
+        check("Does not have focus")
+            .that(event.hasFocus())
+            .isFalse()
+    }
+
+    companion object {
+        /**
+         * Boiler-plate Subject.Factory for EventLogSubject
+         *
+         * @param trace containing this event
+         */
+        private fun getFactory(
+            trace: EventLogSubject? = null
+        ): Factory<FocusEventSubject, FocusEvent> =
+            Factory { fm, subject -> FocusEventSubject(fm, subject, trace) }
+
+        /**
+         * User-defined entry point
+         *
+         * @param event Focus event
+         * @param trace containing this event
+         */
+        @JvmStatic
+        @JvmOverloads
+        fun assertThat(event: FocusEvent, trace: EventLogSubject? = null): FocusEventSubject {
+            val strategy = FlickerFailureStrategy()
+            val subject = StandardSubjectBuilder.forCustomFailureStrategy(strategy)
+                .about(getFactory(trace))
+                .that(event) as FocusEventSubject
+            strategy.init(subject)
+            return subject
+        }
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayerSubject.kt b/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayerSubject.kt
new file mode 100644
index 0000000..efa0352
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayerSubject.kt
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2021 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.traces.layers
+
+import android.graphics.Point
+import com.android.server.wm.flicker.assertions.Assertion
+import com.android.server.wm.flicker.traces.FlickerFailureStrategy
+import com.android.server.wm.flicker.assertions.FlickerSubject
+import com.android.server.wm.flicker.traces.RegionSubject
+import com.android.server.wm.traces.common.Bounds
+import com.android.server.wm.traces.common.layers.Layer
+import com.google.common.truth.FailureMetadata
+import com.google.common.truth.FailureStrategy
+import com.google.common.truth.StandardSubjectBuilder
+import com.google.common.truth.Subject.Factory
+
+/**
+ * Truth subject for [Layer] objects, used to make assertions over behaviors that occur on a
+ * single layer of a SurfaceFlinger state.
+ *
+ * To make assertions over a layer from a state it is recommended to create a subject
+ * using [LayerTraceEntrySubject.layer](layerName)
+ *
+ * Alternatively, it is also possible to use [LayerSubject.assertThat](myLayer) or
+ * Truth.assertAbout([LayerSubject.getFactory]), however they will provide less debug
+ * information because it uses Truth's default [FailureStrategy].
+ *
+ * Example:
+ *    val trace = LayersTraceParser.parseFromTrace(myTraceFile)
+ *    val subject = LayersTraceSubject.assertThat(trace).first()
+ *        .layer("ValidLayer")
+ *        .exists()
+ *        .hasBufferSize(BUFFER_SIZE)
+ *        .invoke { myCustomAssertion(this) }
+ */
+class LayerSubject private constructor(
+    fm: FailureMetadata,
+    val layer: Layer?,
+    val entry: LayerTraceEntrySubject?,
+    private val layerName: String? = null
+) : FlickerSubject(fm, layer) {
+    val isEmpty: Boolean get() = layer == null
+    val isNotEmpty: Boolean get() = !isEmpty
+    val isVisible: Boolean get() = layer?.isVisible == true
+    val isInvisible: Boolean get() = layer?.isVisible == false
+    val name: String get() = layer?.name ?: ""
+
+    /**
+     * Visible region calculated by the Composition Engine
+     */
+    val visibleRegion: RegionSubject get() =
+        RegionSubject.assertThat(layer?.visibleRegion, listOf(this))
+    /**
+     * Visible region calculated by the Composition Engine (when available) or calculated
+     * based on the layer bounds and transform
+     */
+    val screenBounds: RegionSubject get() =
+        RegionSubject.assertThat(layer?.screenBounds, listOf(this))
+
+    override val defaultFacts: String =
+        "${entry?.defaultFacts ?: ""}\nFrame: ${layer?.currFrame}\nLayer: ${layer?.name}"
+
+    /**
+     * If the [layer] exists, executes a custom [assertion] on the current subject
+     */
+    operator fun invoke(assertion: Assertion<Layer>): LayerSubject = apply {
+        layer ?: return exists()
+        assertion(this.layer)
+    }
+
+    /** {@inheritDoc} */
+    override fun clone(): FlickerSubject {
+        return LayerSubject(fm, layer, entry, layerName)
+    }
+
+    /**
+     * Asserts that current subject doesn't exist in the layer hierarchy
+     */
+    fun doesNotExist(): LayerSubject = apply {
+        check("doesNotExist").that(layer).isNull()
+    }
+
+    /**
+     * Asserts that current subject exists in the layer hierarchy
+     */
+    fun exists(): LayerSubject = apply {
+        check("$layerName does not exists").that(layer).isNotNull()
+    }
+
+    @Deprecated("Prefer hasBufferSize(bounds)")
+    fun hasBufferSize(size: Point): LayerSubject = apply {
+        val bounds = Bounds(size.x, size.y)
+        hasBufferSize(bounds)
+    }
+
+    /**
+     * Asserts that current subject has an [Layer.activeBuffer] with width equals to [Point.x]
+     * and height equals to [Point.y]
+     *
+     * @param size expected buffer size
+     */
+    fun hasBufferSize(size: Bounds): LayerSubject = apply {
+        layer ?: return exists()
+        val bufferSize = layer.activeBuffer?.size ?: Bounds.EMPTY
+        check("Incorrect buffer size").that(bufferSize).isEqualTo(size)
+    }
+
+    /**
+     * Asserts that current subject has an [Layer.screenBounds] with width equals to [Point.x]
+     * and height equals to [Point.y]
+     *
+     * @param size expected layer bounds size
+     */
+    fun hasLayerSize(size: Point): LayerSubject = apply {
+        layer ?: return exists()
+        val layerSize = Point(layer.screenBounds.width.toInt(), layer.screenBounds.height.toInt())
+        check("Incorrect number of layers").that(layerSize).isEqualTo(size)
+    }
+
+    /**
+     * Asserts that current subject has an [Layer.effectiveScalingMode] equals to
+     * [expectedScalingMode]
+     */
+    fun hasScalingMode(expectedScalingMode: Int): LayerSubject = apply {
+        layer ?: return exists()
+        val actualScalingMode = layer.effectiveScalingMode
+        check("Incorrect scaling mode").that(actualScalingMode).isEqualTo(expectedScalingMode)
+    }
+
+    /**
+     * Asserts that current subject has an [Layer.bufferTransform] orientation equals to
+     * [expectedOrientation]
+     */
+    fun hasBufferOrientation(expectedOrientation: Int): LayerSubject = apply {
+        layer ?: return exists()
+        // see Transform::getOrientation
+        val bufferTransformType = layer.bufferTransform.type ?: 0
+        val actualOrientation = (bufferTransformType shr 8) and 0xFF
+        check("hasBufferTransformOrientation()")
+                .that(actualOrientation).isEqualTo(expectedOrientation)
+    }
+
+    override fun toString(): String {
+        return "Layer:${layer?.name} frame#${layer?.currFrame}"
+    }
+
+    companion object {
+        /**
+         * Boiler-plate Subject.Factory for LayerSubject
+         */
+        @JvmStatic
+        @JvmOverloads
+        fun getFactory(entry: LayerTraceEntrySubject? = null) =
+            Factory { fm: FailureMetadata, subject: Layer? -> LayerSubject(fm, subject, entry) }
+
+        /**
+         * User-defined entry point for existing layers
+         */
+        @JvmStatic
+        @JvmOverloads
+        fun assertThat(
+            layer: Layer?,
+            entry: LayerTraceEntrySubject? = null
+        ): LayerSubject {
+            val strategy = FlickerFailureStrategy()
+            val subject = StandardSubjectBuilder.forCustomFailureStrategy(strategy)
+                .about(getFactory(entry))
+                .that(layer) as LayerSubject
+            strategy.init(subject)
+            return subject
+        }
+
+        /**
+         * User-defined entry point for non existing layers
+         */
+        @JvmStatic
+        internal fun assertThat(
+            name: String,
+            entry: LayerTraceEntrySubject?
+        ): LayerSubject {
+            val strategy = FlickerFailureStrategy()
+            val subject = StandardSubjectBuilder.forCustomFailureStrategy(strategy)
+                .about(getFactory(entry, name))
+                .that(null) as LayerSubject
+            strategy.init(subject)
+            return subject
+        }
+
+        /**
+         * Boiler-plate Subject.Factory for LayerSubject
+         */
+        @JvmStatic
+        internal fun getFactory(entry: LayerTraceEntrySubject?, name: String) =
+            Factory { fm: FailureMetadata, subject: Layer? ->
+                LayerSubject(fm, subject, entry, name)
+            }
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayerTraceEntrySubject.kt b/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayerTraceEntrySubject.kt
new file mode 100644
index 0000000..7a902ae
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayerTraceEntrySubject.kt
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2021 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.traces.layers
+
+import com.android.server.wm.flicker.assertions.Assertion
+import com.android.server.wm.flicker.assertions.FlickerSubject
+import com.android.server.wm.flicker.containsAny
+import com.android.server.wm.flicker.traces.FlickerFailureStrategy
+import com.android.server.wm.flicker.traces.FlickerSubjectException
+import com.android.server.wm.flicker.traces.RegionSubject
+import com.android.server.wm.traces.common.layers.Layer
+import com.android.server.wm.traces.common.layers.LayerTraceEntry
+import com.google.common.truth.ExpectFailure
+import com.google.common.truth.Fact
+import com.google.common.truth.FailureMetadata
+import com.google.common.truth.FailureStrategy
+import com.google.common.truth.StandardSubjectBuilder
+import com.google.common.truth.Subject
+
+/**
+ * Truth subject for [LayerTraceEntry] objects, used to make assertions over behaviors that
+ * occur on a single SurfaceFlinger state.
+ *
+ * To make assertions over a specific state from a trace it is recommended to create a subject
+ * using [LayersTraceSubject.assertThat](myTrace) and select the specific state using:
+ *     [LayersTraceSubject.first]
+ *     [LayersTraceSubject.last]
+ *     [LayersTraceSubject.entry]
+ *
+ * Alternatively, it is also possible to use [LayerTraceEntrySubject.assertThat](myState) or
+ * Truth.assertAbout([LayerTraceEntrySubject.getFactory]), however they will provide less debug
+ * information because it uses Truth's default [FailureStrategy].
+ *
+ * Example:
+ *    val trace = LayersTraceParser.parseFromTrace(myTraceFile)
+ *    val subject = LayersTraceSubject.assertThat(trace).first()
+ *        .contains("ValidLayer")
+ *        .notContains("ImaginaryLayer")
+ *        .coversExactly(DISPLAY_AREA)
+ *        .invoke { myCustomAssertion(this) }
+ */
+class LayerTraceEntrySubject private constructor(
+    fm: FailureMetadata,
+    val entry: LayerTraceEntry,
+    val trace: LayersTraceSubject?
+) : FlickerSubject(fm, entry) {
+    override val defaultFacts: String = "${trace?.defaultFacts ?: ""}\nEntry: $entry"
+
+    val subjects by lazy {
+        entry.flattenedLayers.map { LayerSubject.assertThat(it, this) }
+    }
+
+    /**
+     * Executes a custom [assertion] on the current subject
+     */
+    operator fun invoke(assertion: Assertion<LayerTraceEntry>): LayerTraceEntrySubject = apply {
+        assertion(this.entry)
+    }
+
+    /** {@inheritDoc} */
+    override fun clone(): FlickerSubject {
+        return LayerTraceEntrySubject(fm, entry, trace)
+    }
+
+    /**
+     * Asserts that current entry subject has an [LayerTraceEntry.timestamp] equals to
+     * [timestamp]
+     */
+    fun hasTimestamp(timestamp: Long): LayerTraceEntrySubject = apply {
+        check("Wrong  entry timestamp").that(entry.timestamp).isEqualTo(timestamp)
+    }
+
+    /**
+     * Asserts that the current SurfaceFlinger state doesn't contain layers
+     */
+    fun isEmpty(): LayerTraceEntrySubject = apply {
+        check("Entry should not be empty")
+            .that(entry.flattenedLayers)
+            .isEmpty()
+    }
+
+    /**
+     * Asserts that the current SurfaceFlinger state contains layers
+     */
+    fun isNotEmpty(): LayerTraceEntrySubject = apply {
+        check("Entry should not be empty")
+            .that(entry.flattenedLayers)
+            .isNotEmpty()
+    }
+
+    /**
+     * Asserts that the current SurfaceFlinger state has [numberLayers] layers
+     */
+    fun hasLayersSize(numberLayers: Int): LayerTraceEntrySubject = apply {
+        check("Wrong number of layers in entry")
+            .that(entry.flattenedLayers.size)
+            .isEqualTo(numberLayers)
+    }
+
+    /**
+     * Obtains the region occupied by all layers with name containing any of [partialLayerNames]
+     *
+     * @param partialLayerNames Name of the layer to search
+     * @param useCompositionEngineRegionOnly If true, uses only the region calculated from the
+     *   Composition Engine (CE) -- visibleRegion in the proto definition. Otherwise calculates
+     *   the visible region when the information is not available from the CE
+     */
+    fun visibleRegion(
+        vararg partialLayerNames: String,
+        useCompositionEngineRegionOnly: Boolean = true
+    ): RegionSubject {
+        val selectedLayers = subjects
+            .filter { it.name.containsAny(*partialLayerNames) }
+
+        if (selectedLayers.isEmpty()) {
+            fail("Could not find", partialLayerNames.joinToString(", "))
+        }
+
+        val visibleLayers = selectedLayers.filter { it.isVisible }
+        return if (useCompositionEngineRegionOnly) {
+            val visibleAreas = visibleLayers.mapNotNull { it.layer?.visibleRegion }.toTypedArray()
+            RegionSubject.assertThat(visibleAreas, selectedLayers)
+        } else {
+            val visibleAreas = visibleLayers.mapNotNull { it.layer?.screenBounds }.toTypedArray()
+            RegionSubject.assertThat(visibleAreas, selectedLayers)
+        }
+    }
+
+    /**
+     * Asserts that the SurfaceFlinger state contains a [Layer] with [Layer.name] containing any of
+     * [partialLayerNames].
+     *
+     * @param partialLayerNames Name of the layers to search
+     */
+    fun contains(vararg partialLayerNames: String): LayerTraceEntrySubject = apply {
+        val found = entry.flattenedLayers.any { it.name.containsAny(*partialLayerNames) }
+        if (partialLayerNames.isNotEmpty() && !found) {
+            fail("Could not find", partialLayerNames.joinToString(", "))
+        }
+    }
+
+    /**
+     * Asserts that the SurfaceFlinger state doesn't contain a [Layer] with [Layer.name] containing any of
+     *
+     * @param partialLayerNames Name of the layers to search
+     */
+    fun notContains(vararg partialLayerNames: String): LayerTraceEntrySubject = apply {
+        val found = entry.flattenedLayers.none { it.name.containsAny(*partialLayerNames) }
+        if (!found) {
+            fail("Could find", partialLayerNames)
+        }
+    }
+
+    /**
+     * Asserts that a [Layer] with [Layer.name] containing any of [partialLayerNames] is visible.
+     *
+     * @param partialLayerNames Name of the layers to search
+     */
+    fun isVisible(vararg partialLayerNames: String): LayerTraceEntrySubject = apply {
+        contains(*partialLayerNames)
+        var reason: Fact? = null
+        val filteredLayers = entry.flattenedLayers
+            .filter { it.name.containsAny(*partialLayerNames) }
+        for (layer in filteredLayers) {
+            if (layer.isHiddenByParent) {
+                reason = Fact.fact("Hidden by parent", layer.parent.name)
+                continue
+            }
+            if (layer.isInvisible) {
+                reason = Fact.fact("Is Invisible", layer.visibilityReason)
+                continue
+            }
+            reason = null
+            break
+        }
+
+        if (reason != null) {
+            fail(reason)
+        }
+    }
+
+    /**
+     * Asserts that a [Layer] with [Layer.name] containing any of [partialLayerNames] doesn't exist or
+     * is invisible.
+     *
+     * @param partialLayerNames Name of the layers to search
+     */
+    fun isInvisible(vararg partialLayerNames: String): LayerTraceEntrySubject = apply {
+        try {
+            isVisible(*partialLayerNames)
+        } catch (e: FlickerSubjectException) {
+            val cause = e.cause
+            require(cause is AssertionError)
+            ExpectFailure.assertThat(cause).factKeys().isNotEmpty()
+            return@apply
+        }
+        fail("Layer is visible", partialLayerNames)
+    }
+
+    /**
+     * Obtains a [LayerSubject] for the first occurrence of a [Layer] with [Layer.name]
+     * containing [name] in [frameNumber].
+     *
+     * Always returns a subject, event when the layer doesn't exist. To verify if layer
+     * actually exists in the hierarchy use [LayerSubject.exists] or [LayerSubject.doesNotExist]
+     *
+     * @return LayerSubject that can be used to make assertions on a single layer matching
+     * [name] and [frameNumber].
+     */
+    fun layer(name: String, frameNumber: Long): LayerSubject {
+        return subjects.firstOrNull {
+            it.layer?.name?.contains(name) == true &&
+                it.layer.currFrame == frameNumber
+        } ?: LayerSubject.assertThat(name, this)
+    }
+
+    override fun toString(): String {
+        return "LayerTraceEntrySubject($entry)"
+    }
+
+    companion object {
+        /**
+         * Boiler-plate Subject.Factory for LayersTraceSubject
+         */
+        private fun getFactory(
+            trace: LayersTraceSubject? = null
+        ): Factory<Subject, LayerTraceEntry> =
+            Factory { fm, subject -> LayerTraceEntrySubject(fm, subject, trace) }
+
+        /**
+         * Creates a [LayerTraceEntrySubject] to representing a SurfaceFlinger state[entry],
+         * which can be used to make assertions.
+         *
+         * @param entry SurfaceFlinger trace entry
+         * @param trace Trace that contains this entry (optional)
+         */
+        @JvmStatic
+        @JvmOverloads
+        fun assertThat(
+            entry: LayerTraceEntry,
+            trace: LayersTraceSubject? = null
+        ): LayerTraceEntrySubject {
+            val strategy = FlickerFailureStrategy()
+            val subject = StandardSubjectBuilder.forCustomFailureStrategy(strategy)
+                .about(getFactory(trace))
+                .that(entry) as LayerTraceEntrySubject
+            strategy.init(subject)
+            return subject
+        }
+
+        /**
+         * Static method for getting the subject factory (for use with assertAbout())
+         */
+        @JvmStatic
+        @JvmOverloads
+        fun entries(trace: LayersTraceSubject? = null): Factory<Subject, LayerTraceEntry> {
+            return getFactory(trace)
+        }
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayerTraceExtensions.kt b/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayerTraceExtensions.kt
new file mode 100644
index 0000000..d6a6810
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayerTraceExtensions.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2021 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.traces.layers
+
+import com.android.server.wm.traces.common.layers.LayerTraceEntry
+import com.android.server.wm.traces.parser.toAndroidRegion
+
+fun LayerTraceEntry.getVisibleBounds(layerName: String): android.graphics.Region {
+    return flattenedLayers.firstOrNull { it.name.contains(layerName) && it.isVisible }
+        ?.visibleRegion?.toAndroidRegion()
+        ?: android.graphics.Region()
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayersTraceSubject.kt b/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayersTraceSubject.kt
new file mode 100644
index 0000000..9c7ba2a
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayersTraceSubject.kt
@@ -0,0 +1,400 @@
+/*
+ * Copyright (C) 2021 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.traces.layers
+
+import android.graphics.Rect
+import android.graphics.Region
+import com.android.server.wm.flicker.assertions.Assertion
+import com.android.server.wm.flicker.assertions.FlickerSubject
+import com.android.server.wm.flicker.traces.FlickerFailureStrategy
+import com.android.server.wm.flicker.traces.FlickerTraceSubject
+import com.android.server.wm.traces.common.layers.Layer
+import com.android.server.wm.traces.common.layers.LayersTrace
+import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
+import com.google.common.truth.FailureMetadata
+import com.google.common.truth.FailureStrategy
+import com.google.common.truth.StandardSubjectBuilder
+import com.google.common.truth.Subject
+import com.google.common.truth.Subject.Factory
+
+/**
+ * Truth subject for [LayersTrace] objects, used to make assertions over behaviors that occur
+ * throughout a whole trace
+ *
+ * To make assertions over a trace it is recommended to create a subject using
+ * [LayersTraceSubject.assertThat](myTrace). Alternatively, it is also possible to use
+ * Truth.assertAbout(LayersTraceSubject.FACTORY), however it will provide less debug
+ * information because it uses Truth's default [FailureStrategy].
+ *
+ * Example:
+ *    val trace = LayersTraceParser.parseFromTrace(myTraceFile)
+ *    val subject = LayersTraceSubject.assertThat(trace)
+ *        .contains("ValidLayer")
+ *        .notContains("ImaginaryLayer")
+ *        .coversExactly(DISPLAY_AREA)
+ *        .forAllEntries()
+ *
+ * Example2:
+ *    val trace = LayersTraceParser.parseFromTrace(myTraceFile)
+ *    val subject = LayersTraceSubject.assertThat(trace) {
+ *        check("Custom check") { myCustomAssertion(this) }
+ *    }
+ */
+class LayersTraceSubject private constructor(
+    fm: FailureMetadata,
+    val trace: LayersTrace
+) : FlickerTraceSubject<LayerTraceEntrySubject>(fm, trace) {
+    override val defaultFacts: String by lazy {
+        buildString {
+            if (trace.hasSource()) {
+                append("Path: ${trace.source}")
+                append("\n")
+            }
+            append("Trace: $trace")
+        }
+    }
+
+    override val subjects by lazy {
+        trace.entries.map { LayerTraceEntrySubject.assertThat(it, this) }
+    }
+
+    /**
+     * Executes a custom [assertion] on the current subject
+     */
+    operator fun invoke(assertion: Assertion<LayersTrace>): LayersTraceSubject = apply {
+        assertion(this.trace)
+    }
+
+    /** {@inheritDoc} */
+    override fun clone(): FlickerSubject {
+        return LayersTraceSubject(fm, trace)
+    }
+
+    /**
+     * Signal that the last assertion set is complete. The next assertion added will start a new
+     * set of assertions.
+     *
+     * E.g.: checkA().then().checkB()
+     *
+     * Will produce two sets of assertions (checkA) and (checkB) and checkB will only be checked
+     * after checkA passes.
+     */
+    fun then(): LayersTraceSubject = apply {
+        startAssertionBlock()
+    }
+
+    fun isEmpty(): LayersTraceSubject = apply {
+        check("Trace is empty").that(trace).isEmpty()
+    }
+
+    fun isNotEmpty() = apply {
+        check("Trace is not empty").that(trace).isNotEmpty()
+    }
+
+    /**
+     * @return LayerSubject that can be used to make assertions on a single layer matching
+     * [name] and [frameNumber].
+     */
+    fun layer(name: String, frameNumber: Long): LayerSubject {
+        return subjects
+            .map { it.layer(name, frameNumber) }
+            .firstOrNull { it.isNotEmpty }
+            ?: LayerSubject.assertThat(null)
+    }
+
+    /**
+     * Asserts that the visible area covered by any [Layer] with [Layer.name] containing any of
+     * [layerName] covers at least [testRegion], that is, if its area of the layer's visible
+     * region covers each point in the region.
+     *
+     * @param testRegion Expected covered area
+     * @param layerName Name of the layer to search
+     */
+    fun coversAtLeast(
+        testRegion: Rect,
+        vararg layerName: String
+    ): LayersTraceSubject = this.coversAtLeast(testRegion, *layerName)
+
+    /**
+     * Asserts that the visible area covered by any [Layer] with [Layer.name] containing any of
+     * [layerName] covers at least [testRegion], that is, if its area of the layer's visible
+     * region covers each point in the region.
+     *
+     * @param testRegion Expected covered area
+     * @param layerName Name of the layer to search
+     */
+    fun coversAtLeast(
+        testRegion: com.android.server.wm.traces.common.Rect,
+        vararg layerName: String
+    ): LayersTraceSubject = this.coversAtLeast(testRegion, *layerName)
+
+    /**
+     * Asserts that the visible area covered by any [Layer] with [Layer.name] containing any of
+     * [layerName] covers at most [testRegion], that is, if the area of any layer doesn't
+     * cover any point outside of [testRegion].
+     *
+     * @param testRegion Expected covered area
+     * @param layerName Name of the layer to search
+     */
+    fun coversAtMost(
+        testRegion: Rect,
+        vararg layerName: String
+    ): LayersTraceSubject = this.coversAtMost(testRegion, *layerName)
+
+    /**
+     * Asserts that the visible area covered by any [Layer] with [Layer.name] containing any of
+     * [layerName] covers at most [testRegion], that is, if the area of any layer doesn't
+     * cover any point outside of [testRegion].
+     *
+     * @param testRegion Expected covered area
+     * @param layerName Name of the layer to search
+     */
+    fun coversAtMost(
+        testRegion: com.android.server.wm.traces.common.Rect,
+        vararg layerName: String
+    ): LayersTraceSubject = this.coversAtMost(testRegion, *layerName)
+
+    /**
+     * Asserts that the visible area covered by any [Layer] with [Layer.name] containing any of
+     * [layerName] covers at least [testRegion], that is, if its area of the layer's visible
+     * region covers each point in the region.
+     *
+     * @param testRegion Expected covered area
+     * @param layerName Name of the layer to search
+     */
+    fun coversAtLeast(
+        testRegion: Region,
+        vararg layerName: String
+    ): LayersTraceSubject = apply {
+        addAssertion("coversAtLeast($testRegion, ${layerName.joinToString(", ")})") {
+            it.visibleRegion(*layerName).coversAtLeast(testRegion)
+        }
+    }
+
+    /**
+     * Asserts that the visible area covered by any [Layer] with [Layer.name] containing any of
+     * [layerName] covers at least [testRegion], that is, if its area of the layer's visible
+     * region covers each point in the region.
+     *
+     * @param testRegion Expected covered area
+     * @param layerName Name of the layer to search
+     */
+    fun coversAtLeast(
+        testRegion: com.android.server.wm.traces.common.Region,
+        vararg layerName: String
+    ): LayersTraceSubject = apply {
+        addAssertion("coversAtLeast($testRegion, ${layerName.joinToString(", ")})") {
+            it.visibleRegion(*layerName).coversAtLeast(testRegion)
+        }
+    }
+
+    /**
+     * Asserts that the visible area covered by any [Layer] with [Layer.name] containing any of
+     * [layerName] covers at most [testRegion], that is, if the area of any layer doesn't
+     * cover any point outside of [testRegion].
+     *
+     * @param testRegion Expected covered area
+     * @param layerName Name of the layer to search
+     */
+    fun coversAtMost(
+        testRegion: Region,
+        vararg layerName: String
+    ): LayersTraceSubject = apply {
+        addAssertion("coversAtMost($testRegion, ${layerName.joinToString(", ")}") {
+            it.visibleRegion(*layerName).coversAtMost(testRegion)
+        }
+    }
+
+    /**
+     * Asserts that the visible area covered by any [Layer] with [Layer.name] containing any of
+     * [layerName] covers at most [testRegion], that is, if the area of any layer doesn't
+     * cover any point outside of [testRegion].
+     *
+     * @param testRegion Expected covered area
+     * @param layerName Name of the layer to search
+     */
+    fun coversAtMost(
+        testRegion: com.android.server.wm.traces.common.Region,
+        vararg layerName: String
+    ): LayersTraceSubject = apply {
+        addAssertion("coversAtMost($testRegion, ${layerName.joinToString(", ")}") {
+            it.visibleRegion(*layerName).coversAtMost(testRegion)
+        }
+    }
+
+    /**
+     * Checks that all visible layers are shown for more than one consecutive entry
+     */
+    @JvmOverloads
+    fun visibleLayersShownMoreThanOneConsecutiveEntry(
+        ignoreLayers: List<String> = listOf(WindowManagerStateHelper.SPLASH_SCREEN_NAME,
+            WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME)
+    ): LayersTraceSubject = apply {
+        visibleEntriesShownMoreThanOneConsecutiveTime { subject ->
+            subject.entry.visibleLayers
+                .filter { ignoreLayers.none { layerName -> layerName in it.name } }
+                .map { it.name }
+                .toSet()
+        }
+    }
+
+    /**
+     * Asserts that a [Layer] with [Layer.name] containing any of [layerName] has a visible region
+     * of exactly [expectedVisibleRegion] in trace entries.
+     *
+     * @param layerName Name of the layer to search
+     * @param expectedVisibleRegion Expected visible region of the layer
+     */
+    fun coversExactly(
+        expectedVisibleRegion: Region,
+        vararg layerName: String
+    ): LayersTraceSubject = apply {
+        addAssertion("coversExactly(${layerName.joinToString(", ")}$expectedVisibleRegion)") {
+            it.visibleRegion(*layerName).coversExactly(expectedVisibleRegion)
+        }
+    }
+
+    /**
+     * Asserts that each entry in the trace doesn't contain a [Layer] with [Layer.name]
+     * containing [layerName].
+     *
+     * @param layerName Name of the layer to search
+     */
+    fun notContains(vararg layerName: String): LayersTraceSubject =
+        apply {
+            addAssertion("notContains(${layerName.joinToString(", ")})") {
+                it.notContains(*layerName)
+            }
+        }
+
+    /**
+     * Asserts that each entry in the trace contains a [Layer] with [Layer.name] containing any of
+     * [layerName].
+     *
+     * @param layerName Name of the layer to search
+     */
+    fun contains(vararg layerName: String): LayersTraceSubject =
+        apply { addAssertion("contains(${layerName.joinToString(", ")})") {
+            it.contains(*layerName) }
+        }
+
+    /**
+     * Asserts that each entry in the trace contains a [Layer] with [Layer.name] containing any of
+     * [layerName] that is visible.
+     *
+     * @param layerName Name of the layer to search
+     */
+    fun isVisible(vararg layerName: String): LayersTraceSubject =
+        apply { addAssertion("isVisible(${layerName.joinToString(", ")})") {
+            it.isVisible(*layerName) }
+        }
+
+    /**
+     * Asserts that each entry in the trace doesn't contain a [Layer] with [Layer.name]
+     * containing [layerName] or that the layer is not visible .
+     *
+     * @param layerName Name of the layer to search
+     */
+    fun isInvisible(vararg layerName: String): LayersTraceSubject =
+        apply {
+            addAssertion("hidesLayer(${layerName.joinToString(", ")})") {
+                it.isInvisible(*layerName)
+            }
+        }
+
+    /**
+     * Executes a custom [assertion] on the current subject
+     */
+    operator fun invoke(
+        name: String,
+        assertion: Assertion<LayerTraceEntrySubject>
+    ): LayersTraceSubject = apply { addAssertion(name, assertion) }
+
+    fun hasFrameSequence(name: String, frameNumbers: Iterable<Long>): LayersTraceSubject = apply {
+        val firstFrame = frameNumbers.first()
+        val entries = trace.entries.asSequence()
+            // map entry to buffer layers with name
+            .map { it.getLayerWithBuffer(name) }
+            // removing all entries without the layer
+            .filterNotNull()
+            // removing all entries with the same frame number
+            .distinctBy { it.currFrame }
+            // drop until the first frame we are interested in
+            .dropWhile { layer -> layer.currFrame != firstFrame }
+
+        var numFound = 0
+        val frameNumbersMatch = entries.zip(frameNumbers.asSequence()) { layer, frameNumber ->
+            numFound++
+            layer.currFrame == frameNumber
+        }.all { it }
+        val allFramesFound = frameNumbers.count() == numFound
+        if (!allFramesFound || !frameNumbersMatch) {
+            val message = "Could not find Layer:" + name +
+                " with frame sequence:" + frameNumbers.joinToString(",") +
+                " Found:\n" + entries.joinToString("\n")
+            fail(message)
+        }
+    }
+
+    /**
+     * Run the assertions for all trace entries within the specified time range
+     */
+    fun forRange(startTime: Long, endTime: Long) {
+        val subjectsInRange = subjects.filter { it.entry.timestamp in startTime..endTime }
+        assertionsChecker.test(subjectsInRange)
+    }
+
+    /**
+     * User-defined entry point for the trace entry with [timestamp]
+     *
+     * @param timestamp of the entry
+     */
+    fun entry(timestamp: Long): LayerTraceEntrySubject =
+        subjects.first { it.entry.timestamp == timestamp }
+
+    companion object {
+        /**
+         * Boiler-plate Subject.Factory for LayersTraceSubject
+         */
+        private val FACTORY: Factory<Subject, LayersTrace> =
+            Factory { fm, subject -> LayersTraceSubject(fm, subject) }
+
+        /**
+         * Creates a [LayersTraceSubject] to representing a SurfaceFlinger trace,
+         * which can be used to make assertions.
+         *
+         * @param trace SurfaceFlinger trace
+         */
+        @JvmStatic
+        fun assertThat(trace: LayersTrace): LayersTraceSubject {
+            val strategy = FlickerFailureStrategy()
+            val subject = StandardSubjectBuilder.forCustomFailureStrategy(strategy)
+                .about(FACTORY)
+                .that(trace) as LayersTraceSubject
+            strategy.init(subject)
+            return subject
+        }
+
+        /**
+         * Static method for getting the subject factory (for use with assertAbout())
+         */
+        @JvmStatic
+        fun entries(): Factory<Subject, LayersTrace> {
+            return FACTORY
+        }
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/traces/windowmanager/WindowManagerStateSubject.kt b/libraries/flicker/src/com/android/server/wm/flicker/traces/windowmanager/WindowManagerStateSubject.kt
new file mode 100644
index 0000000..ea642ec
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/traces/windowmanager/WindowManagerStateSubject.kt
@@ -0,0 +1,705 @@
+/*
+ * Copyright (C) 2021 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.traces.windowmanager
+
+import android.content.ComponentName
+import android.view.Display
+import com.android.server.wm.flicker.assertions.Assertion
+import com.android.server.wm.flicker.assertions.FlickerSubject
+import com.android.server.wm.flicker.containsAny
+import com.android.server.wm.flicker.traces.FlickerFailureStrategy
+import com.android.server.wm.flicker.traces.RegionSubject
+import com.android.server.wm.traces.common.windowmanager.WindowManagerState
+import com.android.server.wm.traces.common.windowmanager.windows.Activity
+import com.android.server.wm.traces.common.windowmanager.windows.WindowState
+import com.android.server.wm.traces.parser.toActivityName
+import com.android.server.wm.traces.parser.toAndroidRegion
+import com.android.server.wm.traces.parser.toWindowName
+import com.google.common.truth.Fact
+import com.google.common.truth.FailureMetadata
+import com.google.common.truth.FailureStrategy
+import com.google.common.truth.StandardSubjectBuilder
+import com.google.common.truth.Subject
+import com.google.common.truth.Subject.Factory
+
+/**
+ * Truth subject for [WindowManagerState] objects, used to make assertions over behaviors that
+ * occur on a single WM state.
+ *
+ * To make assertions over a specific state from a trace it is recommended to create a subject
+ * using [WindowManagerTraceSubject.assertThat](myTrace) and select the specific state using:
+ *     [WindowManagerTraceSubject.first]
+ *     [WindowManagerTraceSubject.last]
+ *     [WindowManagerTraceSubject.entry]
+ *
+ * Alternatively, it is also possible to use [WindowManagerStateSubject.assertThat](myState) or
+ * Truth.assertAbout([WindowManagerStateSubject.getFactory]), however they will provide less debug
+ * information because it uses Truth's default [FailureStrategy].
+ *
+ * Example:
+ *    val trace = WindowManagerTraceParser.parseFromTrace(myTraceFile)
+ *    val subject = WindowManagerTraceSubject.assertThat(trace).first()
+ *        .contains("ValidWindow")
+ *        .notContains("ImaginaryWindow")
+ *        .showsAboveAppWindow("NavigationBar")
+ *        .invoke { myCustomAssertion(this) }
+ */
+class WindowManagerStateSubject private constructor(
+    fm: FailureMetadata,
+    val wmState: WindowManagerState,
+    val trace: WindowManagerTraceSubject?
+) : FlickerSubject(fm, wmState) {
+    override val defaultFacts = "${trace?.defaultFacts ?: ""}\nEntry: $wmState"
+
+    val subjects by lazy {
+        wmState.windowStates.map { WindowStateSubject.assertThat(it, this) }
+    }
+
+    /**
+     * Executes a custom [assertion] on the current subject
+     */
+    operator fun invoke(assertion: Assertion<WindowManagerState>): WindowManagerStateSubject =
+        apply { assertion(this.wmState) }
+
+    /** {@inheritDoc} */
+    override fun clone(): FlickerSubject {
+        return WindowManagerStateSubject(fm, wmState, trace)
+    }
+
+    /**
+     * Asserts that the current WindowManager state doesn't contain [WindowState]s
+     */
+    fun isEmpty(): WindowManagerStateSubject = apply {
+        check("State is empty")
+            .that(wmState.windowStates)
+            .isEmpty()
+    }
+
+    /**
+     * Asserts that the current WindowManager state contains [WindowState]s
+     */
+    fun isNotEmpty(): WindowManagerStateSubject = apply {
+        check("State is not empty")
+            .that(wmState.windowStates)
+            .isNotEmpty()
+    }
+
+    /**
+     * Obtains the region occupied by all windows with name containing any of [partialWindowTitles]
+     *
+     * @param partialWindowTitles Name of the layer to search
+     */
+    fun frameRegion(vararg partialWindowTitles: String): RegionSubject {
+        val selectedWindows = subjects.filter { it.name.containsAny(*partialWindowTitles) }
+
+        if (selectedWindows.isEmpty()) {
+            fail("Could not find", selectedWindows.joinToString(", "))
+        }
+
+        val visibleWindows = selectedWindows.filter { it.isVisible }
+        val frameRegions = visibleWindows.mapNotNull { it.windowState?.frameRegion }.toTypedArray()
+        return RegionSubject.assertThat(frameRegions, selectedWindows)
+    }
+
+    /**
+     * Asserts that the WindowManager state contains a [WindowState] with [WindowState.title]
+     * containing any of [partialWindowTitles].
+     *
+     * @param partialWindowTitles window titles to search to search
+     */
+    fun contains(vararg partialWindowTitles: String): WindowManagerStateSubject = apply {
+        val found = if (partialWindowTitles.isNotEmpty()) {
+            wmState.windowStates.any { it.name.containsAny(*partialWindowTitles) }
+        } else {
+            wmState.windowStates.isNotEmpty()
+        }
+
+        if (!found) {
+            fail("Could not find", partialWindowTitles.joinToString(", "))
+        }
+    }
+
+    /**
+     * Asserts that the WindowManager state doesn't contain a [WindowState] with
+     * [WindowState.title] containing [partialWindowTitles].
+     *
+     * @param partialWindowTitles Title of the window to search
+     */
+    fun notContains(vararg partialWindowTitles: String): WindowManagerStateSubject = apply {
+        val found = wmState.windowStates.none { it.name.containsAny(*partialWindowTitles) }
+        if (!found) {
+            fail("Could find", partialWindowTitles.joinToString(", "))
+        }
+    }
+
+    /**
+     * Asserts that a [WindowState] with [WindowState.title] containing [partialWindowTitles] is visible.
+     *
+     * @param partialWindowTitles Title of the window to search
+     */
+    fun isVisible(vararg partialWindowTitles: String): WindowManagerStateSubject = apply {
+        wmState.windowStates.checkVisibility(*partialWindowTitles, isVisible = true)
+    }
+
+    /**
+     * Asserts that a [WindowState] with [WindowState.title] containing [partialWindowTitles] doesn't
+     * exist or is invisible.
+     *
+     * @param partialWindowTitles Title of the window to search
+     */
+    fun isInvisible(vararg partialWindowTitles: String): WindowManagerStateSubject = apply {
+        wmState.windowStates.checkVisibility(*partialWindowTitles, isVisible = false)
+    }
+
+    private fun Array<WindowState>.checkIsVisible(vararg partialWindowTitles: String) {
+        this@WindowManagerStateSubject.contains(*partialWindowTitles)
+        val visibleWindows = this.filter { it.isVisible }
+            .filter { it.name.containsAny(*partialWindowTitles) }
+
+        if (visibleWindows.isEmpty()) {
+            fail("Is Invisible", partialWindowTitles.joinToString(", "))
+        }
+    }
+
+    private fun Array<WindowState>.checkIsInvisible(vararg partialWindowTitles: String) {
+        try {
+            notContains(*partialWindowTitles)
+        } catch (e: AssertionError) {
+            val invisibleWindows = this.filterNot { it.isVisible }
+                .filter { it.name.containsAny(*partialWindowTitles) }
+            if (invisibleWindows.isEmpty()) {
+                fail("Is Visible", partialWindowTitles.joinToString(", "))
+            }
+        }
+    }
+
+    private fun Array<WindowState>.checkVisibility(
+        vararg partialWindowTitles: String,
+        isVisible: Boolean
+    ) {
+        if (isVisible) {
+            checkIsVisible(*partialWindowTitles)
+        } else {
+            checkIsInvisible(*partialWindowTitles)
+        }
+    }
+
+    /**
+     * Asserts that the non-app window ([WindowManagerState.nonAppWindows]) with title
+     * containing [partialWindowTitles] exists, is above all app windows ([WindowManagerState.appWindows])
+     * and has a visibility equal to [isVisible]
+     *
+     * This assertion can be used, for example, to assert that the Status and Navigation bars
+     * are visible and shown above the app
+     *
+     * @param partialWindowTitles window title to search
+     * @param isVisible if the found window should be visible or not
+     */
+    @JvmOverloads
+    fun isAboveAppWindow(
+        vararg partialWindowTitles: String,
+        isVisible: Boolean = true
+    ): WindowManagerStateSubject = apply {
+        wmState.aboveAppWindows.checkVisibility(*partialWindowTitles, isVisible = isVisible)
+    }
+
+    /**
+     * Asserts that the non-app window ([WindowManagerState.nonAppWindows]) with title
+     * containing [partialWindowTitles] exists, is below all app windows ([WindowManagerState.appWindows])
+     * and has a visibility equal to [isVisible]
+     *
+     * This assertion can be used, for example, to assert that the wallpaper is visible and
+     * shown below the app
+     *
+     * @param partialWindowTitles window title to search
+     * @param isVisible if the found window should be visible or not
+     */
+    @JvmOverloads
+    fun isBelowAppWindow(
+        vararg partialWindowTitles: String,
+        isVisible: Boolean = true
+    ): WindowManagerStateSubject = apply {
+        wmState.belowAppWindows.checkVisibility(*partialWindowTitles, isVisible = isVisible)
+    }
+
+    /**
+     * Asserts that a window A with title containing [aboveWindowTitle] exists,
+     * a window B with title containing [belowWindowTitle] also exists, and that
+     * A is shown above B.
+     *
+     * This assertion can be used, for example, to assert that a PIP window is shown above
+     * other apps.
+     *
+     * @param aboveWindowTitle name of the window that should be above
+     * @param belowWindowTitle name of the window that should be below
+     */
+    fun isAboveWindow(aboveWindowTitle: String, belowWindowTitle: String) {
+        // windows are ordered by z-order, from top to bottom
+        val aboveZ = wmState.windowStates.indexOfFirst { aboveWindowTitle in it.name }
+        val belowZ = wmState.windowStates.indexOfFirst { belowWindowTitle in it.name }
+
+        contains(aboveWindowTitle)
+        contains(belowWindowTitle)
+        if (aboveZ >= belowZ) {
+            fail("$aboveWindowTitle is above $belowWindowTitle")
+        }
+    }
+
+    /**
+     * Asserts that the WindowManager state contains a non-app [WindowState] with
+     * [WindowState.title] containing [partialWindowTitles] and that its visibility is
+     * equal to [isVisible]
+     *
+     * @param partialWindowTitles window title to search
+     * @param isVisible if the found window should be visible or not
+     */
+    @JvmOverloads
+    fun containsNonAppWindow(
+        vararg partialWindowTitles: String,
+        isVisible: Boolean = true
+    ): WindowManagerStateSubject = apply {
+        wmState.nonAppWindows.checkVisibility(*partialWindowTitles, isVisible = isVisible)
+    }
+
+    /**
+     * Asserts that the title of the top visible app window in the state contains any
+     * of [partialWindowTitles]
+     *
+     * @param partialWindowTitles window title to search
+     */
+    fun showsAppWindowOnTop(vararg partialWindowTitles: String): WindowManagerStateSubject = apply {
+        contains(*partialWindowTitles)
+        val windowOnTop = wmState.topVisibleAppWindow.containsAny(*partialWindowTitles)
+
+        if (!windowOnTop) {
+            fail(Fact.fact("Not on top", partialWindowTitles.joinToString(", ")),
+                Fact.fact("Found", wmState.topVisibleAppWindow))
+        }
+    }
+
+    /**
+     * Asserts that the [WindowState.bounds] of the [WindowState] with [WindowState.title]
+     * contained in any of [partialWindowTitles] don't overlap.
+     *
+     * @param partialWindowTitles Title of the windows that should not overlap
+     */
+    fun noWindowsOverlap(vararg partialWindowTitles: String): WindowManagerStateSubject = apply {
+        partialWindowTitles.forEach { contains(it) }
+        val foundWindows = partialWindowTitles.toSet()
+            .associateWith { title -> wmState.windowStates.find { it.name.contains(title) } }
+            // keep entries only for windows that we actually found by removing nulls
+            .filterValues { it != null }
+            .mapValues { (_, v) -> v!!.frameRegion }
+
+        val regions = foundWindows.entries.toList()
+        for (i in regions.indices) {
+            val (ourTitle, ourRegion) = regions[i]
+            for (j in i + 1 until regions.size) {
+                val (otherTitle, otherRegion) = regions[j]
+                if (ourRegion.toAndroidRegion().op(otherRegion.toAndroidRegion(),
+                        android.graphics.Region.Op.INTERSECT)) {
+                    fail(Fact.fact("Overlap", ourTitle), Fact.fact("Overlap", otherTitle))
+                }
+            }
+        }
+    }
+
+    /**
+     * Asserts that the WindowManager state contains an app [WindowState] with
+     * [WindowState.title] containing [partialWindowTitles] and that its visibility
+     * is equal to [isVisible]
+     *
+     * @param partialWindowTitles window title to search
+     * @param isVisible if the found window should be visible or not
+     */
+    @JvmOverloads
+    fun containsAppWindow(
+        vararg partialWindowTitles: String,
+        isVisible: Boolean = true
+    ): WindowManagerStateSubject = apply {
+        wmState.appWindows.checkVisibility(*partialWindowTitles, isVisible = isVisible)
+    }
+
+    /**
+     * Asserts that the display with id [displayId] has rotation [rotation]
+     *
+     * @param rotation to assert
+     * @param displayId of the target display
+     */
+    @JvmOverloads
+    fun hasRotation(
+        rotation: Int,
+        displayId: Int = Display.DEFAULT_DISPLAY
+    ): WindowManagerStateSubject = apply {
+        check("Rotation should be $rotation")
+            .that(rotation)
+            .isEqualTo(wmState.getRotation(displayId))
+    }
+
+    /**
+     * Asserts that the display with id [displayId] has rotation [rotation]
+     *
+     * @param rotation to assert
+     * @param displayId of the target display
+     */
+    @JvmOverloads
+    fun isNotRotation(
+        rotation: Int,
+        displayId: Int = Display.DEFAULT_DISPLAY
+    ): WindowManagerStateSubject = apply {
+        check("Rotation should not be $rotation")
+            .that(rotation)
+            .isNotEqualTo(wmState.getRotation(displayId))
+    }
+
+    /**
+     * Asserts that the WindowManager state contains a [WindowState] with [WindowState.title]
+     * equal to [ComponentName.toWindowName] and an [Activity] with [Activity.title] equal to
+     * [ComponentName.toActivityName]
+     *
+     * @param activity Component name to search
+     */
+    fun contains(activity: ComponentName): WindowManagerStateSubject = apply {
+        val windowName = activity.toWindowName()
+        val activityName = activity.toActivityName()
+        check("Activity=$activityName must exist.")
+            .that(wmState.containsActivity(activityName)).isTrue()
+        check("Window=$windowName must exits.")
+            .that(wmState.containsWindow(windowName)).isTrue()
+    }
+
+    /**
+     * Asserts that the WindowManager state doesn't contain a [WindowState] with [WindowState.title]
+     * equal to [ComponentName.toWindowName] nor an [Activity] with [Activity.title] equal to
+     * [ComponentName.toActivityName]
+     *
+     * @param activity Component name to search
+     */
+    fun notContains(activity: ComponentName): WindowManagerStateSubject = apply {
+        val windowName = activity.toWindowName()
+        val activityName = activity.toActivityName()
+        check("Activity=$activityName must NOT exist.")
+            .that(wmState.containsActivity(activityName)).isFalse()
+        check("Window=$windowName must NOT exits.")
+            .that(wmState.containsWindow(windowName)).isFalse()
+    }
+
+    @JvmOverloads
+    fun isRecentsActivityVisible(visible: Boolean = true): WindowManagerStateSubject = apply {
+        if (wmState.isHomeRecentsComponent) {
+            isHomeActivityVisible()
+        } else {
+            check("Recents activity is ${if (visible) "" else "not"} visible")
+                .that(wmState.isRecentsActivityVisible)
+                .isEqualTo(visible)
+        }
+    }
+
+    /**
+     * Asserts that the WindowManager state is valid, that is, if it has:
+     *   - a resumed activity
+     *   - a focused activity
+     *   - a focused window
+     *   - a front window
+     *   - a focused app
+     */
+    fun isValid(): WindowManagerStateSubject = apply {
+        check("Must have stacks").that(wmState.stackCount).isGreaterThan(0)
+        // TODO: Update when keyguard will be shown on multiple displays
+        if (!wmState.keyguardControllerState.isKeyguardShowing) {
+            check("There should be at least one resumed activity in the system.")
+                .that(wmState.resumedActivitiesCount).isGreaterThan(0)
+        }
+        check("Must have focus activity.")
+            .that(wmState.focusedActivity).isNotEmpty()
+        wmState.rootTasks.forEach { aStack ->
+            val stackId = aStack.rootTaskId
+            aStack.tasks.forEach { aTask ->
+                check("Stack can only contain its own tasks")
+                    .that(stackId).isEqualTo(aTask.rootTaskId)
+            }
+        }
+        check("Must have front window.")
+            .that(wmState.frontWindow).isNotEmpty()
+        check("Must have focused window.")
+            .that(wmState.focusedWindow).isNotEmpty()
+        check("Must have app.")
+            .that(wmState.focusedApp).isNotEmpty()
+    }
+
+    /**
+     * Asserts that the [WindowManagerState.focusedActivity] and [WindowManagerState.focusedApp]
+     * match [activity]
+     *
+     * @param activity Component name to search
+     */
+    fun hasFocusedActivity(activity: ComponentName): WindowManagerStateSubject = apply {
+        val activityComponentName = activity.toActivityName()
+        check("Focused activity invalid")
+            .that(activityComponentName)
+            .isEqualTo(wmState.focusedActivity)
+        check("Focused app invalid")
+            .that(activityComponentName)
+            .isEqualTo(wmState.focusedApp)
+    }
+
+    /**
+     * Asserts that the [WindowManagerState.focusedActivity] and [WindowManagerState.focusedApp]
+     * don't match [activity]
+     *
+     * @param activity Component name to search
+     */
+    fun hasNotFocusedActivity(activity: ComponentName): WindowManagerStateSubject = apply {
+        val activityComponentName = activity.toActivityName()
+        check("Has focused activity")
+            .that(wmState.focusedActivity)
+            .isNotEqualTo(activityComponentName)
+        check("Has focused app")
+            .that(wmState.focusedApp)
+            .isNotEqualTo(activityComponentName)
+    }
+
+    /**
+     * Asserts that the display [displayId] has a [WindowManagerState.focusedApp]
+     * matching [activity]
+     *
+     * @param activity Component name to search
+     */
+    @JvmOverloads
+    fun hasFocusedApp(
+        activity: ComponentName,
+        displayId: Int = Display.DEFAULT_DISPLAY
+    ): WindowManagerStateSubject = apply {
+        val activityComponentName = activity.toActivityName()
+        check("Focused app invalid")
+            .that(activityComponentName)
+            .isEqualTo(wmState.getDisplay(displayId)?.focusedApp)
+    }
+
+    /**
+     * Asserts that WindowManager state has a [WindowManagerState.resumedActivities]
+     * matching [activity]
+     *
+     * @param activity Component name to search
+     */
+    fun hasResumedActivity(activity: ComponentName): WindowManagerStateSubject = apply {
+        val activityComponentName = activity.toActivityName()
+        check("Invalid resumed activity")
+            .that(wmState.resumedActivities)
+            .asList()
+            .contains(activityComponentName)
+    }
+
+    /**
+     * Asserts that WindowManager state [WindowManagerState.resumedActivities] doesn't
+     * match [activity]
+     *
+     * @param activity Component name to search
+     */
+    fun hasNotResumedActivity(activity: ComponentName): WindowManagerStateSubject = apply {
+        val activityComponentName = activity.toActivityName()
+        check("Has resumed activity")
+            .that(wmState.resumedActivities)
+            .asList()
+            .doesNotContain(activityComponentName)
+    }
+
+    /**
+     * Asserts that title of the [WindowManagerState.focusedWindow] on the state matches
+     * [windowTitle]
+     *
+     * @param windowTitle window title to search
+     */
+    fun isFocused(windowTitle: String): WindowManagerStateSubject = apply {
+        check("Invalid focused window")
+            .that(windowTitle)
+            .isEqualTo(wmState.focusedWindow)
+    }
+
+    /**
+     * Asserts that [WindowManagerState.focusedWindow] on the WindowManager state doesn't
+     * match [windowTitle]
+     *
+     * @param windowTitle window title to search
+     */
+    fun isWindowNotFocused(windowTitle: String): WindowManagerStateSubject = apply {
+        check("Has focused window")
+            .that(wmState.focusedWindow)
+            .isNotEqualTo(windowTitle)
+    }
+
+    /**
+     * Asserts that the WindowManager state contains a [WindowState] with [WindowState.title]
+     * equal to [ComponentName.toWindowName] and an [Activity] with [Activity.title] equal to
+     * [ComponentName.toActivityName] and both are visible
+     *
+     * @param activity Component name to search
+     */
+    fun isVisible(activity: ComponentName): WindowManagerStateSubject =
+        hasActivityAndWindowVisibility(activity, visible = true)
+
+    /**
+     * Asserts that the WindowManager state contains a [WindowState] with [WindowState.title]
+     * equal to [ComponentName.toWindowName] and an [Activity] with [Activity.title] equal to
+     * [ComponentName.toActivityName] and both are invisible
+     *
+     * @param activity Component name to search
+     */
+    fun isInvisible(activity: ComponentName): WindowManagerStateSubject =
+        hasActivityAndWindowVisibility(activity, visible = false)
+
+    private fun hasActivityAndWindowVisibility(
+        activity: ComponentName,
+        visible: Boolean
+    ): WindowManagerStateSubject = apply {
+        // Check existence of activity and window.
+        val windowName = activity.toWindowName()
+        val activityName = activity.toActivityName()
+        check("Activity=$activityName must exist.")
+            .that(wmState.containsActivity(activityName)).isTrue()
+        check("Window=$windowName must exist.")
+            .that(wmState.containsWindow(windowName)).isTrue()
+
+        // Check visibility of activity and window.
+        check("Activity=$activityName must ${if (visible) "" else " NOT"} be visible.")
+            .that(visible).isEqualTo(wmState.isActivityVisible(activityName))
+        check("Window=$windowName must ${if (visible) "" else " NOT"} have shown surface.")
+            .that(visible).isEqualTo(wmState.isWindowSurfaceShown(windowName))
+    }
+
+    /**
+     * Asserts that the WindowManager state home activity visibility is equal to [isVisible]
+     *
+     * @param isVisible if the home activity should be visible of not
+     */
+    @JvmOverloads
+    fun isHomeActivityVisible(isVisible: Boolean = true): WindowManagerStateSubject = apply {
+        if (isVisible) {
+            check("Home activity doesn't exist")
+                .that(wmState.homeActivity)
+                .isNotNull()
+
+            check("Home activity is not visible")
+                .that(wmState.homeActivity?.isVisible)
+                .isTrue()
+        } else {
+            check("Home activity is visible")
+                .that(wmState.homeActivity?.isVisible ?: false)
+                .isFalse()
+        }
+    }
+
+    /**
+     * Asserts that the IME surface is visible in the display [displayId]
+     */
+    @JvmOverloads
+    fun isImeWindowVisible(
+        displayId: Int = Display.DEFAULT_DISPLAY
+    ): WindowManagerStateSubject = apply {
+        val imeWinState = wmState.inputMethodWindowState
+        check("IME window must exist")
+            .that(imeWinState).isNotNull()
+        check("IME window must be shown")
+            .that(imeWinState?.isSurfaceShown ?: false).isTrue()
+        check("IME window must be on the given display")
+            .that(displayId).isEqualTo(imeWinState?.displayId ?: -1)
+    }
+
+    /**
+     * Asserts that the IME surface is invisible in the display [displayId]
+     */
+    @JvmOverloads
+    fun isImeWindowInvisible(
+        displayId: Int = Display.DEFAULT_DISPLAY
+    ): WindowManagerStateSubject = apply {
+        val imeWinState = wmState.inputMethodWindowState
+        check("IME window must not be shown")
+            .that(imeWinState?.isSurfaceShown ?: false).isFalse()
+        if (imeWinState?.isSurfaceShown == true) {
+            check("IME window must not be on the given display")
+                .that(displayId).isNotEqualTo(imeWinState.displayId)
+        }
+    }
+
+    /**
+     * Asserts that an activity [activity] exists and is in PIP mode
+     */
+    fun isInPipMode(
+        activity: ComponentName
+    ): WindowManagerStateSubject = apply {
+        val windowName = activity.toWindowName()
+        contains(windowName)
+        val pinnedWindows = wmState.pinnedWindows
+            .map { it.title }
+        check("Window not in PIP mode")
+            .that(pinnedWindows)
+            .contains(windowName)
+    }
+
+    /**
+     * Obtains a [WindowStateSubject] for the first occurrence of a [WindowState] with
+     * [WindowState.title] containing [name].
+     *
+     * Always returns a subject, event when the layer doesn't exist. To verify if layer
+     * actually exists in the hierarchy use [WindowStateSubject.exists] or
+     * [WindowStateSubject.doesNotExist]
+     *
+     * @return WindowStateSubject that can be used to make assertions on a single [WindowState]
+     * matching [name].
+     */
+    fun windowState(name: String): WindowStateSubject {
+        return subjects.firstOrNull {
+            it.windowState?.name?.contains(name) == true
+        } ?: WindowStateSubject.assertThat(name, this)
+    }
+
+    override fun toString(): String {
+        return "WindowManagerStateSubject($wmState)"
+    }
+
+    companion object {
+        /**
+         * Boiler-plate Subject.Factory for WindowManagerStateSubject
+         *
+         * @param trace containing the entry
+         */
+        private fun getFactory(
+            trace: WindowManagerTraceSubject? = null
+        ): Factory<Subject, WindowManagerState> =
+            Factory { fm, subject -> WindowManagerStateSubject(fm, subject, trace) }
+
+        /**
+         * User-defined entry point
+         *
+         * @param entry to assert
+         * @param trace containing the entry
+         */
+        @JvmStatic
+        @JvmOverloads
+        fun assertThat(
+            entry: WindowManagerState,
+            trace: WindowManagerTraceSubject? = null
+        ): WindowManagerStateSubject {
+            val strategy = FlickerFailureStrategy()
+            val subject = StandardSubjectBuilder.forCustomFailureStrategy(strategy)
+                .about(getFactory(trace))
+                .that(entry) as WindowManagerStateSubject
+            strategy.init(subject)
+            return subject
+        }
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/traces/windowmanager/WindowManagerTraceSubject.kt b/libraries/flicker/src/com/android/server/wm/flicker/traces/windowmanager/WindowManagerTraceSubject.kt
new file mode 100644
index 0000000..c790f97
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/traces/windowmanager/WindowManagerTraceSubject.kt
@@ -0,0 +1,514 @@
+/*
+ * Copyright (C) 2021 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.traces.windowmanager
+
+import com.android.server.wm.flicker.assertions.Assertion
+import com.android.server.wm.flicker.assertions.FlickerSubject
+import com.android.server.wm.flicker.traces.FlickerFailureStrategy
+import com.android.server.wm.flicker.traces.FlickerTraceSubject
+import com.android.server.wm.traces.common.Rect
+import com.android.server.wm.traces.common.Region
+import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace
+import com.android.server.wm.traces.common.windowmanager.windows.WindowState
+import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
+import com.google.common.truth.FailureMetadata
+import com.google.common.truth.FailureStrategy
+import com.google.common.truth.StandardSubjectBuilder
+import com.google.common.truth.Subject
+
+/**
+ * Truth subject for [WindowManagerTrace] objects, used to make assertions over behaviors that
+ * occur throughout a whole trace.
+ *
+ * To make assertions over a trace it is recommended to create a subject using
+ * [WindowManagerTraceSubject.assertThat](myTrace). Alternatively, it is also possible to use
+ * Truth.assertAbout(WindowManagerTraceSubject.FACTORY), however it will provide less debug
+ * information because it uses Truth's default [FailureStrategy].
+ *
+ * Example:
+ *    val trace = WindowManagerTraceParser.parseFromTrace(myTraceFile)
+ *    val subject = WindowManagerTraceSubject.assertThat(trace)
+ *        .contains("ValidWindow")
+ *        .notContains("ImaginaryWindow")
+ *        .showsAboveAppWindow("NavigationBar")
+ *        .forAllEntries()
+ *
+ * Example2:
+ *    val trace = WindowManagerTraceParser.parseFromTrace(myTraceFile)
+ *    val subject = WindowManagerTraceSubject.assertThat(trace) {
+ *        check("Custom check") { myCustomAssertion(this) }
+ *    }
+ */
+class WindowManagerTraceSubject private constructor(
+    fm: FailureMetadata,
+    val trace: WindowManagerTrace
+) : FlickerTraceSubject<WindowManagerStateSubject>(fm, trace) {
+    override val defaultFacts: String = buildString {
+        if (trace.hasSource()) {
+            append("Path: ${trace.source}")
+            append("\n")
+        }
+        append("Trace: $trace")
+    }
+
+    override val subjects by lazy {
+        trace.entries.map { WindowManagerStateSubject.assertThat(it, this) }
+    }
+
+    /** {@inheritDoc} */
+    override fun clone(): FlickerSubject {
+        return WindowManagerTraceSubject(fm, trace)
+    }
+
+    /**
+     * Signal that the last assertion set is complete. The next assertion added will start a new
+     * set of assertions.
+     *
+     * E.g.: checkA().then().checkB()
+     *
+     * Will produce two sets of assertions (checkA) and (checkB) and checkB will only be checked
+     * after checkA passes.
+     */
+    fun then(): WindowManagerTraceSubject =
+        apply { startAssertionBlock() }
+
+    /**
+     * 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
+     */
+    fun skipUntilFirstAssertion(): WindowManagerTraceSubject =
+        apply { assertionsChecker.skipUntilFirstAssertion() }
+
+    fun isEmpty(): WindowManagerTraceSubject = apply {
+        check("Trace is empty").that(trace).isEmpty()
+    }
+
+    fun isNotEmpty(): WindowManagerTraceSubject = apply {
+        check("Trace is not empty").that(trace).isNotEmpty()
+    }
+
+    /**
+     * Checks if the non-app window with title containing [partialWindowTitle] exists above the app
+     * windows and is visible
+     *
+     * @param partialWindowTitle window title to search
+     */
+    fun showsAboveAppWindow(vararg partialWindowTitle: String): WindowManagerTraceSubject = apply {
+        addAssertion("showsAboveAppWindow($partialWindowTitle)") {
+            it.isAboveAppWindow(*partialWindowTitle)
+        }
+    }
+
+    /**
+     * Checks if the non-app window with title containing [partialWindowTitle] exists above the app
+     * windows and is invisible
+     *
+     * @param partialWindowTitle window title to search
+     */
+    fun hidesAboveAppWindow(vararg partialWindowTitle: String): WindowManagerTraceSubject = apply {
+        addAssertion("hidesAboveAppWindow($partialWindowTitle)") {
+            it.isAboveAppWindow(*partialWindowTitle, isVisible = false)
+        }
+    }
+
+    /**
+     * Checks if the non-app window with title containing [partialWindowTitle] exists below the app
+     * windows and is visible
+     *
+     * @param partialWindowTitle window title to search
+     */
+    fun showsBelowAppWindow(vararg partialWindowTitle: String): WindowManagerTraceSubject = apply {
+        addAssertion("showsBelowAppWindow($partialWindowTitle)") {
+            it.isBelowAppWindow(*partialWindowTitle)
+        }
+    }
+
+    /**
+     * Checks if the non-app window with title containing [partialWindowTitle] exists below the app
+     * windows and is invisible
+     *
+     * @param partialWindowTitle window title to search
+     */
+    fun hidesBelowAppWindow(vararg partialWindowTitle: String): WindowManagerTraceSubject = apply {
+        addAssertion("hidesBelowAppWindow($partialWindowTitle)") {
+            it.isBelowAppWindow(*partialWindowTitle, isVisible = false)
+        }
+    }
+
+    /**
+     * Checks if non-app window with title containing the [partialWindowTitle] exists above or
+     * below the app windows and is visible
+     *
+     * @param partialWindowTitle window title to search
+     */
+    fun showsNonAppWindow(vararg partialWindowTitle: String): WindowManagerTraceSubject = apply {
+        addAssertion("showsNonAppWindow($partialWindowTitle)") {
+            it.containsNonAppWindow(*partialWindowTitle)
+        }
+    }
+
+    /**
+     * Checks if non-app window with title containing the [partialWindowTitle] exists above or
+     * below the app windows and is invisible
+     *
+     * @param partialWindowTitle window title to search
+     */
+    fun hidesNonAppWindow(vararg partialWindowTitle: String): WindowManagerTraceSubject = apply {
+        addAssertion("hidesNonAppWindow($partialWindowTitle)") {
+            it.containsNonAppWindow(*partialWindowTitle, isVisible = false)
+        }
+    }
+
+    /**
+     * Checks if an app window with title containing the [partialWindowTitles] is on top
+     *
+     * @param partialWindowTitles window title to search
+     */
+    fun showsAppWindowOnTop(vararg partialWindowTitles: String): WindowManagerTraceSubject = apply {
+        val assertionName = "showsAppWindowOnTop(${partialWindowTitles.joinToString(",")})"
+        addAssertion(assertionName) {
+            check("No window titles to search")
+                .that(partialWindowTitles)
+                .isNotEmpty()
+            it.showsAppWindowOnTop(*partialWindowTitles)
+        }
+    }
+
+    /**
+     * Checks if app window with title containing the [partialWindowTitle] is not on top
+     *
+     * @param partialWindowTitle window title to search
+     */
+    fun appWindowNotOnTop(vararg partialWindowTitle: String): WindowManagerTraceSubject = apply {
+        addAssertion("hidesAppWindowOnTop($partialWindowTitle)") {
+            it.containsAppWindow(*partialWindowTitle, isVisible = false)
+        }
+    }
+
+    /**
+     * Checks if app window with title containing the [partialWindowTitle] is visible
+     *
+     * @param partialWindowTitle window title to search
+     */
+    fun showsAppWindow(vararg partialWindowTitle: String): WindowManagerTraceSubject = apply {
+        addAssertion("showsAppWindow($partialWindowTitle)") {
+            it.containsAppWindow(*partialWindowTitle, isVisible = true)
+        }
+    }
+
+    /**
+     * Checks if app window with title containing the [partialWindowTitle] is invisible
+     *
+     * @param partialWindowTitle window title to search
+     */
+    fun hidesAppWindow(vararg partialWindowTitle: String): WindowManagerTraceSubject = apply {
+        addAssertion("hidesAppWindow($partialWindowTitle)") {
+            it.containsAppWindow(*partialWindowTitle, isVisible = false)
+        }
+    }
+
+    /**
+     * Checks if no app windows containing the [partialWindowTitles] overlap with each other.
+     *
+     * @param partialWindowTitles partial titles of windows to check
+     */
+    fun noWindowsOverlap(vararg partialWindowTitles: String): WindowManagerTraceSubject = apply {
+        val repr = partialWindowTitles.joinToString(", ")
+        require(partialWindowTitles.size > 1) {
+            "Must give more than one window to check! (Given $repr)"
+        }
+        addAssertion("noWindowsOverlap($repr)") {
+            it.noWindowsOverlap(*partialWindowTitles)
+        }
+    }
+
+    /**
+     * Checks if the window named [aboveWindowTitle] is above the one named [belowWindowTitle] in
+     * z-order.
+     *
+     * @param aboveWindowTitle partial name of the expected top window
+     * @param belowWindowTitle partial name of the expected bottom window
+     */
+    fun isAboveWindow(
+        aboveWindowTitle: String,
+        belowWindowTitle: String
+    ): WindowManagerTraceSubject = apply {
+        require(aboveWindowTitle != belowWindowTitle)
+        addAssertion("$aboveWindowTitle is above $belowWindowTitle") {
+            it.isAboveWindow(aboveWindowTitle, belowWindowTitle)
+        }
+    }
+
+    /**
+     * Asserts that the visible area covered by the first [WindowState] with [WindowState.title]
+     * containing [partialWindowTitle] covers at least [testRegion], that is, if its area of the
+     * window's bounds cover each point in the region.
+     *
+     * @param partialWindowTitle Name of the layer to search
+     * @param testRegion Expected visible area of the window
+     */
+    fun coversAtLeast(
+        testRegion: Region,
+        partialWindowTitle: String
+    ): WindowManagerTraceSubject = apply {
+        addAssertion("coversAtLeastRegion($partialWindowTitle, $testRegion)") {
+            it.frameRegion(partialWindowTitle).coversAtLeast(testRegion)
+        }
+    }
+
+    /**
+     * Asserts that the visible area covered by the first [WindowState] with [WindowState.title]
+     * containing [partialWindowTitle] covers at least [testRegion], that is, if its area of the
+     * window's bounds cover each point in the region.
+     *
+     * @param partialWindowTitle Name of the layer to search
+     * @param testRegion Expected visible area of the window
+     */
+    fun coversAtLeast(
+        testRegion: android.graphics.Region,
+        partialWindowTitle: String
+    ): WindowManagerTraceSubject = apply {
+        addAssertion("coversAtLeastRegion($partialWindowTitle, $testRegion)") {
+            it.frameRegion(partialWindowTitle).coversAtLeast(testRegion)
+        }
+    }
+
+    /**
+     * Asserts that the visible area covered by the first [WindowState] with [WindowState.title]
+     * containing [partialWindowTitle] covers at least [testRect], that is, if its area of the
+     * window's bounds cover each point in the region.
+     *
+     * @param partialWindowTitle Name of the layer to search
+     * @param testRect Expected visible area of the window
+     */
+    fun coversAtLeast(
+        testRect: android.graphics.Rect,
+        partialWindowTitle: String
+    ): WindowManagerTraceSubject = apply {
+        addAssertion("coversAtLeastRegion($partialWindowTitle, $testRect)") {
+            it.frameRegion(partialWindowTitle).coversAtLeast(testRect)
+        }
+    }
+
+    /**
+     * Asserts that the visible area covered by the first [WindowState] with [WindowState.title]
+     * containing [partialWindowTitle] covers at least [testRect], that is, if its area of the
+     * window's bounds cover each point in the region.
+     *
+     * @param partialWindowTitle Name of the layer to search
+     * @param testRect Expected visible area of the window
+     */
+    fun coversAtLeast(
+        testRect: Rect,
+        partialWindowTitle: String
+    ): WindowManagerTraceSubject = apply {
+        addAssertion("coversAtLeastRegion($partialWindowTitle, $testRect)") {
+            it.frameRegion(partialWindowTitle).coversAtLeast(testRect)
+        }
+    }
+
+    /**
+     * Asserts that the visible area covered by the first [WindowState] with [WindowState.title]
+     * containing [partialWindowTitle] covers at most [testRegion], that is, if the area of the
+     * window state bounds don't cover any point outside of [testRegion].
+     *
+     * @param partialWindowTitle Name of the layer to search
+     * @param testRegion Expected visible area of the window
+     */
+    fun coversAtMost(
+        testRegion: Region,
+        partialWindowTitle: String
+    ): WindowManagerTraceSubject = apply {
+        addAssertion("coversAtMostRegion($partialWindowTitle, $testRegion)") {
+            it.frameRegion(partialWindowTitle).coversAtMost(testRegion)
+        }
+    }
+
+    /**
+     * Asserts that the visible area covered by the first [WindowState] with [WindowState.title]
+     * containing [partialWindowTitle] covers at most [testRegion], that is, if the area of the
+     * window state bounds don't cover any point outside of [testRegion].
+     *
+     * @param partialWindowTitle Name of the layer to search
+     * @param testRegion Expected visible area of the window
+     */
+    fun coversAtMost(
+        testRegion: android.graphics.Region,
+        partialWindowTitle: String
+    ): WindowManagerTraceSubject = apply {
+        addAssertion("coversAtMostRegion($partialWindowTitle, $testRegion)") {
+            it.frameRegion(partialWindowTitle).coversAtMost(testRegion)
+        }
+    }
+
+    /**
+     * Asserts that the visible area covered by the first [WindowState] with [WindowState.title]
+     * containing [partialWindowTitle] covers at most [testRect], that is, if the area of the
+     * window state bounds don't cover any point outside of [testRect].
+     *
+     * @param partialWindowTitle Name of the layer to search
+     * @param testRect Expected visible area of the window
+     */
+    fun coversAtMost(
+        testRect: Rect,
+        partialWindowTitle: String
+    ): WindowManagerTraceSubject = apply {
+        addAssertion("coversAtMostRegion($partialWindowTitle, $testRect)") {
+            it.frameRegion(partialWindowTitle).coversAtMost(testRect)
+        }
+    }
+
+    /**
+     * Asserts that the visible area covered by the first [WindowState] with [WindowState.title]
+     * containing [partialWindowTitle] covers at most [testRect], that is, if the area of the
+     * window state bounds don't cover any point outside of [testRect].
+     *
+     * @param partialWindowTitle Name of the layer to search
+     * @param testRect Expected visible area of the window
+     */
+    fun coversAtMost(
+        testRect: android.graphics.Rect,
+        partialWindowTitle: String
+    ): WindowManagerTraceSubject = apply {
+        addAssertion("coversAtMostRegion($partialWindowTitle, $testRect)") {
+            it.frameRegion(partialWindowTitle).coversAtMost(testRect)
+        }
+    }
+
+    /**
+     * Asserts that the visible area covered by the first [WindowState] with [WindowState.title]
+     * containing [partialWindowTitle] covers exactly [testRegion].
+     *
+     * @param partialWindowTitle Name of the layer to search
+     * @param testRegion Expected visible area of the window
+     */
+    fun coversExactly(
+        testRegion: android.graphics.Region,
+        partialWindowTitle: String
+    ): WindowManagerTraceSubject = apply {
+        addAssertion("coversExactly($partialWindowTitle, $testRegion)") {
+            it.frameRegion(partialWindowTitle).coversExactly(testRegion)
+        }
+    }
+
+    /**
+     * Asserts that the visible area covered by the first [WindowState] with [WindowState.title]
+     * containing [partialWindowTitle] covers exactly [testRect].
+     *
+     * @param partialWindowTitle Name of the layer to search
+     * @param testRect Expected visible area of the window
+     */
+    fun coversExactly(
+        testRect: Rect,
+        partialWindowTitle: String
+    ): WindowManagerTraceSubject = apply {
+        addAssertion("coversExactly($partialWindowTitle, $testRect)") {
+            it.frameRegion(partialWindowTitle).coversExactly(testRect)
+        }
+    }
+
+    /**
+     * Asserts that the visible area covered by the first [WindowState] with [WindowState.title]
+     * containing [partialWindowTitle] covers exactly [testRect].
+     *
+     * @param partialWindowTitle Name of the layer to search
+     * @param testRect Expected visible area of the window
+     */
+    fun coversExactly(
+        testRect: android.graphics.Rect,
+        partialWindowTitle: String
+    ): WindowManagerTraceSubject = apply {
+        addAssertion("coversExactly($partialWindowTitle, $testRect)") {
+            it.frameRegion(partialWindowTitle).coversExactly(testRect)
+        }
+    }
+
+    /**
+     * Checks that all visible layers are shown for more than one consecutive entry
+     */
+    fun visibleWindowsShownMoreThanOneConsecutiveEntry(
+        ignoreWindows: List<String> = listOf(WindowManagerStateHelper.SPLASH_SCREEN_NAME,
+            WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME)
+    ): WindowManagerTraceSubject = apply {
+        visibleEntriesShownMoreThanOneConsecutiveTime { subject ->
+            subject.wmState.windowStates
+                .filter { it.isVisible }
+                .filter {
+                    ignoreWindows.none { windowName -> windowName in it.title }
+                }
+                .map { it.name }
+                .toSet()
+        }
+    }
+
+    /**
+     * Executes a custom [assertion] on the current subject
+     */
+    operator fun invoke(
+        name: String,
+        assertion: Assertion<WindowManagerStateSubject>
+    ): WindowManagerTraceSubject = apply { addAssertion(name, assertion) }
+
+    /**
+     * Run the assertions for all trace entries within the specified time range
+     */
+    fun forRange(startTime: Long, endTime: Long) {
+        val subjectsInRange = subjects.filter { it.wmState.timestamp in startTime..endTime }
+        assertionsChecker.test(subjectsInRange)
+    }
+
+    /**
+     * User-defined entry point for the trace entry with [timestamp]
+     *
+     * @param timestamp of the entry
+     */
+    fun entry(timestamp: Long): WindowManagerStateSubject =
+        subjects.first { it.wmState.timestamp == timestamp }
+
+    companion object {
+        /**
+         * Boiler-plate Subject.Factory for WmTraceSubject
+         */
+        private val FACTORY: Factory<Subject, WindowManagerTrace> =
+            Factory { fm, subject -> WindowManagerTraceSubject(fm, subject) }
+
+        /**
+         * Creates a [WindowManagerTraceSubject] representing a WindowManager trace,
+         * which can be used to make assertions.
+         *
+         * @param trace WindowManager trace
+         */
+        @JvmStatic
+        fun assertThat(trace: WindowManagerTrace): WindowManagerTraceSubject {
+            val strategy = FlickerFailureStrategy()
+            val subject = StandardSubjectBuilder.forCustomFailureStrategy(strategy)
+                .about(FACTORY)
+                .that(trace) as WindowManagerTraceSubject
+            strategy.init(subject)
+            return subject
+        }
+
+        /**
+         * Static method for getting the subject factory (for use with assertAbout())
+         */
+        @JvmStatic
+        fun entries(): Factory<Subject, WindowManagerTrace> = FACTORY
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/traces/windowmanager/WindowStateSubject.kt b/libraries/flicker/src/com/android/server/wm/flicker/traces/windowmanager/WindowStateSubject.kt
new file mode 100644
index 0000000..3666d03
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/traces/windowmanager/WindowStateSubject.kt
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2021 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.traces.windowmanager
+
+import com.android.server.wm.flicker.assertions.Assertion
+import com.android.server.wm.flicker.assertions.FlickerSubject
+import com.android.server.wm.flicker.traces.FlickerFailureStrategy
+import com.android.server.wm.flicker.traces.RegionSubject
+import com.android.server.wm.traces.common.windowmanager.windows.WindowState
+import com.google.common.truth.FailureMetadata
+import com.google.common.truth.FailureStrategy
+import com.google.common.truth.StandardSubjectBuilder
+
+/**
+ * Truth subject for [WindowState] objects, used to make assertions over behaviors that occur on a
+ * single [WindowState] of a WM state.
+ *
+ * To make assertions over a layer from a state it is recommended to create a subject
+ * using [WindowManagerStateSubject.windowState](windowStateName)
+ *
+ * Alternatively, it is also possible to use [WindowStateSubject.assertThat](myWindow) or
+ * Truth.assertAbout([WindowStateSubject.getFactory]), however they will provide less debug
+ * information because it uses Truth's default [FailureStrategy].
+ *
+ * Example:
+ *    val trace = WindowManagerTraceParser.parseFromTrace(myTraceFile)
+ *    val subject = WindowManagerTraceSubject.assertThat(trace).first()
+ *        .windowState("ValidWindow")
+ *        .exists()
+ *        { myCustomAssertion(this) }
+ */
+class WindowStateSubject private constructor(
+    fm: FailureMetadata,
+    val windowState: WindowState?,
+    private val entry: WindowManagerStateSubject?,
+    private val windowTitle: String? = null
+) : FlickerSubject(fm, windowState) {
+    val isEmpty: Boolean get() = windowState == null
+    val isNotEmpty: Boolean get() = !isEmpty
+    val isVisible: Boolean get() = windowState?.isVisible == true
+    val isInvisible: Boolean get() = windowState?.isVisible == false
+    val name: String get() = windowState?.name ?: windowTitle ?: ""
+    val frame: RegionSubject get() = RegionSubject.assertThat(windowState?.frame, listOf(this))
+
+    override val defaultFacts: String =
+        "${entry?.defaultFacts ?: ""}\nWindowTitle: ${windowState?.title}"
+
+    /**
+     * If the [windowState] exists, executes a custom [assertion] on the current subject
+     */
+    operator fun invoke(assertion: Assertion<WindowState>): WindowStateSubject = apply {
+        windowState ?: return exists()
+        assertion(this.windowState)
+    }
+
+    /** {@inheritDoc} */
+    override fun clone(): FlickerSubject {
+        return WindowStateSubject(fm, windowState, entry, windowTitle)
+    }
+
+    /**
+     * Asserts that current subject doesn't exist in the window hierarchy
+     */
+    fun doesNotExist(): WindowStateSubject = apply {
+        check("doesNotExist").that(windowState).isNull()
+    }
+
+    /**
+     * Asserts that current subject exists in the window hierarchy
+     */
+    fun exists(): WindowStateSubject = apply {
+        check("$windowTitle does not exists").that(windowState).isNotNull()
+    }
+
+    override fun toString(): String {
+        return "WindowState:${windowState?.name}"
+    }
+
+    companion object {
+        /**
+         * Boiler-plate Subject.Factory for LayerSubject
+         */
+        @JvmStatic
+        @JvmOverloads
+        fun getFactory(entry: WindowManagerStateSubject? = null) =
+            Factory { fm: FailureMetadata, subject: WindowState? ->
+                WindowStateSubject(fm, subject, entry)
+            }
+
+        /**
+         * User-defined entry point for existing layers
+         */
+        @JvmStatic
+        @JvmOverloads
+        fun assertThat(
+            layer: WindowState?,
+            entry: WindowManagerStateSubject? = null
+        ): WindowStateSubject {
+            val strategy = FlickerFailureStrategy()
+            val subject = StandardSubjectBuilder.forCustomFailureStrategy(strategy)
+                .about(getFactory(entry))
+                .that(layer) as WindowStateSubject
+            strategy.init(subject)
+            return subject
+        }
+
+        /**
+         * User-defined entry point for non existing layers
+         */
+        @JvmStatic
+        internal fun assertThat(
+            name: String,
+            entry: WindowManagerStateSubject?
+        ): WindowStateSubject {
+            val strategy = FlickerFailureStrategy()
+            val subject = StandardSubjectBuilder.forCustomFailureStrategy(strategy)
+                .about(getFactory(entry, name))
+                .that(null) as WindowStateSubject
+            strategy.init(subject)
+            return subject
+        }
+
+        /**
+         * Boiler-plate Subject.Factory for LayerSubject
+         */
+        @JvmStatic
+        internal fun getFactory(entry: WindowManagerStateSubject?, name: String) =
+            Factory { fm: FailureMetadata, subject: WindowState? ->
+                WindowStateSubject(fm, subject, entry, name)
+            }
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/Bounds.kt b/libraries/flicker/src/com/android/server/wm/traces/common/Bounds.kt
new file mode 100644
index 0000000..52b8384
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/Bounds.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.server.wm.traces.common
+
+open class Bounds(val width: Int, val height: Int) {
+    open val isEmpty: Boolean
+        get() = height == 0 || width == 0
+
+    val isNotEmpty: Boolean
+        get() = !isEmpty
+
+    val size: Bounds
+        get() = Bounds(width, height)
+
+    open fun prettyPrint(): String = prettyPrint(this)
+
+    override fun toString(): String = if (isEmpty) "[empty]" else prettyPrint()
+
+    override fun equals(other: Any?): Boolean =
+        other is Bounds &&
+            other.height == height &&
+            other.width == width
+
+    override fun hashCode(): Int {
+        var result = width
+        result = 31 * result + height
+        return result
+    }
+
+    companion object {
+        val EMPTY: Bounds = Bounds(0, 0)
+
+        fun prettyPrint(bounds: Bounds): String = "${bounds.width} x ${bounds.height}"
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/Buffer.kt b/libraries/flicker/src/com/android/server/wm/traces/common/Buffer.kt
new file mode 100644
index 0000000..25e221e
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/Buffer.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.server.wm.traces.common
+
+class Buffer(width: Int, height: Int, val stride: Int, val format: Int) : Bounds(width, height) {
+    override fun prettyPrint(): String = prettyPrint(this)
+
+    override fun equals(other: Any?): Boolean =
+        other is Buffer &&
+        other.height == height &&
+        other.width == width &&
+        other.stride == stride &&
+        other.format == format
+
+    override fun hashCode(): Int {
+        var result = height
+        result = 31 * result + width
+        result = 31 * result + stride
+        result = 31 * result + format
+        return result
+    }
+
+    companion object {
+        val EMPTY: Buffer = Buffer(0, 0, 0, 0)
+
+        fun prettyPrint(buffer: Buffer): String = "w:${buffer.width}, h:${buffer.height}, " +
+            "stride:${buffer.stride}, format:${buffer.format}"
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/Color.kt b/libraries/flicker/src/com/android/server/wm/traces/common/Color.kt
new file mode 100644
index 0000000..a4334fe
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/Color.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.server.wm.traces.common
+
+data class Color(val r: Float, val g: Float, val b: Float, val a: Float) {
+    val isEmpty: Boolean
+        get() = a == 0f || r < 0 || g < 0 || b < 0
+
+    val isNotEmpty: Boolean
+        get() = !isEmpty
+
+    fun prettyPrint(): String = prettyPrint(this)
+
+    override fun toString(): String = if (isEmpty) "[empty]" else prettyPrint()
+
+    companion object {
+        val EMPTY = Color(r = -1f, g = -1f, b = -1f, a = 0f)
+
+        fun prettyPrint(color: Color): String {
+            val r = FloatFormatter.format(color.r)
+            val g = FloatFormatter.format(color.g)
+            val b = FloatFormatter.format(color.b)
+            val a = FloatFormatter.format(color.a)
+            return "r:$r g:$g b:$b a:$a"
+        }
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/Extensions.kt b/libraries/flicker/src/com/android/server/wm/traces/common/Extensions.kt
new file mode 100644
index 0000000..1b24bae
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/Extensions.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2021 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.traces.common
+
+private const val MILLISECOND_AS_NANOSECONDS: Long = 1000000
+private const val SECOND_AS_NANOSECONDS: Long = 1000000000
+private const val MINUTE_AS_NANOSECONDS: Long = 60000000000
+private const val HOUR_AS_NANOSECONDS: Long = 3600000000000
+private const val DAY_AS_NANOSECONDS: Long = 86400000000000
+
+internal fun prettyTimestamp(timestampNs: Long): String {
+    // Necessary for compatibility with JS Number
+    var remainingNs = "$timestampNs".toLong()
+    val prettyTimestamp = StringBuilder()
+
+    val timeUnitToNanoSeconds = mapOf(
+        "d" to DAY_AS_NANOSECONDS,
+        "h" to HOUR_AS_NANOSECONDS,
+        "m" to MINUTE_AS_NANOSECONDS,
+        "s" to SECOND_AS_NANOSECONDS,
+        "ms" to MILLISECOND_AS_NANOSECONDS
+    )
+
+    for ((timeUnit, ns) in timeUnitToNanoSeconds) {
+        val convertedTime = remainingNs / ns
+        remainingNs %= ns
+        prettyTimestamp.append("$convertedTime$timeUnit")
+    }
+
+    return prettyTimestamp.toString()
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/FloatFormatter.kt b/libraries/flicker/src/com/android/server/wm/traces/common/FloatFormatter.kt
new file mode 100644
index 0000000..31ee05a
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/FloatFormatter.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2021 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.traces.common
+
+import kotlin.math.min
+
+/**
+ * A formatter to print floats with 3 decimal digits.
+ *
+ * This is necessary because multiplatform kotlin projects don't support String.format
+ * yet (issue KT-21644)
+ */
+object FloatFormatter {
+    fun format(value: Float): String {
+        val strValue = value.toString()
+        val dotIndex = strValue.indexOf(".")
+        return if (dotIndex > -1) {
+            strValue.substring(0, min(strValue.length, dotIndex + 4))
+        } else {
+            "$strValue.000"
+        }
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/ITrace.kt b/libraries/flicker/src/com/android/server/wm/traces/common/ITrace.kt
new file mode 100644
index 0000000..6dc532d
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/ITrace.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.server.wm.traces.common
+
+interface ITrace<Entry : ITraceEntry> {
+    val entries: List<Entry>
+    val source: String
+    val sourceChecksum: String
+
+    fun getEntry(timestamp: Long): Entry {
+        return entries.firstOrNull { it.timestamp == timestamp }
+                ?: throw RuntimeException("Entry does not exist for timestamp $timestamp")
+    }
+
+    fun hasSource(): Boolean = source.isNotEmpty()
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/ITraceEntry.kt b/libraries/flicker/src/com/android/server/wm/traces/common/ITraceEntry.kt
new file mode 100644
index 0000000..3813a1d
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/ITraceEntry.kt
@@ -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.
+ */
+
+package com.android.server.wm.traces.common
+
+/**
+ * Common interface for Layer and WindowManager trace entries.
+ */
+interface ITraceEntry {
+    /**
+     * @return timestamp of current entry
+     */
+    val timestamp: Long
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/Point.kt b/libraries/flicker/src/com/android/server/wm/traces/common/Point.kt
new file mode 100644
index 0000000..3b3c2b2
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/Point.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2021 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.traces.common
+
+data class Point(val x: Int, val y: Int) {
+    fun prettyPrint(): String = prettyPrint(this)
+
+    override fun toString(): String = prettyPrint()
+
+    companion object {
+        fun prettyPrint(point: Point): String = "(${point.x}, ${point.y})"
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/README.md b/libraries/flicker/src/com/android/server/wm/traces/common/README.md
new file mode 100644
index 0000000..c9e2b5c
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/README.md
@@ -0,0 +1,7 @@
+Common library code used by Kotlin and JavaScript flicker library.
+
+All elements in this package are shared between the Android codebase and
+Winscope (development/tools/winscope), which is written in JavaScript.
+
+We rely on Kotlin JS to translate this package into JavaScript, therefore, the classes
+of this package must not rely on Java or Android specific elements.
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/Rect.kt b/libraries/flicker/src/com/android/server/wm/traces/common/Rect.kt
new file mode 100644
index 0000000..07caa72
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/Rect.kt
@@ -0,0 +1,92 @@
+/*
+ * 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.server.wm.traces.common
+
+open class Rect(
+    val left: Int = 0,
+    val top: Int = 0,
+    val right: Int = 0,
+    val bottom: Int = 0
+) {
+    val height: Int get() = bottom - top
+    val width: Int get() = right - left
+    fun centerX(): Int = left + right / 2
+    fun centerY(): Int = top + bottom / 2
+    /**
+     * Returns true if the rectangle is empty (left >= right or top >= bottom)
+     */
+    val isEmpty: Boolean = width == 0 || height == 0
+
+    val isNotEmpty: Boolean = !isEmpty
+
+    /**
+     * Returns a [RectF] version fo this rectangle.
+     */
+    fun toRectF(): RectF {
+        return RectF(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat())
+    }
+
+    open fun prettyPrint(): String = prettyPrint(this)
+
+    override fun equals(other: Any?): Boolean = other?.toString() == this.toString()
+
+    /**
+     * Returns true iff the specified rectangle r is inside or equal to this
+     * rectangle. An empty rectangle never contains another rectangle.
+     *
+     * @param rect The rectangle being tested for containment.
+     * @return true iff the specified rectangle r is inside or equal to this
+     *              rectangle
+     */
+    operator fun contains(rect: Rect): Boolean {
+        val thisRect = toRectF()
+        val otherRect = rect.toRectF()
+        return thisRect.contains(otherRect)
+    }
+
+    /**
+     * If the specified rectangle intersects this rectangle, return true and set
+     * this rectangle to that intersection, otherwise return false and do not
+     * change this rectangle. No check is performed to see if either rectangle
+     * is empty. To just test for intersection, use intersects()
+     *
+     * @param rect The rectangle being intersected with this rectangle.
+     * @return A rectangle with the intersection coordinates
+     */
+    fun intersection(rect: Rect): Rect {
+        val thisRect = toRectF()
+        val otherRect = rect.toRectF()
+        return thisRect.intersection(otherRect).toRect()
+    }
+
+    override fun hashCode(): Int {
+        var result = left
+        result = 31 * result + top
+        result = 31 * result + right
+        result = 31 * result + bottom
+        return result
+    }
+
+    override fun toString(): String = if (isEmpty) "[empty]" else prettyPrint()
+
+    companion object {
+        val EMPTY = Rect()
+
+        fun prettyPrint(rect: Rect): String = "(${rect.left}, ${rect.top}) - " +
+            "(${rect.right}, ${rect.bottom})"
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/RectF.kt b/libraries/flicker/src/com/android/server/wm/traces/common/RectF.kt
new file mode 100644
index 0000000..db55eda
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/RectF.kt
@@ -0,0 +1,125 @@
+/*
+ * 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.server.wm.traces.common
+
+data class RectF(
+    val left: Float = 0f,
+    val top: Float = 0f,
+    val right: Float = 0f,
+    val bottom: Float = 0f
+) {
+    val height: Float get() = bottom - top
+    val width: Float get() = right - left
+
+    /**
+     * Returns true if the rectangle is empty (left >= right or top >= bottom)
+     */
+    val isEmpty: Boolean
+        get() = width == 0f || height == 0f
+    val isNotEmpty: Boolean
+        get() = !isEmpty
+
+    /**
+     * Returns a [Rect] version fo this rectangle.
+     *
+     * All fractional parts are rounded to 0
+     */
+    fun toRect(): Rect {
+        return Rect(left.toInt(), top.toInt(), right.toInt(), bottom.toInt())
+    }
+
+    /**
+     * Returns true iff the specified rectangle r is inside or equal to this
+     * rectangle. An empty rectangle never contains another rectangle.
+     *
+     * @param r The rectangle being tested for containment.
+     * @return true iff the specified rectangle r is inside or equal to this
+     *              rectangle
+     */
+    operator fun contains(r: RectF): Boolean {
+        // check for empty first
+        return this.left < this.right && this.top < this.bottom && // now check for containment
+                left <= r.left && top <= r.top && right >= r.right && bottom >= r.bottom
+    }
+
+    /**
+     * If the rectangle specified by left,top,right,bottom intersects this
+     * rectangle, return true and set this rectangle to that intersection,
+     * otherwise return false and do not change this rectangle. No check is
+     * performed to see if either rectangle is empty. Note: To just test for
+     * intersection, use intersects()
+     *
+     * @param left The left side of the rectangle being intersected with this
+     * rectangle
+     * @param top The top of the rectangle being intersected with this rectangle
+     * @param right The right side of the rectangle being intersected with this
+     * rectangle.
+     * @param bottom The bottom of the rectangle being intersected with this
+     * rectangle.
+     * @return A rectangle with the intersection coordinates
+     */
+    fun intersection(left: Float, top: Float, right: Float, bottom: Float): RectF {
+        if (this.left < right && left < this.right && this.top < bottom && top < this.bottom) {
+            var intersectionLeft = 0f
+            var intersectionTop = 0f
+            var intersectionRight = 0f
+            var intersectionBottom = 0f
+
+            if (this.left < left) {
+                intersectionLeft = left
+            }
+            if (this.top < top) {
+                intersectionTop = top
+            }
+            if (this.right > right) {
+                intersectionRight = right
+            }
+            if (this.bottom > bottom) {
+                intersectionBottom = bottom
+            }
+            return RectF(intersectionLeft, intersectionTop, intersectionRight, intersectionBottom)
+        }
+        return EMPTY
+    }
+
+    /**
+     * If the specified rectangle intersects this rectangle, return true and set
+     * this rectangle to that intersection, otherwise return false and do not
+     * change this rectangle. No check is performed to see if either rectangle
+     * is empty. To just test for intersection, use intersects()
+     *
+     * @param r The rectangle being intersected with this rectangle.
+     * @return A rectangle with the intersection coordinates
+     */
+    fun intersection(r: RectF): RectF = intersection(r.left, r.top, r.right, r.bottom)
+
+    fun prettyPrint(): String = prettyPrint(this)
+
+    override fun toString(): String = if (isEmpty) "[empty]" else prettyPrint()
+
+    companion object {
+        val EMPTY = RectF()
+
+        fun prettyPrint(rect: RectF): String {
+            val left = FloatFormatter.format(rect.left)
+            val top = FloatFormatter.format(rect.top)
+            val right = FloatFormatter.format(rect.right)
+            val bottom = FloatFormatter.format(rect.bottom)
+            return "($left, $top) - ($right, $bottom)"
+        }
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/Region.kt b/libraries/flicker/src/com/android/server/wm/traces/common/Region.kt
new file mode 100644
index 0000000..0bacfe9
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/Region.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2021 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.traces.common
+
+class Region(val rects: Array<Rect>) : Rect(
+    rects.map { it.left }.minOrNull() ?: 0,
+    rects.map { it.top }.minOrNull() ?: 0,
+    rects.map { it.right }.maxOrNull() ?: 0,
+    rects.map { it.bottom }.maxOrNull() ?: 0
+) {
+    constructor(
+        left: Int,
+        top: Int,
+        right: Int,
+        bottom: Int
+    ) : this(Rect(left, top, right, bottom))
+
+    constructor(rect: Rect?): this(rect?.let { arrayOf(rect) } ?: emptyArray())
+
+    constructor(rect: RectF?): this(rect?.toRect())
+
+    constructor() : this(Rect.EMPTY)
+
+    override fun toString(): String = prettyPrint()
+
+    override fun prettyPrint(): String = rects.joinToString(", ") { it.prettyPrint() }
+
+    companion object {
+        val EMPTY = Region()
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/layers/Layer.kt b/libraries/flicker/src/com/android/server/wm/traces/common/layers/Layer.kt
new file mode 100644
index 0000000..2dd7630
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/layers/Layer.kt
@@ -0,0 +1,325 @@
+/*
+ * 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.server.wm.traces.common.layers
+
+import com.android.server.wm.traces.common.Buffer
+import com.android.server.wm.traces.common.Color
+import com.android.server.wm.traces.common.Rect
+import com.android.server.wm.traces.common.Region
+import com.android.server.wm.traces.common.RectF
+
+/**
+ * Represents a single layer with links to its parent and child layers.
+ *
+ * This is a generic object that is reused by both Flicker and Winscope and cannot
+ * access internal Java/Android functionality
+ *
+ **/
+open class Layer(
+    val name: String,
+    val id: Int,
+    val parentId: Int,
+    val z: Int,
+    val visibleRegion: Region?,
+    val activeBuffer: Buffer,
+    val flags: Int,
+    _bounds: RectF?,
+    val color: Color,
+    _isOpaque: Boolean,
+    val shadowRadius: Float,
+    val cornerRadius: Float,
+    val type: String,
+    _screenBounds: RectF?,
+    val transform: Transform,
+    _sourceBounds: RectF?,
+    val currFrame: Long,
+    val effectiveScalingMode: Int,
+    val bufferTransform: Transform,
+    val hwcCompositionType: Int,
+    val hwcCrop: RectF,
+    val hwcFrame: Rect,
+    val backgroundBlurRadius: Int,
+    val crop: Rect?,
+    val isRelativeOf: Boolean,
+    val zOrderRelativeOfId: Int
+) {
+    lateinit var parent: Layer
+    var zOrderRelativeOf: Layer? = null
+    var zOrderRelativeParentOf: Int = 0
+
+    /**
+     * Checks if the [Layer] is a root layer in the hierarchy
+     *
+     * @return
+     */
+    val isRootLayer: Boolean
+        get() {
+            return !::parent.isInitialized
+        }
+
+    val children = mutableListOf<Layer>()
+    val occludedBy = mutableListOf<Layer>()
+    val partiallyOccludedBy = mutableListOf<Layer>()
+    val coveredBy = mutableListOf<Layer>()
+
+    fun addChild(childLayer: Layer) {
+        children.add(childLayer)
+    }
+
+    val bounds: RectF = _bounds ?: RectF.EMPTY
+    val sourceBounds: RectF = _sourceBounds ?: RectF.EMPTY
+
+    /**
+     * Checks if the layer's active buffer is empty
+     *
+     * An active buffer is empty if it is not in the proto or if its height or width are 0
+     *
+     * @return
+     */
+    val isActiveBufferEmpty: Boolean get() = activeBuffer.isEmpty
+
+    /**
+     * Checks if the layer is hidden, that is, if its flags contain 0x1 (FLAG_HIDDEN)
+     *
+     * @return
+     */
+    val isHiddenByPolicy: Boolean
+        get() {
+            return (flags and /* FLAG_HIDDEN */0x1) != 0x0 ||
+                // offscreen layer root has a unique layer id
+                id == 0x7FFFFFFD
+        }
+
+    /**
+     * Checks if the layer is visible.
+     *
+     * A layer is visible if:
+     * - it has an active buffer or has effects
+     * - is not hidden
+     * - is not transparent
+     * - not occluded by other layers
+     *
+     * @return
+     */
+    val isVisible: Boolean
+        get() {
+            return when {
+                isHiddenByParent -> false
+                isHiddenByPolicy -> false
+                isActiveBufferEmpty && !hasEffects -> false
+                !fillsColor -> false
+                occludedBy.isNotEmpty() -> false
+                visibleRegion?.isEmpty ?: false -> false
+                else -> !bounds.isEmpty
+            }
+        }
+
+    val isOpaque: Boolean = if (color.a != 1.0f) false else _isOpaque
+
+    /**
+     * Checks if the [Layer] has a color
+     *
+     * @return
+     */
+    val fillsColor: Boolean
+        get() {
+            return color.isNotEmpty
+        }
+
+    /**
+     * Checks if the [Layer] draws a shadow
+     *
+     * @return
+     */
+    val drawsShadows: Boolean get() = shadowRadius > 0
+
+    /**
+     * Checks if the [Layer] has blur
+     *
+     * @return
+     */
+    val hasBlur: Boolean get() = backgroundBlurRadius > 0
+
+    /**
+     * Checks if the [Layer] has rounded corners
+     *
+     * @return
+     */
+    val hasRoundedCorners: Boolean get() = cornerRadius > 0
+
+    /**
+     * Checks if the [Layer] draws has effects, which include:
+     * - is a color layer
+     * - is an effects layers which [fillsColor] or [drawsShadows]
+     *
+     * @return
+     */
+    val hasEffects: Boolean
+        get() {
+            // Support previous color layer
+            if (isColorLayer) {
+                return true
+            }
+
+            // Support newer effect layer
+            return isEffectLayer && (fillsColor || drawsShadows)
+        }
+
+    /**
+     * Checks if the [Layer] type is BufferStateLayer or BufferQueueLayer
+     *
+     * @return
+     */
+    val isBufferLayer: Boolean
+        get() = type == "BufferStateLayer" || type == "BufferQueueLayer"
+
+    /**
+     * Checks if the [Layer] type is ColorLayer
+     *
+     * @return
+     */
+    val isColorLayer: Boolean get() = type == "ColorLayer"
+
+    /**
+     * Checks if the [Layer] type is ContainerLayer
+     *
+     * @return
+     */
+    val isContainerLayer: Boolean get() = type == "ContainerLayer"
+
+    /**
+     * Checks if the [Layer] type is EffectLayer
+     *
+     * @return
+     */
+    val isEffectLayer: Boolean get() = type == "EffectLayer"
+
+    /**
+     * Checks if the [Layer] is not visible
+     *
+     * @return
+     */
+    val isInvisible: Boolean get() = !isVisible
+
+    /**
+     * Checks if the [Layer] is hidden by its parent
+     *
+     * @return
+     */
+    val isHiddenByParent: Boolean
+        get() = !isRootLayer && (parent.isHiddenByPolicy || parent.isHiddenByParent)
+
+    /**
+     * Gets a description of why the layer is (in)visible
+     *
+     * @return
+     */
+    val visibilityReason: String
+        get() {
+            return when {
+                isVisible -> ""
+                isContainerLayer -> "ContainerLayer"
+                isHiddenByPolicy -> "Flag is hidden"
+                isHiddenByParent -> "Hidden by parent ${parent.name}"
+                isBufferLayer && isActiveBufferEmpty -> "Buffer is empty"
+                color.isEmpty -> "Alpha is 0"
+                crop?.isEmpty ?: false -> "Crop is 0x0"
+                bounds.isEmpty -> "Bounds is 0x0"
+                !transform.isValid -> "Transform is invalid"
+                isRelativeOf && zOrderRelativeOf == null -> "RelativeOf layer has been removed"
+                isEffectLayer && !fillsColor && !drawsShadows && !hasBlur ->
+                    "Effect layer does not have color fill, shadow or blur"
+                occludedBy.isNotEmpty() -> {
+                    val occludedByIds = occludedBy.joinToString(", ") { it.id.toString() }
+                    "Layer is occluded by: $occludedByIds"
+                }
+                visibleRegion?.isEmpty ?: false ->
+                    "Visible region calculated by Composition Engine is empty"
+                else -> "Unknown"
+            }
+        }
+
+    val screenBounds: RectF = when {
+        visibleRegion?.isNotEmpty == true -> visibleRegion.toRectF()
+        _screenBounds != null -> _screenBounds
+        else -> transform.apply(bounds)
+    }
+
+    fun contains(innerLayer: Layer): Boolean {
+        return if (!this.transform.isSimpleRotation || !innerLayer.transform.isSimpleRotation) {
+            false
+        } else {
+            this.screenBounds.contains(innerLayer.screenBounds)
+        }
+    }
+
+    fun overlaps(other: Layer): Boolean =
+        !this.screenBounds.intersection(other.screenBounds).isEmpty
+
+    override fun toString(): String {
+        return buildString {
+            append(name)
+
+            if (activeBuffer.isNotEmpty) {
+                append(" buffer:${activeBuffer.width}x${activeBuffer.height}")
+                append(" frame#$currFrame")
+            }
+
+            if (isVisible) {
+                append(" visible:$visibleRegion")
+            }
+        }
+    }
+
+    override fun equals(other: Any?): Boolean {
+        return other is Layer &&
+            other.parentId == this.parentId &&
+            other.name == this.name &&
+            other.flags == this.flags &&
+            other.currFrame == this.currFrame &&
+            other.activeBuffer == this.activeBuffer &&
+            other.screenBounds == this.screenBounds
+    }
+
+    override fun hashCode(): Int {
+        var result = name.hashCode()
+        result = 31 * result + id
+        result = 31 * result + parentId
+        result = 31 * result + z
+        result = 31 * result + visibleRegion.hashCode()
+        result = 31 * result + activeBuffer.hashCode()
+        result = 31 * result + flags
+        result = 31 * result + bounds.hashCode()
+        result = 31 * result + color.hashCode()
+        result = 31 * result + isOpaque.hashCode()
+        result = 31 * result + shadowRadius.hashCode()
+        result = 31 * result + cornerRadius.hashCode()
+        result = 31 * result + type.hashCode()
+        result = 31 * result + screenBounds.hashCode()
+        result = 31 * result + transform.hashCode()
+        result = 31 * result + sourceBounds.hashCode()
+        result = 31 * result + currFrame.hashCode()
+        result = 31 * result + effectiveScalingMode
+        result = 31 * result + bufferTransform.hashCode()
+        result = 31 * result + parent.hashCode()
+        result = 31 * result + children.hashCode()
+        result = 31 * result + occludedBy.hashCode()
+        result = 31 * result + partiallyOccludedBy.hashCode()
+        result = 31 * result + coveredBy.hashCode()
+        return result
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/layers/LayerTraceEntry.kt b/libraries/flicker/src/com/android/server/wm/traces/common/layers/LayerTraceEntry.kt
new file mode 100644
index 0000000..c440de9
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/layers/LayerTraceEntry.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2021 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.traces.common.layers
+
+import com.android.server.wm.traces.common.ITraceEntry
+import com.android.server.wm.traces.common.prettyTimestamp
+
+/**
+ * Represents a single Layer trace entry.
+ *
+ * This is a generic object that is reused by both Flicker and Winscope and cannot
+ * access internal Java/Android functionality
+ *
+ **/
+open class LayerTraceEntry constructor(
+    override val timestamp: Long, // hierarchical representation of layers
+    val hwcBlob: String,
+    val where: String,
+    _rootLayers: Array<Layer>
+) : ITraceEntry {
+    val flattenedLayers: Array<Layer> = fillFlattenedLayers(_rootLayers)
+    val rootLayers: Array<Layer> get() = flattenedLayers.filter { it.isRootLayer }.toTypedArray()
+
+    private fun fillFlattenedLayers(rootLayers: Array<Layer>): Array<Layer> {
+        val opaqueLayers = mutableListOf<Layer>()
+        val transparentLayers = mutableListOf<Layer>()
+        val layers = mutableListOf<Layer>()
+        val roots = rootLayers.fillOcclusionState(
+            opaqueLayers, transparentLayers).toMutableList()
+        while (roots.isNotEmpty()) {
+            val layer = roots.removeAt(0)
+            layers.add(layer)
+            roots.addAll(layer.children)
+        }
+        return layers.toTypedArray()
+    }
+
+    private fun Array<Layer>.topDownTraversal(): List<Layer> {
+        return this
+                .sortedBy { it.z }
+                .flatMap { it.topDownTraversal() }
+    }
+
+    val visibleLayers: Array<Layer>
+        get() = flattenedLayers.filter { it.isVisible }.toTypedArray()
+
+    private fun Layer.topDownTraversal(): List<Layer> {
+        val traverseList = mutableListOf(this)
+
+        this.children.sortedBy { it.z }
+                .forEach { childLayer ->
+                    traverseList.addAll(childLayer.topDownTraversal())
+                }
+
+        return traverseList
+    }
+
+    private fun Array<Layer>.fillOcclusionState(
+        opaqueLayers: MutableList<Layer>,
+        transparentLayers: MutableList<Layer>
+    ): Array<Layer> {
+        val traversalList = topDownTraversal().reversed()
+
+        traversalList.forEach { layer ->
+            val visible = layer.isVisible
+
+            if (visible) {
+                layer.occludedBy.addAll(opaqueLayers
+                    .filter { it.contains(layer) && !it.hasRoundedCorners })
+                layer.partiallyOccludedBy.addAll(
+                    opaqueLayers.filter { it.overlaps(layer) && it !in layer.occludedBy })
+                layer.coveredBy.addAll(transparentLayers.filter { it.overlaps(layer) })
+
+                if (layer.isOpaque) {
+                    opaqueLayers.add(layer)
+                } else {
+                    transparentLayers.add(layer)
+                }
+            }
+        }
+
+        return this
+    }
+
+    fun getLayerWithBuffer(name: String): Layer? {
+        return flattenedLayers.firstOrNull {
+            it.name.contains(name) && it.activeBuffer.isNotEmpty
+        }
+    }
+
+    /**
+     * Check if at least one window which matches provided window name is visible.
+     */
+    fun isVisible(windowName: String): Boolean =
+        visibleLayers.any { it.name == windowName }
+
+    override fun toString(): String {
+        return prettyTimestamp(timestamp)
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/layers/LayerTraceEntryBuilder.kt b/libraries/flicker/src/com/android/server/wm/traces/common/layers/LayerTraceEntryBuilder.kt
new file mode 100644
index 0000000..446678f
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/layers/LayerTraceEntryBuilder.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2021 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.traces.common.layers
+
+/**
+ * Builder for LayerTraceEntries
+ */
+class LayerTraceEntryBuilder(
+    timestamp: Any,
+    layers: List<Layer>,
+    val hwcBlob: String = "",
+    val where: String = ""
+) {
+    // Necessary for compatibility with JS number type
+    private val timestamp: Long = "$timestamp".toLong()
+    private var orphanLayerCallback: ((Layer) -> Boolean)? = null
+    private val orphans = mutableListOf<Layer>()
+    private val layers = setLayers(layers)
+
+    private fun setLayers(layers: List<Layer>): Map<Int, Layer> {
+        val result = mutableMapOf<Int, Layer>()
+        layers.forEach { layer ->
+            val id = layer.id
+            if (result.containsKey(id)) {
+                throw RuntimeException("Duplicate layer id found: $id")
+            }
+            result[id] = layer
+        }
+
+        return result
+    }
+
+    fun setOrphanLayerCallback(value: ((Layer) -> Boolean)?): LayerTraceEntryBuilder = apply {
+        this.orphanLayerCallback = value
+    }
+
+    private fun notifyOrphansLayers() {
+        val callback = this.orphanLayerCallback ?: return
+
+        // Fail if we find orphan layers.
+        orphans.forEach { orphan ->
+            // Workaround for b/141326137, ignore the existence of an orphan layer
+            if (callback.invoke(orphan)) {
+                return@forEach
+            }
+            throw RuntimeException(
+                ("Failed to parse layers trace. Found orphan layer with id = ${orphan.id}" +
+                    " with parentId = ${orphan.parentId}"))
+        }
+    }
+
+    /**
+     * Update the parent layers or each trace
+     *
+     * @return root layer
+     */
+    private fun updateParents() {
+        for (layer in layers.values) {
+            val parentId = layer.parentId
+
+            val parentLayer = layers[parentId]
+            if (parentLayer == null) {
+                orphans.add(layer)
+                continue
+            }
+            parentLayer.addChild(layer)
+            layer.parent = parentLayer
+        }
+    }
+
+    /**
+     * Update the parent layers or each trace
+     *
+     * @return root layer
+     */
+    private fun updateRelZParents() {
+        for (layer in layers.values) {
+            val parentId = layer.zOrderRelativeOfId
+
+            val parentLayer = layers[parentId]
+            if (parentLayer == null) {
+                layer.zOrderRelativeParentOf = parentId
+                continue
+            }
+            layer.zOrderRelativeOf = parentLayer
+        }
+    }
+
+    private fun computeRootLayers(): List<Layer> {
+        updateParents()
+        updateRelZParents()
+
+        // Getting the first orphan works because when dumping the layers, the root layer comes
+        // first, and given that orphans are added in the same order as the layers are provided
+        // in the first orphan layer should be the root layer.
+        val firstRoot = orphans.firstOrNull() ?: throw IllegalStateException(
+            "Display root layer not found.")
+        orphans.remove(firstRoot)
+
+        // Find all root layers (any sibling of the root layer is considered a root layer in the trace)
+        val rootLayers = mutableListOf(firstRoot)
+        val remainingRoots = orphans.filter { it.parentId == firstRoot.parentId }
+        rootLayers.addAll(remainingRoots)
+
+        // Remove RootLayers from orphans
+        orphans.removeAll(rootLayers)
+
+        return rootLayers
+    }
+
+    /** Constructs the layer hierarchy from a flattened list of layers.  */
+    fun build(): LayerTraceEntry {
+        val rootLayers = computeRootLayers()
+
+        // Fail if we find orphan layers.
+        notifyOrphansLayers()
+
+        return LayerTraceEntry(timestamp, hwcBlob, where, rootLayers.toTypedArray())
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/layers/LayersTrace.kt b/libraries/flicker/src/com/android/server/wm/traces/common/layers/LayersTrace.kt
new file mode 100644
index 0000000..5162b7c
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/layers/LayersTrace.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2021 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.traces.common.layers
+
+import com.android.server.wm.traces.common.ITrace
+
+/**
+ * Contains a collection of parsed Layers trace entries and assertions to apply over a single entry.
+ *
+ * Each entry is parsed into a list of [LayerTraceEntry] objects.
+ *
+ * This is a generic object that is reused by both Flicker and Winscope and cannot
+ * access internal Java/Android functionality
+ *
+ */
+open class LayersTrace(
+    override val entries: List<LayerTraceEntry>,
+    override val source: String = "",
+    override val sourceChecksum: String = ""
+) : ITrace<LayerTraceEntry>, List<LayerTraceEntry> by entries {
+    constructor(entry: LayerTraceEntry): this(listOf(entry))
+
+    override fun toString(): String {
+        return "LayersTrace(Start: ${entries.first()}, " +
+            "End: ${entries.last()})"
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/layers/Transform.kt b/libraries/flicker/src/com/android/server/wm/traces/common/layers/Transform.kt
new file mode 100644
index 0000000..144fbab
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/layers/Transform.kt
@@ -0,0 +1,176 @@
+/*
+ * 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.server.wm.traces.common.layers
+
+import com.android.server.wm.traces.common.RectF
+
+open class Transform(val type: Int?, val matrix: Matrix) {
+
+    /**
+     * Returns true if the applying the transform on an an axis aligned rectangle
+     * results in another axis aligned rectangle.
+     */
+    val isSimpleRotation: Boolean = !(type?.isFlagSet(ROT_INVALID_VAL) ?: true)
+
+    /**
+     * The transformation matrix is defined as the product of:
+     * | cos(a) -sin(a) |  \/  | X 0 |
+     * | sin(a)  cos(a) |  /\  | 0 Y |
+     *
+     * where a is a rotation angle, and X and Y are scaling factors.
+     * A transformation matrix is invalid when either X or Y is zero,
+     * as a rotation matrix is valid for any angle. When either X or Y
+     * is 0, then the scaling matrix is not invertible, which makes the
+     * transformation matrix not invertible as well. A 2D matrix with
+     * components | A B | is not invertible if and only if AD - BC = 0.
+     *            | C D |
+     * This check is included above.
+     */
+    val isValid: Boolean
+        get() {
+            // determinant of transform
+            return matrix.dsdx * matrix.dtdy != matrix.dtdx * matrix.dsdy
+        }
+
+    private val typeFlags: Array<String>
+        get() {
+            if (type == null) {
+                return arrayOf("IDENTITY")
+            }
+
+            val result = mutableListOf<String>()
+
+            if (type.isFlagClear(SCALE_VAL or ROTATE_VAL or TRANSLATE_VAL)) {
+                result.add("IDENTITY")
+            }
+
+            if (type.isFlagSet(SCALE_VAL)) {
+                result.add("SCALE")
+            }
+
+            if (type.isFlagSet(TRANSLATE_VAL)) {
+                result.add("TRANSLATE")
+            }
+
+            when {
+                type.isFlagSet(ROT_INVALID_VAL) -> result.add("ROT_INVALID")
+                type.isFlagSet(ROT_90_VAL or FLIP_V_VAL or FLIP_H_VAL) -> result.add("ROT_270")
+                type.isFlagSet(FLIP_V_VAL or FLIP_H_VAL) -> result.add("ROT_180")
+                else -> {
+                    if (type.isFlagSet(ROT_90_VAL)) {
+                        result.add("ROT_90")
+                    }
+                    if (type.isFlagSet(FLIP_V_VAL)) {
+                        result.add("FLIP_V")
+                    }
+                    if (type.isFlagSet(FLIP_H_VAL)) {
+                        result.add("FLIP_H")
+                    }
+                }
+            }
+
+            if (result.isEmpty()) {
+                throw RuntimeException("Unknown transform type $type")
+            }
+
+            return result.toTypedArray()
+        }
+
+    fun prettyPrint(): String {
+        val transformType = typeFlags.joinToString("|")
+
+        if (isSimpleTransform(type)) {
+            return transformType
+        }
+
+        return "$transformType ${matrix.prettyPrint()}"
+    }
+
+    fun apply(bounds: RectF?): RectF {
+        return multiplyRect(matrix, bounds ?: RectF.EMPTY)
+    }
+
+    //          |dsdx dsdy  tx|
+    // matrix = |dtdx dtdy  ty|
+    //          |0    0     1 |
+    data class Matrix(
+        val dsdx: Float,
+        val dtdx: Float,
+        val tx: Float,
+
+        val dsdy: Float,
+        val dtdy: Float,
+        val ty: Float
+    ) {
+        fun prettyPrint(): String = "dsdx:$dsdx   dtdx:$dtdx   dsdy:$dsdy   dtdy:$dtdy"
+    }
+
+    private data class Vec2(val x: Float, val y: Float)
+
+    private fun multiplyRect(matrix: Matrix, rect: RectF): RectF {
+        //          |dsdx dsdy  tx|         | left, top         |
+        // matrix = |dtdx dtdy  ty|  rect = |                   |
+        //          |0    0     1 |         |     right, bottom |
+
+        val leftTop = multiplyVec2(matrix, rect.left, rect.top)
+        val rightTop = multiplyVec2(matrix, rect.right, rect.top)
+        val leftBottom = multiplyVec2(matrix, rect.left, rect.bottom)
+        val rightBottom = multiplyVec2(matrix, rect.right, rect.bottom)
+
+        return RectF(
+            left = arrayOf(leftTop.x, rightTop.x, leftBottom.x, rightBottom.x).minOrNull() ?: 0f,
+            top = arrayOf(leftTop.y, rightTop.y, leftBottom.y, rightBottom.y).minOrNull() ?: 0f,
+            right = arrayOf(leftTop.x, rightTop.x, leftBottom.x, rightBottom.x).minOrNull() ?: 0f,
+            bottom = arrayOf(leftTop.y, rightTop.y, leftBottom.y, rightBottom.y).minOrNull() ?: 0f
+        )
+    }
+
+    private fun multiplyVec2(matrix: Matrix, x: Float, y: Float): Vec2 {
+        // |dsdx dsdy  tx|     | x |
+        // |dtdx dtdy  ty|  x  | y |
+        // |0    0     1 |     | 1 |
+        return Vec2(
+            matrix.dsdx * x + matrix.dsdy * y + matrix.tx,
+            matrix.dtdx * x + matrix.dtdy * y + matrix.ty
+        )
+    }
+
+    companion object {
+        /* transform type flags */
+        const val TRANSLATE_VAL = 0x0001
+        const val ROTATE_VAL = 0x0002
+        const val SCALE_VAL = 0x0004
+
+        /* orientation flags */
+        const val FLIP_H_VAL = 0x0100 // (1 << 0 << 8)
+        const val FLIP_V_VAL = 0x0200 // (1 << 1 << 8)
+        const val ROT_90_VAL = 0x0400 // (1 << 2 << 8)
+        const val ROT_INVALID_VAL = 0x8000 // (0x80 << 8)
+
+        fun isSimpleTransform(type: Int?): Boolean {
+                return type?.isFlagClear(ROT_INVALID_VAL or SCALE_VAL) ?: false
+        }
+
+        fun Int.isFlagClear(bits: Int): Boolean {
+            return this and bits == 0
+        }
+
+        fun Int.isFlagSet(bits: Int): Boolean {
+            return this and bits == bits
+        }
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/WindowManagerState.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/WindowManagerState.kt
new file mode 100644
index 0000000..34b7598
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/WindowManagerState.kt
@@ -0,0 +1,611 @@
+/*
+ * Copyright (C) 2021 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.traces.common.windowmanager
+
+import com.android.server.wm.traces.common.Rect
+import com.android.server.wm.traces.common.ITraceEntry
+import com.android.server.wm.traces.common.prettyTimestamp
+import com.android.server.wm.traces.common.windowmanager.windows.Activity
+import com.android.server.wm.traces.common.windowmanager.windows.DisplayArea
+import com.android.server.wm.traces.common.windowmanager.windows.DisplayContent
+import com.android.server.wm.traces.common.windowmanager.windows.KeyguardControllerState
+import com.android.server.wm.traces.common.windowmanager.windows.RootWindowContainer
+import com.android.server.wm.traces.common.windowmanager.windows.ActivityTask
+import com.android.server.wm.traces.common.windowmanager.windows.WindowContainer
+import com.android.server.wm.traces.common.windowmanager.windows.WindowManagerPolicy
+import com.android.server.wm.traces.common.windowmanager.windows.WindowState
+
+/**
+ * Represents a single WindowManager trace entry.
+ *
+ * This is a generic object that is reused by both Flicker and Winscope and cannot
+ * access internal Java/Android functionality
+ *
+ **/
+open class WindowManagerState(
+    val where: String,
+    val policy: WindowManagerPolicy?,
+    val focusedApp: String,
+    val focusedDisplayId: Int,
+    val focusedWindow: String,
+    val inputMethodWindowAppToken: String,
+    val isHomeRecentsComponent: Boolean,
+    val isDisplayFrozen: Boolean,
+    val pendingActivities: Array<String>,
+    val root: RootWindowContainer,
+    val keyguardControllerState: KeyguardControllerState,
+    override val timestamp: Long = 0
+) : ITraceEntry {
+    val isVisible: Boolean = true
+    val stableId: String get() = this::class.simpleName ?: error("Unable to determine class")
+    val name: String get() = prettyTimestamp(timestamp)
+
+    val windowContainers: Array<WindowContainer>
+        get() = root.collectDescendants()
+
+    val children: Array<WindowContainer>
+        get() = root.children.reversedArray()
+
+    // Displays in z-order with the top most at the front of the list, starting with primary.
+    val displays: Array<DisplayContent>
+        get() = windowContainers.filterIsInstance<DisplayContent>().toTypedArray()
+
+    // Stacks in z-order with the top most at the front of the list, starting with primary display.
+    val rootTasks: Array<ActivityTask>
+        get() = displays.flatMap { it.rootTasks.toList() }.toTypedArray()
+
+    // Windows in z-order with the top most at the front of the list.
+    val windowStates: Array<WindowState>
+        get() = windowContainers.filterIsInstance<WindowState>().toTypedArray()
+
+    @Deprecated("Please use windowStates instead", replaceWith = ReplaceWith("windowStates"))
+    val windows: Array<WindowState>
+        get() = windowStates
+
+    val appWindows: Array<WindowState>
+        get() = windowStates.filter { it.isAppWindow }.toTypedArray()
+    val nonAppWindows: Array<WindowState>
+        get() = windowStates.filterNot { it.isAppWindow }.toTypedArray()
+    val aboveAppWindows: Array<WindowState>
+        get() = windowStates.takeWhile { !appWindows.contains(it) }.toTypedArray()
+    val belowAppWindows: Array<WindowState>
+        get() = windowStates
+            .dropWhile { !appWindows.contains(it) }.drop(appWindows.size).toTypedArray()
+    val visibleWindows: Array<WindowState>
+        get() = windowStates.filter { it.isSurfaceShown }.toTypedArray()
+    val topVisibleAppWindow: String
+        get() = appWindows.filter { it.isVisible }
+            .map { it.title }
+            .firstOrNull() ?: ""
+    val pinnedWindows: Array<WindowState>
+        get() = visibleWindows
+            .filter { it.windowingMode == WINDOWING_MODE_PINNED }
+            .toTypedArray()
+
+    val focusedDisplay: DisplayContent? get() = getDisplay(focusedDisplayId)
+    val focusedStackId: Int get() = focusedDisplay?.focusedRootTaskId ?: -1
+    val focusedActivity: String get() {
+        val focusedDisplay = focusedDisplay
+        return if (focusedDisplay != null && focusedDisplay.resumedActivity.isNotEmpty()) {
+            focusedDisplay.resumedActivity
+        } else {
+            getActivityForWindow(focusedWindow, focusedDisplayId)?.name ?: ""
+        }
+    }
+    val resumedActivitiesInDisplays: Array<String>
+        get() = displays.flatMap { display ->
+            display.rootTasks.flatMap { it.resumedActivities.toList() }
+        }.toTypedArray()
+    val defaultPinnedStackBounds: Rect
+        get() = displays
+            .lastOrNull { it.defaultPinnedStackBounds.isNotEmpty }?.defaultPinnedStackBounds
+            ?: Rect.EMPTY
+    val pinnedStackMovementBounds: Rect
+        get() = displays
+            .lastOrNull { it.defaultPinnedStackBounds.isNotEmpty }?.pinnedStackMovementBounds
+            ?: Rect.EMPTY
+    val focusedStackActivityType: Int
+        get() = getRootTask(focusedStackId)?.activityType ?: ACTIVITY_TYPE_UNDEFINED
+    val focusedStackWindowingMode: Int
+        get() = getRootTask(focusedStackId)?.windowingMode ?: WINDOWING_MODE_UNDEFINED
+    val resumedActivities: Array<String>
+        get() = rootTasks.flatMap { it.resumedActivities.toList() }.toTypedArray()
+    val resumedActivitiesCount: Int get() = resumedActivities.size
+    val stackCount: Int get() = rootTasks.size
+    val displayCount: Int get() = displays.size
+    val homeTask: ActivityTask? get() = getStackByActivityType(ACTIVITY_TYPE_HOME)?.topTask
+    val recentsTask: ActivityTask? get() = getStackByActivityType(ACTIVITY_TYPE_RECENTS)?.topTask
+    val homeActivity: Activity? get() = homeTask?.activities?.lastOrNull()
+    val recentsActivity: Activity? get() = recentsTask?.activities?.lastOrNull()
+    val rootTasksCount: Int get() = rootTasks.size
+    val isRecentsActivityVisible: Boolean get() = recentsActivity?.isVisible ?: false
+    val dreamTask: ActivityTask?
+        get() = getStackByActivityType(ACTIVITY_TYPE_DREAM)?.topTask
+    val defaultDisplayLastTransition: String get() = getDefaultDisplay()?.lastTransition
+            ?: "Default display not found"
+    val defaultDisplayAppTransitionState: String get() = getDefaultDisplay()?.appTransitionState
+            ?: "Default display not found"
+    val allNavigationBarStates: Array<WindowState>
+        get() = windowStates.filter { it.isValidNavBarType }.toTypedArray()
+    val frontWindow: String? get() = windowStates.map { it.title }.firstOrNull()
+    val stableBounds: Rect get() = getDefaultDisplay()?.stableBounds ?: Rect.EMPTY
+    val inputMethodWindowState: WindowState?
+        get() = getWindowStateForAppToken(inputMethodWindowAppToken)
+
+    fun getDefaultDisplay(): DisplayContent? =
+        displays.firstOrNull { it.id == DEFAULT_DISPLAY }
+
+    fun getDisplay(displayId: Int): DisplayContent? =
+        displays.firstOrNull { it.id == displayId }
+
+    fun getTaskDisplayArea(activityName: String): DisplayArea? {
+        val result = displays.mapNotNull { it.getTaskDisplayArea(activityName) }
+
+        if (result.size > 1) {
+            throw IllegalArgumentException(
+                "There must be exactly one activity among all TaskDisplayAreas.")
+        }
+
+        return result.firstOrNull()
+    }
+
+    fun getFrontRootTaskId(displayId: Int): Int =
+        getDisplay(displayId)?.rootTasks?.first()?.rootTaskId ?: 0
+
+    fun getFrontStackActivityType(displayId: Int): Int =
+        getDisplay(displayId)?.rootTasks?.first()?.activityType ?: 0
+
+    fun getFrontStackWindowingMode(displayId: Int): Int =
+        getDisplay(displayId)?.rootTasks?.first()?.windowingMode ?: 0
+
+    fun getTopActivityName(displayId: Int): String {
+        return getDisplay(displayId)
+            ?.rootTasks?.firstOrNull()
+            ?.topTask
+            ?.activities?.firstOrNull()
+            ?.title
+            ?: ""
+    }
+
+    fun getResumedActivitiesCountInPackage(packageName: String): Int {
+        val componentPrefix = "$packageName/"
+        var count = 0
+        displays.forEach { display ->
+            display.rootTasks.forEach { task ->
+                count += task.resumedActivities.count {
+                    it.isNotEmpty() && it.startsWith(componentPrefix)
+                }
+            }
+        }
+        return count
+    }
+
+    fun getResumedActivity(displayId: Int): String {
+        return getDisplay(displayId)?.resumedActivity ?: ""
+    }
+
+    fun containsStack(windowingMode: Int, activityType: Int): Boolean {
+        return countStacks(windowingMode, activityType) > 0
+    }
+
+    fun countStacks(windowingMode: Int, activityType: Int): Int {
+        var count = 0
+        for (stack in rootTasks) {
+            if (activityType != ACTIVITY_TYPE_UNDEFINED && activityType != stack.activityType) {
+                continue
+            }
+            if (windowingMode != WINDOWING_MODE_UNDEFINED && windowingMode != stack.windowingMode) {
+                continue
+            }
+            ++count
+        }
+        return count
+    }
+
+    fun getRootTask(taskId: Int): ActivityTask? =
+        rootTasks.firstOrNull { it.rootTaskId == taskId }
+
+    fun getRotation(displayId: Int): Int =
+            getDisplay(displayId)?.rotation ?: error("Default display not found")
+
+    fun getOrientation(displayId: Int): Int =
+            getDisplay(displayId)?.lastOrientation ?: error("Default display not found")
+
+    fun getStackByActivityType(activityType: Int): ActivityTask? =
+        rootTasks.firstOrNull { it.activityType == activityType }
+
+    fun getStandardStackByWindowingMode(windowingMode: Int): ActivityTask? =
+        rootTasks.firstOrNull {
+            it.activityType == ACTIVITY_TYPE_STANDARD &&
+                it.windowingMode == windowingMode
+        }
+
+    fun getStandardTaskCountByWindowingMode(windowingMode: Int): Int {
+        var count = 0
+        for (stack in rootTasks) {
+            if (stack.activityType != ACTIVITY_TYPE_STANDARD) {
+                continue
+            }
+            if (stack.windowingMode == windowingMode) {
+                count += if (stack.tasks.isEmpty()) 1 else stack.tasks.size
+            }
+        }
+        return count
+    }
+
+    /** Get the stack on its display.  */
+    fun getStackByActivity(activityName: String): ActivityTask? {
+        return displays.map { display ->
+            display.rootTasks.reversed().firstOrNull { stack ->
+                stack.containsActivity(activityName)
+            }
+        }.firstOrNull()
+    }
+
+    /**
+     * Get the first activity on display with id [displayId], containing a window whose title
+     * contains [partialWindowTitle]
+     *
+     * @param partialWindowTitle window title to search
+     * @param displayId display where to search the activity
+     */
+    fun getActivityForWindow(
+        partialWindowTitle: String,
+        displayId: Int = DEFAULT_DISPLAY
+    ): Activity? {
+        return displays.firstOrNull { it.id == displayId }?.rootTasks?.map { stack ->
+            stack.getActivity { activity ->
+                activity.hasWindow(partialWindowTitle)
+            }
+        }?.firstOrNull()
+    }
+
+    /** Get the stack position on its display. */
+    fun getStackIndexByActivityType(activityType: Int): Int {
+        return displays
+            .map { it.rootTasks.indexOfFirst { p -> p.activityType == activityType } }
+            .firstOrNull { it > -1 }
+            ?: -1
+    }
+
+    /** Get the stack position on its display. */
+    fun getStackIndexByActivity(activityName: String): Int {
+        for (display in displays) {
+            for (i in display.rootTasks.indices.reversed()) {
+                val stack = display.rootTasks[i]
+                if (stack.containsActivity(activityName)) return i
+            }
+        }
+        return -1
+    }
+
+    /** Get display id by activity on it. */
+    fun getDisplayByActivity(activityComponent: String): Int {
+        val task = getTaskByActivity(activityComponent) ?: return -1
+        return getRootTask(task.rootTaskId)?.displayId
+            ?: error("Task with name $activityComponent not found")
+    }
+
+    fun containsActivity(activityName: String): Boolean =
+        rootTasks.any { it.containsActivity(activityName) }
+
+    fun containsNoneOf(activityNames: Iterable<String>): Boolean {
+        for (activityName in activityNames) {
+            for (stack in rootTasks) {
+                if (stack.containsActivity(activityName)) return false
+            }
+        }
+        return true
+    }
+
+    fun containsActivityInWindowingMode(
+        activityName: String,
+        windowingMode: Int
+    ): Boolean {
+        for (stack in rootTasks) {
+            val activity = stack.getActivity(activityName)
+            if (activity != null && activity.windowingMode == windowingMode) {
+                return true
+            }
+        }
+        return false
+    }
+
+    fun isActivityVisible(activityName: String): Boolean =
+        rootTasks.map { it.getActivity(activityName)?.isVisible ?: false }.firstOrNull()
+            ?: false
+
+    fun isActivityTranslucent(activityName: String): Boolean =
+        rootTasks.map { it.getActivity(activityName)?.isTranslucent ?: false }.firstOrNull()
+            ?: false
+
+    fun isBehindOpaqueActivities(activityName: String): Boolean {
+        for (stack in rootTasks) {
+            val activity = stack.getActivity { a -> a.title == activityName || !a.isTranslucent }
+            if (activity != null) {
+                if (activity.title == activityName) {
+                    return false
+                }
+                if (!activity.isTranslucent) {
+                    return true
+                }
+            }
+        }
+
+        return false
+    }
+
+    fun containsStartedActivities(): Boolean = rootTasks.map {
+        it.getActivity { a -> a.state != STATE_STOPPED && a.state != STATE_DESTROYED } != null
+    }.firstOrNull() ?: false
+
+    fun hasActivityState(activityName: String, activityState: String): Boolean =
+        rootTasks.any { it.getActivity(activityName)?.state == activityState }
+
+    fun getActivityProcId(activityName: String): Int =
+        rootTasks.mapNotNull { it.getActivity(activityName)?.procId }
+            .firstOrNull()
+            ?: -1
+
+    fun getStackIdByActivity(activityName: String): Int =
+        getTaskByActivity(activityName)?.rootTaskId ?: INVALID_STACK_ID
+
+    fun getTaskByActivity(activityName: String): ActivityTask? =
+        getTaskByActivity(activityName, WINDOWING_MODE_UNDEFINED)
+
+    fun getTaskByActivity(activityName: String, windowingMode: Int): ActivityTask? {
+        for (stack in rootTasks) {
+            if (windowingMode == WINDOWING_MODE_UNDEFINED || windowingMode == stack.windowingMode) {
+                val task = stack.getTask { it.getActivity(activityName) != null }
+                if (task != null) {
+                    return task
+                }
+            }
+        }
+        return null
+    }
+
+    /**
+     * Get the number of activities in the task, with the option to count only activities with
+     * specific name.
+     * @param taskId Id of the task where we're looking for the number of activities.
+     * @param activityName Optional name of the activity we're interested in.
+     * @return Number of all activities in the task if activityName is `null`, otherwise will
+     * report number of activities that have specified name.
+     */
+    fun getActivityCountInTask(taskId: Int, activityName: String?): Int {
+        // If activityName is null, count all activities in the task.
+        // Otherwise count activities that have specified name.
+        for (stack in rootTasks) {
+            val task = stack.getTask(taskId) ?: continue
+
+            if (activityName == null) {
+                return task.activities.size
+            }
+            var count = 0
+            for (activity in task.activities) {
+                if (activity.title == activityName) {
+                    count++
+                }
+            }
+            return count
+        }
+        return 0
+    }
+
+    fun getRootTasksCount(displayId: Int): Int {
+        var count = 0
+        for (rootTask in rootTasks) {
+            if (rootTask.displayId == displayId) ++count
+        }
+        return count
+    }
+
+    fun pendingActivityContain(activityName: String): Boolean {
+        return pendingActivities.contains(activityName)
+    }
+
+    fun getMatchingVisibleWindowState(windowName: String): List<WindowState> {
+        return windowStates.filter { it.isSurfaceShown && it.title == windowName }
+    }
+
+    fun getWindowByPackageName(packageName: String, windowType: Int): WindowState? =
+        getWindowsByPackageName(packageName, windowType).firstOrNull()
+
+    fun getWindowsByPackageName(
+        packageName: String,
+        vararg restrictToTypes: Int
+    ): List<WindowState> =
+        windowStates.filter { ws ->
+            ((ws.title == packageName ||
+                ws.title.startsWith("$packageName/")) &&
+                restrictToTypes.any { type -> type == ws.attributes.type })
+        }
+
+    fun getMatchingWindowType(type: Int): List<WindowState> =
+        windowStates.filter { it.attributes.type == type }
+
+    fun getMatchingWindowTokens(windowName: String): List<String> =
+        windowStates.filter { it.title === windowName }.map { it.token }
+
+    fun getNavBarWindow(displayId: Int): WindowState? {
+        val navWindow = windowStates.filter { it.isValidNavBarType && it.displayId == displayId }
+
+        // We may need some time to wait for nav bar showing.
+        // It's Ok to get 0 nav bar here.
+        if (navWindow.size > 1) {
+            throw IllegalStateException("There should be at most one navigation bar on a display")
+        }
+        return navWindow.firstOrNull()
+    }
+
+    fun getWindowStateForAppToken(appToken: String): WindowState? =
+        windowStates.firstOrNull { it.token == appToken }
+
+    /**
+     * Check if there exists a window record with matching windowName.
+     */
+    fun containsWindow(windowName: String): Boolean =
+        windowStates.any { it.title == windowName }
+
+    /**
+     * Check if at least one window which matches the specified name has shown it's surface.
+     */
+    fun isWindowSurfaceShown(windowName: String): Boolean {
+        for (window in windowStates) {
+            if (window.title == windowName) {
+                if (window.isSurfaceShown) {
+                    return true
+                }
+            }
+        }
+        return false
+    }
+
+    /**
+     * Check if at least one window which matches provided window name is visible.
+     */
+    fun isWindowVisible(windowName: String): Boolean =
+        visibleWindows.any { it.title == windowName }
+
+    /**
+     * Checks if the state has any window in PIP mode
+     */
+    fun hasPipWindow(): Boolean = pinnedWindows.isNotEmpty()
+
+    /**
+     * Checks that an activity [windowName] is in PIP mode
+     */
+    fun isInPipMode(windowName: String): Boolean {
+        return pinnedWindows.any { it.title.contains(windowName) }
+    }
+
+    /**
+     * Checks whether the display contains the given activity.
+     */
+    fun hasActivityInDisplay(displayId: Int, activityName: String): Boolean {
+        for (stack in getDisplay(displayId)!!.rootTasks) {
+            if (stack.containsActivity(activityName)) {
+                return true
+            }
+        }
+        return false
+    }
+
+    fun findFirstWindowWithType(type: Int): WindowState? =
+        windowStates.firstOrNull { it.attributes.type == type }
+
+    fun getZOrder(w: WindowState): Int = windowStates.size - windowStates.indexOf(w)
+
+    fun getStandardRootStackByWindowingMode(windowingMode: Int): ActivityTask? {
+        for (task in rootTasks) {
+            if (task.activityType != ACTIVITY_TYPE_STANDARD) {
+                continue
+            }
+            if (task.windowingMode == windowingMode) {
+                return task
+            }
+        }
+        return null
+    }
+
+    fun defaultMinimalTaskSize(displayId: Int): Int =
+        dpToPx(DEFAULT_RESIZABLE_TASK_SIZE_DP.toFloat(), getDisplay(displayId)!!.dpi)
+
+    fun defaultMinimalDisplaySizeForSplitScreen(displayId: Int): Int {
+        return dpToPx(DEFAULT_MINIMAL_SPLIT_SCREEN_DISPLAY_SIZE_DP.toFloat(),
+            getDisplay(displayId)!!.dpi)
+    }
+
+    fun getIsIncompleteReason(): String {
+        return buildString {
+            if (rootTasks.isEmpty()) {
+                append("No stacks found...")
+            }
+            if (focusedStackId == -1) {
+                append("No focused stack found...")
+            }
+            if (focusedActivity.isEmpty()) {
+                append("No focused activity found...")
+            }
+            if (resumedActivities.isEmpty()) {
+                append("No resumed activities found...")
+            }
+            if (windowStates.isEmpty()) {
+                append("No Windows found...")
+            }
+            if (focusedWindow.isEmpty()) {
+                append("No Focused Window...")
+            }
+            if (focusedApp.isEmpty()) {
+                append("No Focused App...")
+            }
+            if (keyguardControllerState.isKeyguardShowing) {
+                append("Keyguard showing...")
+            }
+        }
+    }
+
+    fun isComplete(): Boolean = !isIncomplete()
+    fun isIncomplete(): Boolean {
+        return rootTasks.isEmpty() || focusedStackId == -1 || windowStates.isEmpty() ||
+            (focusedApp.isEmpty() && homeActivity == null) || focusedWindow.isEmpty() ||
+            (focusedActivity.isEmpty() || resumedActivities.isEmpty()) &&
+            !keyguardControllerState.isKeyguardShowing
+    }
+
+    override fun toString(): String {
+        return prettyTimestamp(timestamp)
+    }
+
+    companion object {
+        const val STATE_INITIALIZING = "INITIALIZING"
+        const val STATE_RESUMED = "RESUMED"
+        const val STATE_PAUSED = "PAUSED"
+        const val STATE_STOPPED = "STOPPED"
+        const val STATE_DESTROYED = "DESTROYED"
+        const val APP_STATE_IDLE = "APP_STATE_IDLE"
+        internal const val ACTIVITY_TYPE_UNDEFINED = 0
+        internal const val ACTIVITY_TYPE_STANDARD = 1
+        internal const val DEFAULT_DISPLAY = 0
+        internal const val DEFAULT_MINIMAL_SPLIT_SCREEN_DISPLAY_SIZE_DP = 440
+        internal const val INVALID_STACK_ID = -1
+        internal const val ACTIVITY_TYPE_HOME = 2
+        internal const val ACTIVITY_TYPE_RECENTS = 3
+        internal const val ACTIVITY_TYPE_DREAM = 5
+        internal const val WINDOWING_MODE_UNDEFINED = 0
+        private const val DENSITY_DEFAULT = 160
+        /**
+         * @see android.app.WindowConfiguration.WINDOWING_MODE_PINNED
+         */
+        private const val WINDOWING_MODE_PINNED = 2
+
+        /**
+         * @see WindowManager.LayoutParams
+         */
+        internal const val TYPE_NAVIGATION_BAR_PANEL = 2024
+
+        // Default minimal size of resizable task, used if none is set explicitly.
+        // Must be kept in sync with 'default_minimal_size_resizable_task'
+        // dimen from frameworks/base.
+        internal const val DEFAULT_RESIZABLE_TASK_SIZE_DP = 220
+
+        fun dpToPx(dp: Float, densityDpi: Int): Int {
+            return (dp * densityDpi / DENSITY_DEFAULT + 0.5f).toInt()
+        }
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/WindowManagerTrace.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/WindowManagerTrace.kt
new file mode 100644
index 0000000..cd937ac
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/WindowManagerTrace.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2021 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.traces.common.windowmanager
+
+import com.android.server.wm.traces.common.ITrace
+
+/**
+ * Contains a collection of parsed WindowManager trace entries and assertions to apply over a single
+ * entry.
+ *
+ * Each entry is parsed into a list of [WindowManagerState] objects.
+ *
+ * This is a generic object that is reused by both Flicker and Winscope and cannot
+ * access internal Java/Android functionality
+ *
+ */
+open class WindowManagerTrace(
+    override val entries: List<WindowManagerState>,
+    override val source: String,
+    override val sourceChecksum: String
+) : ITrace<WindowManagerState>,
+    List<WindowManagerState> by entries {
+    override fun toString(): String {
+        return "WindowManagerTrace(Start: ${entries.first()}, " +
+            "End: ${entries.last()})"
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/Activity.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/Activity.kt
new file mode 100644
index 0000000..cff1a7f
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/Activity.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.server.wm.traces.common.windowmanager.windows
+
+/**
+ * Represents an activity in the window manager hierarchy
+ *
+ * This is a generic object that is reused by both Flicker and Winscope and cannot
+ * access internal Java/Android functionality
+ *
+ **/
+open class Activity(
+    name: String,
+    val state: String,
+    visible: Boolean,
+    val frontOfTask: Boolean,
+    val procId: Int,
+    val isTranslucent: Boolean,
+    windowContainer: WindowContainer
+) : WindowContainer(windowContainer, name, visible) {
+    /**
+     * Checks if the activity contains a window with title containing [partialWindowTitle]
+     *
+     * @param partialWindowTitle window title to search
+     */
+    fun hasWindow(partialWindowTitle: String): Boolean {
+        return this.windows.any { it.title.contains(partialWindowTitle) }
+    }
+
+    override fun toString(): String {
+        return "${this::class.simpleName}: {$token $title} state=$state visible=$isVisible"
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/ActivityTask.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/ActivityTask.kt
new file mode 100644
index 0000000..63f80a3
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/ActivityTask.kt
@@ -0,0 +1,99 @@
+/*
+ * 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.server.wm.traces.common.windowmanager.windows
+
+import com.android.server.wm.traces.common.Rect
+
+/**
+ * Represents a task in the window manager hierarchy
+ *
+ * This is a generic object that is reused by both Flicker and Winscope and cannot
+ * access internal Java/Android functionality
+ *
+ */
+open class ActivityTask(
+    override val activityType: Int,
+    override val isFullscreen: Boolean,
+    override val bounds: Rect,
+    val taskId: Int,
+    val rootTaskId: Int,
+    val displayId: Int,
+    _lastNonFullscreenBounds: Rect?,
+    val realActivity: String,
+    val origActivity: String,
+    val resizeMode: Int,
+    private val _resumedActivity: String,
+    var animatingBounds: Boolean,
+    val surfaceWidth: Int,
+    val surfaceHeight: Int,
+    val createdByOrganizer: Boolean,
+    val minWidth: Int,
+    val minHeight: Int,
+    windowContainer: WindowContainer
+) : WindowContainer(windowContainer) {
+    override val isVisible: Boolean = false
+    override val name: String = taskId.toString()
+    override val isEmpty: Boolean get() = tasks.isEmpty() && activities.isEmpty()
+
+    val lastNonFullscreenBounds: Rect = _lastNonFullscreenBounds ?: Rect.EMPTY
+    val isRootTask: Boolean get() = taskId == rootTaskId
+    val tasks: List<ActivityTask>
+        get() = this.children.reversed().filterIsInstance<ActivityTask>()
+    val activities: List<Activity>
+        get() = this.children.reversed().filterIsInstance<Activity>()
+    /** The top task in the stack.
+     */
+    // NOTE: Unlike the WindowManager internals, we dump the state from top to bottom,
+    //       so the indices are inverted
+    val topTask: ActivityTask? get() = tasks.firstOrNull()
+    val resumedActivities: Array<String> get() {
+        val result = mutableSetOf<String>()
+        if (this._resumedActivity.isNotEmpty()) {
+            result.add(this._resumedActivity)
+        }
+        val activitiesInChildren = this.tasks
+            .flatMap { it.resumedActivities.toList() }
+            .filter { it.isNotEmpty() }
+        result.addAll(activitiesInChildren)
+        return result.toTypedArray()
+    }
+
+    fun getTask(predicate: (ActivityTask) -> Boolean) =
+        tasks.firstOrNull { predicate(it) } ?: if (predicate(this)) this else null
+
+    fun getTask(taskId: Int) = getTask { t -> t.taskId == taskId }
+
+    fun forAllTasks(consumer: (ActivityTask) -> Any) {
+        tasks.forEach { consumer(it) }
+    }
+
+    fun getActivity(predicate: (Activity) -> Boolean): Activity? {
+        return activities.firstOrNull { predicate(it) }
+            ?: tasks.flatMap { it.activities }
+                .firstOrNull { predicate(it) }
+    }
+
+    fun getActivity(activityName: String): Activity? {
+        return getActivity { activity -> activity.title == activityName }
+    }
+
+    fun containsActivity(activityName: String) = getActivity(activityName) != null
+
+    override fun toString(): String {
+        return "${this::class.simpleName}: {$token $title} id=$taskId bounds=$bounds"
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/Configuration.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/Configuration.kt
new file mode 100644
index 0000000..44c64f3
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/Configuration.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.server.wm.traces.common.windowmanager.windows
+
+/**
+ * Represents the configuration of a WM container
+ *
+ * This is a generic object that is reused by both Flicker and Winscope and cannot
+ * access internal Java/Android functionality
+ *
+ */
+data class Configuration(
+    val windowConfiguration: WindowConfiguration?,
+    val densityDpi: Int,
+    val orientation: Int,
+    val screenHeightDp: Int,
+    val screenWidthDp: Int,
+    val smallestScreenWidthDp: Int,
+    val screenLayout: Int,
+    val uiMode: Int
+) {
+    val isEmpty: Boolean
+        get() = (windowConfiguration == null) &&
+            densityDpi == 0 &&
+            orientation == 0 &&
+            screenHeightDp == 0 &&
+            screenWidthDp == 0 &&
+            smallestScreenWidthDp == 0 &&
+            screenLayout == 0 &&
+            uiMode == 0
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/ConfigurationContainer.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/ConfigurationContainer.kt
new file mode 100644
index 0000000..4ba0b6a
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/ConfigurationContainer.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.server.wm.traces.common.windowmanager.windows
+
+/**
+ * Represents the configuration of an element in the window manager hierarchy
+ *
+ * This is a generic object that is reused by both Flicker and Winscope and cannot
+ * access internal Java/Android functionality
+ *
+ */
+open class ConfigurationContainer(
+    val overrideConfiguration: Configuration?,
+    val fullConfiguration: Configuration?,
+    val mergedOverrideConfiguration: Configuration?
+) {
+    constructor(configurationContainer: ConfigurationContainer) : this(
+        configurationContainer.overrideConfiguration,
+        configurationContainer.fullConfiguration,
+        configurationContainer.mergedOverrideConfiguration
+    )
+
+    val windowingMode: Int get() = fullConfiguration?.windowConfiguration?.windowingMode ?: 0
+
+    open val activityType: Int get() = fullConfiguration?.windowConfiguration?.activityType ?: 0
+
+    open val isEmpty: Boolean
+        get() = (overrideConfiguration?.isEmpty ?: true) &&
+            (fullConfiguration?.isEmpty ?: true) &&
+            (mergedOverrideConfiguration?.isEmpty ?: true)
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/DisplayArea.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/DisplayArea.kt
new file mode 100644
index 0000000..3067ac6
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/DisplayArea.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.server.wm.traces.common.windowmanager.windows
+
+/**
+ * Represents a display area in the window manager hierarchy
+ *
+ * This is a generic object that is reused by both Flicker and Winscope and cannot
+ * access internal Java/Android functionality
+ *
+ */
+open class DisplayArea(
+    val isTaskDisplayArea: Boolean,
+    windowContainer: WindowContainer
+) : WindowContainer(windowContainer) {
+    val activities: Array<Activity>
+        get() = if (isTaskDisplayArea) {
+            this.collectDescendants()
+        } else {
+            emptyArray()
+        }
+
+    fun containsActivity(activityName: String): Boolean {
+        return if (!isTaskDisplayArea) {
+            false
+        } else {
+            activities.any { it.title == activityName }
+        }
+    }
+
+    override fun toString(): String {
+        return "${this::class.simpleName} {$token $title} isTaskArea=$isTaskDisplayArea"
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/DisplayContent.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/DisplayContent.kt
new file mode 100644
index 0000000..cb21672
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/DisplayContent.kt
@@ -0,0 +1,96 @@
+/*
+ * 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.server.wm.traces.common.windowmanager.windows
+
+import com.android.server.wm.traces.common.Rect
+
+/**
+ * Represents a display content in the window manager hierarchy
+ *
+ * This is a generic object that is reused by both Flicker and Winscope and cannot
+ * access internal Java/Android functionality
+ *
+ */
+open class DisplayContent(
+    val id: Int,
+    val focusedRootTaskId: Int,
+    val resumedActivity: String,
+    val singleTaskInstance: Boolean,
+    _defaultPinnedStackBounds: Rect?,
+    _pinnedStackMovementBounds: Rect?,
+    val displayRect: Rect,
+    val appRect: Rect,
+    val dpi: Int,
+    val flags: Int,
+    _stableBounds: Rect?,
+    val surfaceSize: Int,
+    val focusedApp: String,
+    val lastTransition: String,
+    val appTransitionState: String,
+    val rotation: Int,
+    val lastOrientation: Int,
+    windowContainer: WindowContainer
+) : WindowContainer(windowContainer) {
+    override val name: String = id.toString()
+    override val isVisible: Boolean = false
+
+    val defaultPinnedStackBounds: Rect = _defaultPinnedStackBounds ?: Rect.EMPTY
+    val pinnedStackMovementBounds: Rect = _pinnedStackMovementBounds ?: Rect.EMPTY
+    val stableBounds: Rect = _stableBounds ?: Rect.EMPTY
+
+    val rootTasks: Array<ActivityTask>
+        get() {
+            val tasks = this.collectDescendants<ActivityTask> { it.isRootTask }.toMutableList()
+            // TODO(b/149338177): figure out how CTS tests deal with organizer. For now,
+            //                    don't treat them as regular stacks
+            val rootOrganizedTasks = mutableListOf<ActivityTask>()
+            val reversedTaskList = tasks.reversed()
+            reversedTaskList.forEach { task ->
+                // Skip tasks created by an organizer
+                if (task.createdByOrganizer) {
+                    tasks.remove(task)
+                    rootOrganizedTasks.add(task)
+                }
+            }
+            // Add root tasks controlled by an organizer
+            rootOrganizedTasks.reversed().forEach { task ->
+                tasks.addAll(task.children.reversed().map { it as ActivityTask })
+            }
+
+            return tasks.toTypedArray()
+        }
+
+    fun containsActivity(activityName: String): Boolean =
+        rootTasks.any { it.containsActivity(activityName) }
+
+    fun getTaskDisplayArea(activityName: String): DisplayArea? {
+        val taskDisplayAreas = this.collectDescendants<DisplayArea> { it.isTaskDisplayArea }
+            .filter { it.containsActivity(activityName) }
+
+        if (taskDisplayAreas.size > 1) {
+            throw IllegalArgumentException(
+                "There must be exactly one activity among all TaskDisplayAreas.")
+        }
+
+        return taskDisplayAreas.firstOrNull()
+    }
+
+    override fun toString(): String {
+        return "${this::class.simpleName} #$id: name=$title mDisplayRect=$displayRect " +
+            "mAppRect=$appRect mFlags=$flags"
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/KeyguardControllerState.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/KeyguardControllerState.kt
new file mode 100644
index 0000000..131e970
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/KeyguardControllerState.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.server.wm.traces.common.windowmanager.windows
+
+/**
+ * Represents the keyguard controller in the window manager hierarchy
+ *
+ * This is a generic object that is reused by both Flicker and Winscope and cannot
+ * access internal Java/Android functionality
+ *
+ */
+data class KeyguardControllerState(
+    val isAodShowing: Boolean,
+    val isKeyguardShowing: Boolean,
+    val keyguardOccludedStates: Map<Int, Boolean>
+) {
+    fun isKeyguardOccluded(displayId: Int): Boolean =
+        keyguardOccludedStates[displayId] ?: false
+
+    override fun toString(): String {
+        return "KeyguardControllerState: {aod=$isAodShowing keyguard=$isKeyguardShowing}"
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/RootWindowContainer.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/RootWindowContainer.kt
new file mode 100644
index 0000000..f25b41d
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/RootWindowContainer.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.server.wm.traces.common.windowmanager.windows
+
+/**
+ * Represents the root window container in the window manager hierarchy
+ *
+ * This is a generic object that is reused by both Flicker and Winscope and cannot
+ * access internal Java/Android functionality
+ *
+ */
+open class RootWindowContainer(
+    windowContainer: WindowContainer
+) : WindowContainer(windowContainer) {
+    override fun toString(): String {
+        return "${this::class.simpleName}: {$token $title}"
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowConfiguration.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowConfiguration.kt
new file mode 100644
index 0000000..ffdda82
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowConfiguration.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.server.wm.traces.common.windowmanager.windows
+
+import com.android.server.wm.traces.common.Rect
+
+/**
+ * Represents the configuration of a WM window
+ *
+ * This is a generic object that is reused by both Flicker and Winscope and cannot
+ * access internal Java/Android functionality
+ *
+ */
+open class WindowConfiguration(
+    _appBounds: Rect?,
+    _bounds: Rect?,
+    _maxBounds: Rect?,
+    val windowingMode: Int,
+    val activityType: Int
+) {
+    val appBounds: Rect = _appBounds ?: Rect.EMPTY
+    val bounds: Rect = _bounds ?: Rect.EMPTY
+    val maxBounds: Rect = _maxBounds ?: Rect.EMPTY
+
+    val isEmpty: Boolean
+        get() = appBounds.isEmpty &&
+            bounds.isEmpty &&
+            maxBounds.isEmpty &&
+            windowingMode == 0 &&
+            activityType == 0
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowContainer.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowContainer.kt
new file mode 100644
index 0000000..db48e0d
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowContainer.kt
@@ -0,0 +1,118 @@
+/*
+ * 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.server.wm.traces.common.windowmanager.windows
+
+import com.android.server.wm.traces.common.Rect
+
+/**
+ * Represents WindowContainer classes such as DisplayContent.WindowContainers and
+ * DisplayContent.NonAppWindowContainers. This can be expanded into a specific class
+ * if we need track and assert some state in the future.
+ *
+ * This is a generic object that is reused by both Flicker and Winscope and cannot
+ * access internal Java/Android functionality
+ *
+ */
+open class WindowContainer constructor(
+    val title: String,
+    val token: String,
+    val orientation: Int,
+    _isVisible: Boolean,
+    configurationContainer: ConfigurationContainer,
+    val children: Array<WindowContainer>
+) : ConfigurationContainer(configurationContainer) {
+    protected constructor(
+        windowContainer: WindowContainer,
+        titleOverride: String? = null,
+        isVisibleOverride: Boolean? = null
+    ) : this(
+        titleOverride ?: windowContainer.title,
+        windowContainer.token,
+        windowContainer.orientation,
+        isVisibleOverride ?: windowContainer.isVisible,
+        windowContainer,
+        windowContainer.children
+    )
+
+    open val isVisible: Boolean = _isVisible
+    open val name: String = title
+    val stableId: String get() = "${this::class.simpleName} $token $title"
+    open val isFullscreen: Boolean = false
+    open val bounds: Rect = Rect.EMPTY
+
+    val windows: Array<WindowState>
+        get() = this.collectDescendants()
+
+    fun traverseTopDown(): List<WindowContainer> {
+        val traverseList = mutableListOf(this)
+
+        this.children.reversed()
+            .forEach { childLayer ->
+                traverseList.addAll(childLayer.traverseTopDown())
+            }
+
+        return traverseList
+    }
+
+    /**
+     * For a given WindowContainer, traverse down the hierarchy and collect all children of type
+     * [T] if the child passes the test [predicate].
+     *
+     * @param predicate Filter function
+     */
+    inline fun <reified T : WindowContainer> collectDescendants(
+        predicate: (T) -> Boolean = { true }
+    ): Array<T> {
+        val traverseList = traverseTopDown()
+
+        return traverseList.filterIsInstance<T>()
+            .filter { predicate(it) }
+            .toTypedArray()
+    }
+
+    override fun toString(): String {
+        if (this.title.isEmpty() || listOf("WindowContainer", "Task")
+                .any { it.contains(this.title) }) {
+            return ""
+        }
+
+        return "$${removeRedundancyInName(this.title)}@${this.token}"
+    }
+
+    private fun removeRedundancyInName(name: String): String {
+        if (!name.contains('/')) {
+            return name
+        }
+
+        val split = name.split('/')
+        val pkg = split[0]
+        var clazz = split.slice(1..split.lastIndex).joinToString("/")
+
+        if (clazz.startsWith("$pkg.")) {
+            clazz = clazz.slice(pkg.length + 1..clazz.lastIndex)
+
+            return "$pkg/$clazz"
+        }
+
+        return name
+    }
+
+    override val isEmpty: Boolean
+        get() = super.isEmpty &&
+            title.isEmpty() &&
+            token.isEmpty()
+}
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowLayoutParams.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowLayoutParams.kt
new file mode 100644
index 0000000..def86db
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowLayoutParams.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2021 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.traces.common.windowmanager.windows
+
+/**
+ * Represents the attributes of a WindowState in the window manager hierarchy
+ *
+ * This is a generic object that is reused by both Flicker and Winscope and cannot
+ * access internal Java/Android functionality
+ *
+ */
+data class WindowLayoutParams(
+    val type: Int,
+    val x: Int,
+    val y: Int,
+    val width: Int,
+    val height: Int,
+    val horizontalMargin: Float,
+    val verticalMargin: Float,
+    val gravity: Int,
+    val softInputMode: Int,
+    val format: Int,
+    val windowAnimations: Int,
+    val alpha: Float,
+    val screenBrightness: Float,
+    val buttonBrightness: Float,
+    val rotationAnimation: Int,
+    val preferredRefreshRate: Float,
+    val preferredDisplayModeId: Int,
+    val hasSystemUiListeners: Boolean,
+    val inputFeatureFlags: Int,
+    val userActivityTimeout: Long,
+    val colorMode: Int,
+    val flags: Int,
+    val privateFlags: Int,
+    val systemUiVisibilityFlags: Int,
+    val subtreeSystemUiVisibilityFlags: Int,
+    val appearance: Int,
+    val behavior: Int,
+    val fitInsetsTypes: Int,
+    val fitInsetsSides: Int,
+    val fitIgnoreVisibility: Boolean
+) {
+    val isValidNavBarType: Boolean = this.type == TYPE_NAVIGATION_BAR
+
+    companion object {
+        /**
+         * @see WindowManager.LayoutParams
+         */
+        private const val TYPE_NAVIGATION_BAR = 2019
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowManagerPolicy.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowManagerPolicy.kt
new file mode 100644
index 0000000..2bc52d0
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowManagerPolicy.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.server.wm.traces.common.windowmanager.windows
+
+/**
+ * Represents the requested policy of a WM container
+ *
+ * This is a generic object that is reused by both Flicker and Winscope and cannot
+ * access internal Java/Android functionality
+ *
+ */
+data class WindowManagerPolicy(
+    val focusedAppToken: String,
+    val forceStatusBar: Boolean,
+    val forceStatusBarFromKeyguard: Boolean,
+    val keyguardDrawComplete: Boolean,
+    val keyguardOccluded: Boolean,
+    val keyguardOccludedChanged: Boolean,
+    val keyguardOccludedPending: Boolean,
+    val lastSystemUiFlags: Int,
+    val orientation: Int,
+    val rotation: Int,
+    val rotationMode: Int,
+    val screenOnFully: Boolean,
+    val windowManagerDrawComplete: Boolean
+)
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowState.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowState.kt
new file mode 100644
index 0000000..d85e860
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowState.kt
@@ -0,0 +1,141 @@
+/*
+ * 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.server.wm.traces.common.windowmanager.windows
+
+import com.android.server.wm.traces.common.Bounds
+import com.android.server.wm.traces.common.Rect
+import com.android.server.wm.traces.common.Region
+
+/**
+ * Represents a window in the window manager hierarchy
+ *
+ * This is a generic object that is reused by both Flicker and Winscope and cannot
+ * access internal Java/Android functionality
+ *
+ */
+open class WindowState(
+    val attributes: WindowLayoutParams,
+    val displayId: Int,
+    val stackId: Int,
+    val layer: Int,
+    val isSurfaceShown: Boolean,
+    val windowType: Int,
+    val requestedSize: Bounds,
+    val surfacePosition: Rect?,
+    _frame: Rect?,
+    _containingFrame: Rect?,
+    _parentFrame: Rect?,
+    _contentFrame: Rect?,
+    _contentInsets: Rect?,
+    _surfaceInsets: Rect?,
+    _givenContentInsets: Rect?,
+    _crop: Rect?,
+    windowContainer: WindowContainer,
+    val isAppWindow: Boolean
+) : WindowContainer(windowContainer, getWindowTitle(windowContainer.title)) {
+    override val isVisible: Boolean get() = super.isVisible && attributes.alpha > 0
+
+    val frame: Rect = _frame ?: Rect.EMPTY
+    val containingFrame: Rect = _containingFrame ?: Rect.EMPTY
+    val parentFrame: Rect = _parentFrame ?: Rect.EMPTY
+    val contentFrame: Rect = _contentFrame ?: Rect.EMPTY
+    val contentInsets: Rect = _contentInsets ?: Rect.EMPTY
+    val surfaceInsets: Rect = _surfaceInsets ?: Rect.EMPTY
+    val givenContentInsets: Rect = _givenContentInsets ?: Rect.EMPTY
+    val crop: Rect = _crop ?: Rect.EMPTY
+    override val isFullscreen: Boolean get() = this.attributes.flags.and(FLAG_FULLSCREEN) > 0
+
+    val isStartingWindow: Boolean = windowType == WINDOW_TYPE_STARTING
+    val isExitingWindow: Boolean = windowType == WINDOW_TYPE_EXITING
+    val isDebuggerWindow: Boolean = windowType == WINDOW_TYPE_DEBUGGER
+    val isValidNavBarType: Boolean = attributes.isValidNavBarType
+
+    val frameRegion: Region = Region(frame)
+
+    private fun getWindowTypeSuffix(windowType: Int): String =
+        when (windowType) {
+            WINDOW_TYPE_STARTING -> " STARTING"
+            WINDOW_TYPE_EXITING -> " EXITING"
+            WINDOW_TYPE_DEBUGGER -> " DEBUGGER"
+            else -> ""
+        }
+
+    override fun toString(): String = "${this::class.simpleName}: " +
+        "{$token $title${getWindowTypeSuffix(windowType)}} " +
+        "type=${attributes.type} cf=$containingFrame pf=$parentFrame"
+
+    override fun equals(other: Any?): Boolean {
+        return other is WindowState &&
+            other.stableId == stableId &&
+            other.attributes == attributes &&
+            other.token == token &&
+            other.title == title &&
+            other.containingFrame == containingFrame &&
+            other.parentFrame == parentFrame
+    }
+
+    override fun hashCode(): Int {
+        var result = attributes.hashCode()
+        result = 31 * result + displayId
+        result = 31 * result + stackId
+        result = 31 * result + layer
+        result = 31 * result + isSurfaceShown.hashCode()
+        result = 31 * result + windowType
+        result = 31 * result + frame.hashCode()
+        result = 31 * result + containingFrame.hashCode()
+        result = 31 * result + parentFrame.hashCode()
+        result = 31 * result + contentFrame.hashCode()
+        result = 31 * result + contentInsets.hashCode()
+        result = 31 * result + surfaceInsets.hashCode()
+        result = 31 * result + givenContentInsets.hashCode()
+        result = 31 * result + crop.hashCode()
+        result = 31 * result + isAppWindow.hashCode()
+        result = 31 * result + isStartingWindow.hashCode()
+        result = 31 * result + isExitingWindow.hashCode()
+        result = 31 * result + isDebuggerWindow.hashCode()
+        result = 31 * result + isValidNavBarType.hashCode()
+        result = 31 * result + frameRegion.hashCode()
+        return result
+    }
+
+    companion object {
+        /**
+         * From {@see android.view.WindowManager.FLAG_FULLSCREEN}.
+         *
+         * This class is shared between JVM and JS (Winscope) and cannot access
+         * Android internals
+         */
+        private const val FLAG_FULLSCREEN = 0x00000400
+        internal const val WINDOW_TYPE_STARTING = 1
+        internal const val WINDOW_TYPE_EXITING = 2
+        private const val WINDOW_TYPE_DEBUGGER = 3
+
+        internal const val STARTING_WINDOW_PREFIX = "Starting "
+        internal const val DEBUGGER_WINDOW_PREFIX = "Waiting For Debugger: "
+
+        private fun getWindowTitle(title: String): String {
+            return when {
+                // Existing code depends on the prefix being removed
+                title.startsWith(STARTING_WINDOW_PREFIX) ->
+                    title.substring(STARTING_WINDOW_PREFIX.length)
+                title.startsWith(DEBUGGER_WINDOW_PREFIX) ->
+                    title.substring(DEBUGGER_WINDOW_PREFIX.length)
+                else -> title
+            }
+        }
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowToken.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowToken.kt
new file mode 100644
index 0000000..cfcaafc
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowToken.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.server.wm.traces.common.windowmanager.windows
+
+/**
+ * Represents a window token in the window manager hierarchy
+ *
+ * This is a generic object that is reused by both Flicker and Winscope and cannot
+ * access internal Java/Android functionality
+ *
+ */
+open class WindowToken(windowContainer: WindowContainer) : WindowContainer(windowContainer) {
+    override val isVisible: Boolean get() = false
+    override fun toString(): String {
+        return "${this::class.simpleName}: {$token $title}"
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/traces/parser/Condition.kt b/libraries/flicker/src/com/android/server/wm/traces/parser/Condition.kt
new file mode 100644
index 0000000..be03b2d
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/parser/Condition.kt
@@ -0,0 +1,219 @@
+/*
+ * 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.server.wm.traces.parser
+
+import android.os.SystemClock
+import android.util.Log
+
+/**
+ * The utility class to wait a condition with customized options.
+ * The default retry policy is 5 times with interval 1 second.
+ *
+ * @param <T> The type of the object to validate.
+ *
+ * <p>Sample:</p>
+ * <pre>
+ * // Simple case.
+ * if (Condition.waitFor("true value", () -> true)) {
+ *     println("Success");
+ * }
+ * // Wait for customized result with customized validation.
+ * String result = Condition.waitForResult(new Condition<String>("string comparison")
+ *         .setResultSupplier(() -> "Result string")
+ *         .setResultValidator(str -> str.equals("Expected string"))
+ *         .setRetryIntervalMs(500)
+ *         .setRetryLimit(3)
+ *         .setOnFailure(str -> println("Failed on " + str)));
+ * </pre>
+ */
+class Condition<T>
+/**
+ * Constructs with a simple boolean condition.
+ *
+ * When satisfier = null, it is expected that the condition will be configured with
+ * [.setResultSupplier] and [.setResultValidator].
+ *
+ * @param message The message to show what is waiting for.
+ * @param satisfier If it returns true, that means the condition is satisfied.
+ */
+@JvmOverloads constructor(
+    private var message: String = "",
+    /**
+     * It decides whether this condition is satisfied.
+     */
+    private var satisfier: (() -> Boolean)? = null,
+    private var retryLimit: Int = DEFAULT_RETRY_LIMIT,
+    private var retryIntervalMs: Long = DEFAULT_RETRY_INTERVAL_MS
+) {
+    private var returnLastResult: Boolean = false
+
+    /**
+     * It is used when the condition is not a simple boolean expression, such as the caller may
+     * want to get the validated product as the return value.
+     */
+    private var resultSupplier: (() -> T?)? = null
+
+    /**
+     * It validates the result from [.mResultSupplier].
+     */
+    private var resultValidator: ((T?) -> Boolean)? = null
+    private var onFailure: ((T) -> Any)? = null
+    private var onRetry: Runnable? = null
+    private var lastResult: T? = null
+    private var validatedResult: T? = null
+
+    /**
+     * Set the supplier which provides the result object to validate.
+     */
+    fun setResultSupplier(supplier: () -> T?): Condition<T> =
+        apply { resultSupplier = supplier }
+
+    /**
+     * Set the validator which tests the object provided by the supplier.
+     */
+    fun setResultValidator(validator: (T?) -> Boolean): Condition<T> =
+        apply { resultValidator = validator }
+
+    /**
+     * If true, when using [.waitForResult], the method will return the last result
+     * provided by [.mResultSupplier] even it is not valid (by [.mResultValidator]).
+     */
+    fun setReturnLastResult(returnLastResult: Boolean): Condition<T> =
+        apply { this.returnLastResult = returnLastResult }
+
+    /**
+     * Executes the action when the condition does not satisfy within the time limit. The passed
+     * object to the consumer will be the last result from the supplier.
+     */
+    fun setOnFailure(onFailure: (T) -> Any): Condition<T> = apply { this.onFailure = onFailure }
+
+    fun setOnRetry(onRetry: Runnable): Condition<T> = apply { this.onRetry = onRetry }
+
+    fun setRetryIntervalMs(millis: Long): Condition<T> = apply { retryIntervalMs = millis }
+
+    fun setRetryLimit(limit: Int): Condition<T> = apply { retryLimit = limit }
+
+    /**
+     * Build the condition by [.mResultSupplier] and [.mResultValidator].
+     */
+    private fun prepareSatisfier(): () -> Boolean {
+        val supplier = resultSupplier
+        val validator = resultValidator
+        require(!(supplier == null || validator == null)) { "No specified condition" }
+
+        return {
+            val result = supplier.invoke()
+            lastResult = result
+            if (validator.invoke(result)) {
+                validatedResult = result
+                true
+            } else {
+                false
+            }
+        }.also {
+            satisfier = it
+        }
+    }
+
+    companion object {
+        // TODO(b/112837428): Implement a incremental retry policy to reduce the unnecessary
+        // constant time, currently keep the default as 5*1s because most of the original code
+        // uses it, and some tests might be sensitive to the waiting interval.
+        private const val DEFAULT_RETRY_LIMIT = 5
+        private const val DEFAULT_RETRY_INTERVAL_MS = 1000L
+
+        /**
+         * @see .waitFor
+         * @see .Condition
+         */
+        @JvmStatic
+        @JvmOverloads
+        fun <T> waitFor(
+            message: String,
+            retryLimit: Int = DEFAULT_RETRY_LIMIT,
+            retryIntervalMs: Long = DEFAULT_RETRY_INTERVAL_MS,
+            satisfier: () -> Boolean
+        ): Boolean {
+            val condition = Condition<T>(message, satisfier, retryLimit, retryIntervalMs)
+            return waitFor(condition)
+        }
+
+        /**
+         * @return `false` if the condition does not satisfy within the time limit.
+         */
+        @JvmStatic
+        fun <T> waitFor(condition: Condition<T>): Boolean {
+            val satisfier = condition.satisfier ?: condition.prepareSatisfier()
+            val startTime = SystemClock.elapsedRealtime()
+            Log.v(LOG_TAG, "***Waiting for ${condition.message}")
+            for (i in 1..condition.retryLimit) {
+                if (satisfier.invoke()) {
+                    Log.v(LOG_TAG, "***Waiting for ${condition.message} ... Success!")
+                    return true
+                } else {
+                    SystemClock.sleep(condition.retryIntervalMs)
+                    Log.v(LOG_TAG, "***Waiting for ${condition.message} ... retry=$i" +
+                        " elapsed=${SystemClock.elapsedRealtime() - startTime} ms")
+                    val onRetry = condition.onRetry
+                    if (onRetry != null && i < condition.retryLimit) {
+                        onRetry.run()
+                    }
+                }
+            }
+            if (satisfier.invoke()) {
+                Log.v(LOG_TAG, "***Waiting for ${condition.message} ... Success!")
+                return true
+            }
+            val onFailure = condition.onFailure
+            if (onFailure == null) {
+                Log.e(LOG_TAG, "***Waiting for ${condition.message} ... Failed!")
+            } else {
+                val result = condition.lastResult
+                require(result != null) { "Missing last result for failure notification" }
+                onFailure.invoke(result)
+            }
+            return false
+        }
+
+        /**
+         * @see .waitForResult
+         */
+        @JvmStatic
+        fun <T> waitForResult(message: String, setup: (Condition<T>) -> Any): T? {
+            val condition = Condition<T>(message)
+            setup.invoke(condition)
+            return waitForResult(condition)
+        }
+
+        /**
+         * @return `null` if the condition does not satisfy within the time limit or the result
+         * supplier returns `null`.
+         */
+        @JvmStatic
+        fun <T> waitForResult(condition: Condition<T>): T? {
+            condition.validatedResult = null
+            condition.lastResult = condition.validatedResult
+            condition.prepareSatisfier()
+            waitFor(condition)
+            return when {
+                condition.validatedResult != null -> condition.validatedResult
+                condition.returnLastResult -> condition.lastResult
+                else -> null
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/parser/DeviceStateDump.kt b/libraries/flicker/src/com/android/server/wm/traces/parser/DeviceStateDump.kt
new file mode 100644
index 0000000..6534b31
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/parser/DeviceStateDump.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2021 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.traces.parser
+
+import com.android.server.wm.traces.common.layers.LayersTrace
+import com.android.server.wm.traces.common.layers.LayerTraceEntry
+import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace
+import com.android.server.wm.traces.common.windowmanager.WindowManagerState
+import com.android.server.wm.traces.parser.layers.LayersTraceParser
+import com.android.server.wm.traces.parser.windowmanager.WindowManagerTraceParser
+
+/**
+ * Represents a state dump containing the [WindowManagerTrace] and the [LayersTrace] both parsed
+ * and in raw (byte) data.
+ */
+class DeviceStateDump(
+    /**
+     * Parsed [WindowManagerTrace]
+     */
+    val wmTrace: WindowManagerTrace?,
+    /**
+     * Parsed [LayersTrace]
+     */
+    val layersTrace: LayersTrace?
+) {
+    companion object {
+        /**
+         * Creates a device state dump containing the [WindowManagerTrace] and [LayersTrace]
+         * obtained from a `dumpsys` command. The parsed traces will contain a single
+         * [WindowManagerState] or [LayerTraceEntry].
+         *
+         * @param wmTraceData [WindowManagerTrace] content
+         * @param layersTraceData [LayersTrace] content
+         */
+        @JvmStatic
+        fun fromDump(wmTraceData: ByteArray, layersTraceData: ByteArray): DeviceStateDump {
+            return DeviceStateDump(
+                wmTrace = if (wmTraceData.isNotEmpty()) {
+                    WindowManagerTraceParser.parseFromDump(wmTraceData)
+                } else {
+                    null
+                },
+                layersTrace = if (layersTraceData.isNotEmpty()) {
+                    LayersTraceParser.parseFromDump(layersTraceData)
+                } else {
+                    null
+                }
+            )
+        }
+
+        /**
+         * Creates a device state dump containing the WindowManager and Layers trace
+         * obtained from a regular trace. The parsed traces may contain a multiple
+         * [WindowManagerState] or [LayerTraceEntry].
+         *
+         * @param wmTraceData [WindowManagerTrace] content
+         * @param layersTraceData [LayersTrace] content
+         */
+        @JvmStatic
+        fun fromTrace(wmTraceData: ByteArray, layersTraceData: ByteArray): DeviceStateDump {
+            return DeviceStateDump(
+                wmTrace = if (wmTraceData.isNotEmpty()) {
+                    WindowManagerTraceParser.parseFromTrace(wmTraceData)
+                } else {
+                    null
+                },
+                layersTrace = if (layersTraceData.isNotEmpty()) {
+                    LayersTraceParser.parseFromTrace(layersTraceData)
+                } else {
+                    null
+                }
+            )
+        }
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/traces/parser/Extensions.kt b/libraries/flicker/src/com/android/server/wm/traces/parser/Extensions.kt
new file mode 100644
index 0000000..bb20b5c
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/parser/Extensions.kt
@@ -0,0 +1,88 @@
+/*
+ * 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.
+ */
+
+@file:JvmName("Extensions")
+
+package com.android.server.wm.traces.parser
+
+import android.app.UiAutomation
+import android.content.ComponentName
+import android.os.ParcelFileDescriptor
+import android.util.Log
+import com.android.server.wm.traces.common.Rect
+import com.android.server.wm.traces.common.Region
+
+internal const val LOG_TAG = "AMWM_FLICKER"
+
+fun Region.toAndroidRegion(): android.graphics.Region {
+    return android.graphics.Region(left, top, right, bottom)
+}
+
+fun Rect.toAndroidRect(): android.graphics.Rect {
+    return android.graphics.Rect(left, top, right, bottom)
+}
+
+fun ComponentName.toActivityName(): String = this.flattenToShortString()
+
+fun ComponentName.toWindowName(): String = this.flattenToString()
+
+private fun executeCommand(uiAutomation: UiAutomation, cmd: String): ByteArray {
+    val fileDescriptor = uiAutomation.executeShellCommand(cmd)
+    ParcelFileDescriptor.AutoCloseInputStream(fileDescriptor).use { inputStream ->
+        return inputStream.readBytes()
+    }
+}
+
+private fun getCurrentWindowManagerState(uiAutomation: UiAutomation) =
+    executeCommand(uiAutomation, "dumpsys window --proto")
+
+private fun getCurrentLayersState(uiAutomation: UiAutomation) =
+    executeCommand(uiAutomation, "dumpsys SurfaceFlinger --proto")
+
+@JvmOverloads
+fun getCurrentState(
+    uiAutomation: UiAutomation,
+    @WmStateDumpFlags dumpFlags: Int = FLAG_STATE_DUMP_FLAG_WM.or(FLAG_STATE_DUMP_FLAG_LAYERS)
+): Pair<ByteArray, ByteArray> {
+    if (dumpFlags == 0) {
+        throw IllegalArgumentException("No dump specified")
+    }
+
+    Log.d(LOG_TAG, "Requesting new device state dump")
+    val wmTraceData = if (dumpFlags.and(FLAG_STATE_DUMP_FLAG_WM) > 0) {
+        getCurrentWindowManagerState(uiAutomation)
+    } else {
+        ByteArray(0)
+    }
+    val layersTraceData = if (dumpFlags.and(FLAG_STATE_DUMP_FLAG_LAYERS) > 0) {
+        getCurrentLayersState(uiAutomation)
+    } else {
+        ByteArray(0)
+    }
+
+    return Pair(wmTraceData, layersTraceData)
+}
+
+@JvmOverloads
+fun getCurrentStateDump(
+    uiAutomation: UiAutomation,
+    @WmStateDumpFlags dumpFlags: Int = FLAG_STATE_DUMP_FLAG_WM.or(FLAG_STATE_DUMP_FLAG_LAYERS)
+): DeviceStateDump {
+    val currentStateDump = getCurrentState(uiAutomation, dumpFlags)
+    val wmTraceData = currentStateDump.first
+    val layersTraceData = currentStateDump.second
+    return DeviceStateDump.fromDump(wmTraceData, layersTraceData)
+}
diff --git a/libraries/flicker/src/com/android/server/wm/traces/parser/WmStateDumpFlags.kt b/libraries/flicker/src/com/android/server/wm/traces/parser/WmStateDumpFlags.kt
new file mode 100644
index 0000000..349ab67
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/parser/WmStateDumpFlags.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.server.wm.traces.parser
+
+import android.support.annotation.IntDef
+
+@IntDef(flag = true, value = [FLAG_STATE_DUMP_FLAG_WM, FLAG_STATE_DUMP_FLAG_LAYERS])
+@Retention(AnnotationRetention.SOURCE)
+annotation class WmStateDumpFlags
+
+/**
+ * Include WM trace in the dump
+ */
+const val FLAG_STATE_DUMP_FLAG_WM = 1
+
+/**
+ * Include the layers trace ni the dump
+ */
+const val FLAG_STATE_DUMP_FLAG_LAYERS = 2
diff --git a/libraries/flicker/src/com/android/server/wm/traces/parser/layers/LayersTraceParser.kt b/libraries/flicker/src/com/android/server/wm/traces/parser/layers/LayersTraceParser.kt
new file mode 100644
index 0000000..5bf48bf
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/parser/layers/LayersTraceParser.kt
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2021 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.traces.parser.layers
+
+import android.graphics.Rect
+import android.surfaceflinger.nano.Layers
+import android.surfaceflinger.nano.Layers.RectProto
+import android.surfaceflinger.nano.Layers.RegionProto
+import android.surfaceflinger.nano.Layerstrace
+import android.util.Log
+import com.android.server.wm.traces.common.Buffer
+import com.android.server.wm.traces.common.Color
+import com.android.server.wm.traces.common.RectF
+import com.android.server.wm.traces.common.Region
+import com.android.server.wm.traces.common.layers.Layer
+import com.android.server.wm.traces.common.layers.LayerTraceEntry
+import com.android.server.wm.traces.common.layers.LayerTraceEntryBuilder
+import com.android.server.wm.traces.common.layers.LayersTrace
+import com.android.server.wm.traces.parser.LOG_TAG
+import com.google.protobuf.nano.InvalidProtocolBufferNanoException
+import java.nio.file.Path
+import kotlin.system.measureTimeMillis
+
+/**
+ * Parser for [LayersTrace] objects containing traces or state dumps
+ **/
+class LayersTraceParser {
+    companion object {
+        /**
+         * Parses [LayersTrace] from [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 sourceChecksum Checksum of the source file
+         * @param orphanLayerCallback a callback to handle any unexpected orphan layers
+         */
+        @JvmOverloads
+        @JvmStatic
+        fun parseFromTrace(
+            data: ByteArray,
+            source: Path? = null,
+            sourceChecksum: String = "",
+            orphanLayerCallback: ((Layer) -> Boolean)? = null
+        ): LayersTrace {
+            val fileProto: Layerstrace.LayersTraceFileProto
+            try {
+                measureTimeMillis {
+                    fileProto = Layerstrace.LayersTraceFileProto.parseFrom(data)
+                }.also {
+                    Log.v(LOG_TAG, "Parsing proto (Layers Trace): ${it}ms")
+                }
+            } catch (e: Exception) {
+                throw RuntimeException(e)
+            }
+            return parseFromTrace(fileProto, source, sourceChecksum, orphanLayerCallback)
+        }
+
+        /**
+         * Parses [LayersTrace] from [proto] and uses the proto to generates a list
+         * of trace entries, storing the flattened layers into its hierarchical structure.
+         *
+         * @param proto Parsed proto data
+         * @param source Path to source of data for additional debug information
+         * @param sourceChecksum Checksum of the source file
+         * @param orphanLayerCallback a callback to handle any unexpected orphan layers
+         */
+        @JvmOverloads
+        @JvmStatic
+        fun parseFromTrace(
+            proto: Layerstrace.LayersTraceFileProto,
+            source: Path? = null,
+            sourceChecksum: String = "",
+            orphanLayerCallback: ((Layer) -> Boolean)? = null
+        ): LayersTrace {
+            val entries: MutableList<LayerTraceEntry> = ArrayList()
+            var traceParseTime = 0L
+            for (traceProto: Layerstrace.LayersTraceProto in proto.entry) {
+                val entryParseTime = measureTimeMillis {
+                    val entry = newEntry(
+                        traceProto.elapsedRealtimeNanos, traceProto.layers.layers,
+                        traceProto.hwcBlob, traceProto.where, orphanLayerCallback)
+                    entries.add(entry)
+                }
+                traceParseTime += entryParseTime
+            }
+            Log.v(LOG_TAG, "Parsing duration (Layers Trace): ${traceParseTime}ms " +
+                "(avg ${traceParseTime / entries.size}ms per entry)")
+            return LayersTrace(entries, source?.toString() ?: "", sourceChecksum)
+        }
+
+        /**
+         * Parses [LayersTrace] from [proto] and uses the proto to generates
+         * a list of trace entries.
+         *
+         * @param proto Parsed proto data
+         */
+        @JvmStatic
+        fun parseFromDump(proto: Layers.LayersProto): LayersTrace {
+            val entry = newEntry(timestamp = 0, protos = proto.layers)
+            return LayersTrace(entry)
+        }
+
+        /**
+         * Parses [LayersTrace] from [data] and uses the proto to generates
+         * a list of trace entries.
+         *
+         * @param data binary proto data
+         */
+        @JvmStatic
+        fun parseFromDump(data: ByteArray?): LayersTrace {
+            val traceProto = try {
+                Layers.LayersProto.parseFrom(data)
+            } catch (e: InvalidProtocolBufferNanoException) {
+                throw RuntimeException(e)
+            }
+            return parseFromDump(traceProto)
+        }
+
+        @JvmStatic
+        private fun newEntry(
+            timestamp: Long,
+            protos: Array<Layers.LayerProto>,
+            hwcBlob: String = "",
+            where: String = "",
+            orphanLayerCallback: ((Layer) -> Boolean)? = null
+        ): LayerTraceEntry {
+            val layers = protos.map { newLayer(it) }
+            val builder = LayerTraceEntryBuilder(timestamp, layers, hwcBlob, where)
+            builder.setOrphanLayerCallback(orphanLayerCallback)
+            return builder.build()
+        }
+
+        @JvmStatic
+        private fun newLayer(proto: Layers.LayerProto): Layer {
+            // Differentiate between the cases when there's no HWC data on
+            // the trace, and when the visible region is actually empty
+            val activeBuffer = proto.activeBuffer.toBuffer()
+            var visibleRegion = proto.visibleRegion.toRegion()
+            if (visibleRegion == null && activeBuffer.isEmpty) {
+                visibleRegion = Region.EMPTY
+            }
+            val crop = getCrop(proto.crop)
+            return Layer(
+                    proto.name ?: "",
+                    proto.id,
+                    proto.parent,
+                    proto.z,
+                    visibleRegion,
+                    activeBuffer,
+                    proto.flags,
+                    proto.bounds?.toRectF() ?: RectF.EMPTY,
+                    proto.color.toColor(),
+                    proto.isOpaque,
+                    proto.shadowRadius,
+                    proto.cornerRadius,
+                    proto.type ?: "",
+                    proto.screenBounds?.toRectF(),
+                    Transform(proto.transform, proto.position),
+                    proto.sourceBounds?.toRectF(),
+                    proto.currFrame,
+                    proto.effectiveScalingMode,
+                    Transform(proto.bufferTransform, position = null),
+                    proto.hwcCompositionType,
+                    proto.hwcCrop.toRectF() ?: RectF.EMPTY,
+                    proto.hwcFrame.toRect(),
+                    proto.backgroundBlurRadius,
+                    crop,
+                    proto.isRelativeOf,
+                    proto.zOrderRelativeOf
+            )
+        }
+
+        @JvmStatic
+        private fun Layers.FloatRectProto?.toRectF(): RectF? {
+            return this?.let {
+                RectF(left = left, top = top, right = right, bottom = bottom)
+            }
+        }
+
+        @JvmStatic
+        private fun Layers.ColorProto?.toColor(): Color {
+            if (this == null) {
+                return Color.EMPTY
+            }
+            return Color(r, g, b, a)
+        }
+
+        @JvmStatic
+        private fun Layers.ActiveBufferProto?.toBuffer(): Buffer {
+            if (this == null) {
+                return Buffer.EMPTY
+            }
+            return Buffer(width, height, stride, format)
+        }
+
+        @JvmStatic
+        private fun getCrop(crop: RectProto?): com.android.server.wm.traces.common.Rect? {
+            return when {
+                crop == null -> com.android.server.wm.traces.common.Rect.EMPTY
+                // crop (0,0) (-1,-1) means no crop
+                crop.right == -1 && crop.left == 0 && crop.bottom == -1 && crop.top == 0 ->
+                    null
+                (crop.right - crop.left) <= 0 || (crop.bottom - crop.top) <= 0 ->
+                    com.android.server.wm.traces.common.Rect.EMPTY
+                else ->
+                    com.android.server.wm.traces.common.Rect(
+                        crop.left, crop.top, crop.right, crop.bottom)
+            }
+        }
+
+        /**
+         * Extracts [Rect] from [RegionProto] by returning a rect that encompasses all
+         * the rectangles making up the region.
+         */
+        @JvmStatic
+        private fun RegionProto?.toRegion(): Region? {
+            return if (this == null) {
+                null
+            } else {
+                val rects = this.rect.map { it.toRect() }.toTypedArray()
+                return Region(rects)
+            }
+        }
+
+        @JvmStatic
+        private fun RectProto?.toRect(): com.android.server.wm.traces.common.Rect {
+            return if ((this == null) ||
+                ((this.right - this.left) <= 0 || (this.bottom - this.top) <= 0)) {
+                com.android.server.wm.traces.common.Rect.EMPTY
+            } else {
+                com.android.server.wm.traces.common.Rect(
+                    this.left, this.top, this.right, this.bottom)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/parser/layers/Transform.kt b/libraries/flicker/src/com/android/server/wm/traces/parser/layers/Transform.kt
new file mode 100644
index 0000000..ca1b28f
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/parser/layers/Transform.kt
@@ -0,0 +1,68 @@
+/*
+ * 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.server.wm.traces.parser.layers
+
+import android.surfaceflinger.nano.Layers
+import com.android.server.wm.traces.common.layers.Transform
+import com.android.server.wm.traces.common.layers.Transform.Companion.FLIP_H_VAL
+import com.android.server.wm.traces.common.layers.Transform.Companion.FLIP_V_VAL
+import com.android.server.wm.traces.common.layers.Transform.Companion.ROTATE_VAL
+import com.android.server.wm.traces.common.layers.Transform.Companion.ROT_90_VAL
+import com.android.server.wm.traces.common.layers.Transform.Companion.SCALE_VAL
+import com.android.server.wm.traces.common.layers.Transform.Companion.isFlagClear
+import com.android.server.wm.traces.common.layers.Transform.Companion.isFlagSet
+
+class Transform(transform: Layers.TransformProto?, position: Layers.PositionProto?) :
+        Transform(
+            transform?.type,
+            getMatrix(transform, position)
+        )
+
+private fun getMatrix(transform: Layers.TransformProto?, position: Layers.PositionProto?):
+        Transform.Matrix {
+    val x = position?.x ?: 0f
+    val y = position?.y ?: 0f
+
+    return when {
+        transform == null || Transform.isSimpleTransform(transform.type) ->
+            transform?.type.getDefaultTransform(x, y)
+        else ->
+            Transform.Matrix(transform.dsdx, transform.dtdx, x, transform.dsdy, transform.dtdy, y)
+    }
+}
+
+private fun Int?.getDefaultTransform(x: Float, y: Float): Transform.Matrix {
+    return when {
+        // IDENTITY
+        this == null ->
+            Transform.Matrix(1f, 0f, x, 0f, 1f, y)
+        // // ROT_270 = ROT_90|FLIP_H|FLIP_V
+        isFlagSet(ROT_90_VAL or FLIP_V_VAL or FLIP_H_VAL) ->
+            Transform.Matrix(0f, -1f, x, 1f, 0f, y)
+        // ROT_180 = FLIP_H|FLIP_V
+        isFlagSet(FLIP_V_VAL or FLIP_H_VAL) ->
+            Transform.Matrix(-1f, 0f, x, 0f, -1f, y)
+        // ROT_90
+        isFlagSet(ROT_90_VAL) ->
+            Transform.Matrix(0f, 1f, x, -1f, 0f, y)
+        // IDENTITY
+        isFlagClear(SCALE_VAL or ROTATE_VAL) ->
+            Transform.Matrix(1f, 0f, x, 0f, 1f, y)
+        else ->
+            throw IllegalStateException("Unknown transform type $this")
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/parser/windowmanager/WaitForValidActivityState.kt b/libraries/flicker/src/com/android/server/wm/traces/parser/windowmanager/WaitForValidActivityState.kt
new file mode 100644
index 0000000..1a0b23c
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/parser/windowmanager/WaitForValidActivityState.kt
@@ -0,0 +1,134 @@
+/*
+ * 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.server.wm.traces.parser.windowmanager
+
+import android.app.ActivityTaskManager
+import android.app.WindowConfiguration
+import android.content.ComponentName
+import com.android.server.wm.traces.parser.toActivityName
+import com.android.server.wm.traces.parser.toWindowName
+
+data class WaitForValidActivityState(
+    @JvmField
+    val activityName: ComponentName?,
+    @JvmField
+    val windowName: String?,
+    @JvmField
+    val stackId: Int,
+    @JvmField
+    val windowingMode: Int,
+    @JvmField
+    val activityType: Int
+) {
+    constructor(activityName: ComponentName) : this(
+        activityName,
+        windowName = activityName.toWindowName(),
+        stackId = ActivityTaskManager.INVALID_STACK_ID,
+        windowingMode = WindowConfiguration.WINDOWING_MODE_UNDEFINED,
+        activityType = WindowConfiguration.ACTIVITY_TYPE_UNDEFINED
+    )
+
+    private constructor(builder: Builder) : this(
+        activityName = builder.activityName,
+        windowName = builder.windowName,
+        stackId = builder.stackId,
+        windowingMode = builder.windowingMode,
+        activityType = builder.activityType
+    )
+
+    override fun toString(): String {
+        val sb = StringBuilder("wait:")
+        if (activityName != null) {
+            sb.append(" activity=").append(activityName.toActivityName())
+        }
+        if (activityType != WindowConfiguration.ACTIVITY_TYPE_UNDEFINED) {
+            sb.append(" type=").append(activityTypeName(activityType))
+        }
+        if (windowName != null) {
+            sb.append(" window=").append(windowName)
+        }
+        if (windowingMode != WindowConfiguration.WINDOWING_MODE_UNDEFINED) {
+            sb.append(" mode=").append(windowingModeName(windowingMode))
+        }
+        if (stackId != ActivityTaskManager.INVALID_STACK_ID) {
+            sb.append(" stack=").append(stackId)
+        }
+        return sb.toString()
+    }
+
+    class Builder constructor(internal var activityName: ComponentName? = null) {
+        internal var windowName: String? = activityName?.toWindowName()
+        internal var stackId = ActivityTaskManager.INVALID_STACK_ID
+        internal var windowingMode = WindowConfiguration.WINDOWING_MODE_UNDEFINED
+        internal var activityType = WindowConfiguration.ACTIVITY_TYPE_UNDEFINED
+
+        fun setWindowName(windowName: String): Builder {
+            this.windowName = windowName
+            return this
+        }
+
+        fun setStackId(stackId: Int): Builder {
+            this.stackId = stackId
+            return this
+        }
+
+        fun setWindowingMode(windowingMode: Int): Builder {
+            this.windowingMode = windowingMode
+            return this
+        }
+
+        fun setActivityType(activityType: Int): Builder {
+            this.activityType = activityType
+            return this
+        }
+
+        fun build(): WaitForValidActivityState {
+            return WaitForValidActivityState(this)
+        }
+    }
+
+    companion object {
+        @JvmStatic
+        fun forWindow(windowName: String): WaitForValidActivityState {
+            return Builder().setWindowName(windowName).build()
+        }
+
+        private fun windowingModeName(windowingMode: Int): String {
+            return when (windowingMode) {
+                WindowConfiguration.WINDOWING_MODE_UNDEFINED -> "UNDEFINED"
+                WindowConfiguration.WINDOWING_MODE_FULLSCREEN -> "FULLSCREEN"
+                WindowConfiguration.WINDOWING_MODE_PINNED -> "PINNED"
+                WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY -> "SPLIT_SCREEN_PRIMARY"
+                WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY ->
+                    "SPLIT_SCREEN_SECONDARY"
+                WindowConfiguration.WINDOWING_MODE_FREEFORM -> "FREEFORM"
+                else -> throw IllegalArgumentException("Unknown WINDOWING_MODE_: $windowingMode")
+            }
+        }
+
+        private fun activityTypeName(activityType: Int): String {
+            return when (activityType) {
+                WindowConfiguration.ACTIVITY_TYPE_UNDEFINED -> "UNDEFINED"
+                WindowConfiguration.ACTIVITY_TYPE_STANDARD -> "STANDARD"
+                WindowConfiguration.ACTIVITY_TYPE_HOME -> "HOME"
+                WindowConfiguration.ACTIVITY_TYPE_RECENTS -> "RECENTS"
+                WindowConfiguration.ACTIVITY_TYPE_ASSISTANT -> "ASSISTANT"
+                else -> throw IllegalArgumentException("Unknown ACTIVITY_TYPE_: $activityType")
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/parser/windowmanager/WindowManagerStateHelper.kt b/libraries/flicker/src/com/android/server/wm/traces/parser/windowmanager/WindowManagerStateHelper.kt
new file mode 100644
index 0000000..3d4b083
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/parser/windowmanager/WindowManagerStateHelper.kt
@@ -0,0 +1,503 @@
+/*
+ * 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.server.wm.traces.parser.windowmanager
+
+import android.app.ActivityTaskManager
+import android.app.Instrumentation
+import android.app.WindowConfiguration
+import android.content.ComponentName
+import android.graphics.Rect
+import android.graphics.Region
+import android.os.SystemClock
+import android.util.Log
+import android.view.Display
+import androidx.annotation.VisibleForTesting
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.server.wm.traces.common.layers.LayerTraceEntry
+import com.android.server.wm.traces.common.windowmanager.WindowManagerState
+import com.android.server.wm.traces.parser.getCurrentStateDump
+import com.android.server.wm.traces.common.windowmanager.windows.ConfigurationContainer
+import com.android.server.wm.traces.common.windowmanager.windows.WindowContainer
+import com.android.server.wm.traces.common.windowmanager.windows.WindowState
+import com.android.server.wm.traces.parser.Condition
+import com.android.server.wm.traces.parser.LOG_TAG
+import com.android.server.wm.traces.parser.toActivityName
+import com.android.server.wm.traces.parser.toAndroidRegion
+import com.android.server.wm.traces.parser.toWindowName
+
+open class WindowManagerStateHelper @JvmOverloads constructor(
+    /**
+     * Instrumentation to run the tests
+     */
+    private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation(),
+    /**
+     * Predicate to supply a new UI information
+     */
+    private val deviceDumpSupplier: () -> Dump = {
+        val currState = getCurrentStateDump(
+            instrumentation.uiAutomation)
+        Dump(
+            currState.wmTrace?.entries?.first() ?: error("Unable to parse WM trace"),
+            currState.layersTrace?.entries?.first() ?: error("Unable to parse Layers trace")
+        )
+    },
+    /**
+     * Number of attempts to satisfy a wait condition
+     */
+    private val numRetries: Int = 5,
+    /**
+     * Interval between wait for state dumps during wait conditions
+     */
+    private val retryIntervalMs: Long = 500L
+) {
+    /**
+     * Fetches the current device state
+     */
+    val currentState: Dump
+        get() = computeState(ignoreInvalidStates = true)
+
+    /**
+     * Queries the supplier for a new device state
+     *
+     * @param ignoreInvalidStates If false, retries up to [numRetries] times (with a sleep
+     * interval of [retryIntervalMs] ms to obtain a complete WM state, otherwise returns the
+     * first state
+     */
+    protected open fun computeState(ignoreInvalidStates: Boolean = false): Dump {
+        var newState = deviceDumpSupplier.invoke()
+        for (retryNr in 0..numRetries) {
+            val wmState = newState.wmState
+            if (!ignoreInvalidStates && wmState.isIncomplete()) {
+                Log.w(LOG_TAG, "***Incomplete AM state: ${wmState.getIsIncompleteReason()}" +
+                    " Waiting ${retryIntervalMs}ms and retrying ($retryNr/$numRetries)...")
+                SystemClock.sleep(retryIntervalMs)
+                newState = deviceDumpSupplier.invoke()
+            } else {
+                break
+            }
+        }
+
+        return newState
+    }
+
+    private fun ConfigurationContainer.isWindowingModeCompatible(
+        requestedWindowingMode: Int
+    ): Boolean {
+        return when (requestedWindowingMode) {
+            WindowConfiguration.WINDOWING_MODE_UNDEFINED -> true
+            WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY ->
+                (windowingMode == WindowConfiguration.WINDOWING_MODE_FULLSCREEN ||
+                    windowingMode == WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY)
+            else -> windowingMode == requestedWindowingMode
+        }
+    }
+
+    /**
+     * Wait for the activities to appear in proper stacks and for valid state in AM and WM.
+     * @param waitForActivitiesVisible array of activity states to wait for.
+     */
+    fun waitForValidState(vararg waitForActivitiesVisible: WaitForValidActivityState): Boolean {
+        val success = Condition.waitFor<WindowManagerState>("valid stacks and activities states",
+            retryLimit = numRetries, retryIntervalMs = retryIntervalMs) {
+            // TODO: Get state of AM and WM at the same time to avoid mismatches caused by
+            // requesting dump in some intermediate state.
+            val state = computeState()
+            !(shouldWaitForValidityCheck(state) ||
+                shouldWaitForValidStacks(state) ||
+                shouldWaitForActivities(state, *waitForActivitiesVisible) ||
+                shouldWaitForWindows(state))
+        }
+        if (!success) {
+            Log.e(LOG_TAG, "***Waiting for states failed: " +
+                waitForActivitiesVisible.contentToString())
+        }
+        return success
+    }
+
+    fun waitForFullScreenApp(componentName: ComponentName): Boolean =
+            waitForValidState(
+                    WaitForValidActivityState
+                            .Builder(componentName)
+                            .setWindowingMode(WindowConfiguration.WINDOWING_MODE_FULLSCREEN)
+                            .setActivityType(WindowConfiguration.ACTIVITY_TYPE_STANDARD)
+                            .build())
+
+    fun waitForHomeActivityVisible(): Boolean {
+        return waitFor { it.wmState.homeActivity?.isVisible == true } &&
+            waitForNavBarStatusBarVisible() &&
+            waitForAppTransitionIdle()
+    }
+
+    fun waitForRecentsActivityVisible(): Boolean =
+        waitFor("recents activity to be visible") {
+            it.wmState.isRecentsActivityVisible
+        }
+
+    fun waitForAodShowing(): Boolean =
+        waitFor("AOD showing") {
+            it.wmState.keyguardControllerState.isAodShowing
+        }
+
+    fun waitForKeyguardGone(): Boolean =
+        waitFor("Keyguard gone") {
+            !it.wmState.keyguardControllerState.isKeyguardShowing
+        }
+
+    /**
+     * Wait for specific rotation for the default display. Values are Surface#Rotation
+     */
+    @JvmOverloads
+    fun waitForRotation(rotation: Int, displayId: Int = Display.DEFAULT_DISPLAY): Boolean =
+        waitFor("Rotation: $rotation") {
+            val currRotation = it.wmState.getRotation(displayId)
+            val rotationLayerExists = it.layerState.isVisible(ROTATION_LAYER_NAME)
+            val blackSurfaceLayerExists = it.layerState.isVisible(BLACK_SURFACE_LAYER_NAME)
+            val anyLayerAnimating = it.layerState.visibleLayers.any { layer ->
+                !layer.transform.isSimpleRotation
+            }
+            Log.v(LOG_TAG, "currRotation($currRotation) " +
+                "anyLayerAnimating($anyLayerAnimating) " +
+                "blackSurfaceLayerExists($blackSurfaceLayerExists) " +
+                "rotationLayerExists($rotationLayerExists)")
+            currRotation == rotation &&
+                !anyLayerAnimating &&
+                !rotationLayerExists &&
+                !blackSurfaceLayerExists
+        }
+
+    /**
+     * Wait for specific orientation for the default display.
+     * Values are ActivityInfo.ScreenOrientation
+     */
+    @JvmOverloads
+    fun waitForLastOrientation(
+        orientation: Int,
+        displayId: Int = Display.DEFAULT_DISPLAY
+    ): Boolean =
+        waitFor("LastOrientation: $orientation") {
+            val result = it.wmState.getOrientation(displayId)
+            Log.v(LOG_TAG, "Current: $result Expected: $orientation")
+            result == orientation
+        }
+
+    fun waitForActivityState(activity: ComponentName, activityState: String): Boolean {
+        val activityName = activity.toActivityName()
+        return waitFor("state of $activityName to be $activityState") {
+            it.wmState.hasActivityState(activityName, activityState)
+        }
+    }
+
+    /**
+     * Waits until the navigation and status bars are visible (windows and layers)
+     */
+    fun waitForNavBarStatusBarVisible(): Boolean =
+        waitFor("Navigation and Status bar to be visible") {
+            val navBarWindowVisible = it.wmState.isWindowVisible(NAV_BAR_WINDOW_NAME)
+            val statusBarWindowVisible = it.wmState.isWindowVisible(STATUS_BAR_WINDOW_NAME)
+            val navBarLayerVisible = it.layerState.isVisible(NAV_BAR_LAYER_NAME)
+            val navBarLayerAlpha = it.layerState.getLayerWithBuffer(NAV_BAR_LAYER_NAME)
+                ?.color?.a ?: 0f
+            val statusBarLayerVisible = it.layerState.isVisible(STATUS_BAR_LAYER_NAME)
+            val statusBarLayerAlpha = it.layerState.getLayerWithBuffer(STATUS_BAR_LAYER_NAME)
+                ?.color?.a ?: 0f
+            val result = navBarWindowVisible &&
+                navBarLayerVisible &&
+                statusBarWindowVisible &&
+                statusBarLayerVisible &&
+                navBarLayerAlpha == 1f &&
+                statusBarLayerAlpha == 1f
+
+            Log.v(LOG_TAG, "Current $result " +
+                "navBarWindowVisible($navBarWindowVisible) " +
+                "navBarLayerVisible($navBarLayerVisible) " +
+                "statusBarWindowVisible($statusBarWindowVisible) " +
+                "statusBarLayerVisible($statusBarLayerVisible) " +
+                "navBarLayerAlpha($navBarLayerAlpha) " +
+                "statusBarLayerAlpha($statusBarLayerAlpha)")
+
+            result
+        }
+
+    fun waitForVisibleWindow(activity: ComponentName): Boolean {
+        val activityName = activity.toActivityName()
+        val windowName = activity.toWindowName()
+        return waitFor("$activityName to exist") {
+            val containsActivity = it.wmState.containsActivity(activityName)
+            val containsWindow = it.wmState.containsWindow(windowName)
+            val activityVisible = containsActivity && it.wmState.isActivityVisible(activityName)
+            val windowVisible = containsWindow && it.wmState.isWindowSurfaceShown(windowName)
+            val result = containsActivity &&
+                containsWindow &&
+                activityVisible &&
+                windowVisible
+
+            Log.v(LOG_TAG, "Current: $result " +
+                "containsActivity($containsActivity) " +
+                "containsWindow($containsWindow) " +
+                "activityVisible($activityVisible) " +
+                "windowVisible($windowVisible)")
+
+            result
+        }
+    }
+
+    fun waitForActivityRemoved(activity: ComponentName): Boolean {
+        val activityName = activity.toActivityName()
+        val windowName = activity.toWindowName()
+        return waitFor("$activityName to be removed") {
+            val containsActivity = it.wmState.containsActivity(activityName)
+            val containsWindow = it.wmState.containsWindow(windowName)
+            val result = !containsActivity && !containsWindow
+
+            Log.v(LOG_TAG, "Current: $result" +
+                "containsActivity($containsActivity)" +
+                "containsWindow($containsWindow)")
+            result
+        }
+    }
+
+    fun waitForPendingActivityContain(activity: ComponentName): Boolean {
+        val activityName: String = activity.toActivityName()
+        return waitFor("$activityName in pending list") {
+            it.wmState.pendingActivityContain(activityName)
+        }
+    }
+
+    @JvmOverloads
+    fun waitForAppTransitionIdle(displayId: Int = Display.DEFAULT_DISPLAY): Boolean =
+        waitFor("app transition idle on Display $displayId") {
+            val result =
+                it.wmState.getDisplay(displayId)?.appTransitionState
+            Log.v(LOG_TAG, "Current: $result")
+            WindowManagerState.APP_STATE_IDLE == result
+        }
+
+    fun waitForWindowSurfaceDisappeared(componentName: ComponentName): Boolean {
+        val windowName = componentName.toWindowName()
+        return waitFor("$windowName's surface is disappeared") {
+            !it.wmState.isWindowSurfaceShown(windowName)
+        }
+    }
+
+    fun waitForSurfaceAppeared(surfaceName: String): Boolean {
+        return waitFor("$surfaceName surface is appeared") {
+            it.wmState.isWindowSurfaceShown(surfaceName)
+        }
+    }
+
+    fun waitWindowingModeTopFocus(
+        windowingMode: Int,
+        topFocus: Boolean,
+        message: String
+    ): Boolean = waitFor(message) {
+        val stack = it.wmState.getStandardStackByWindowingMode(windowingMode)
+        (stack != null && topFocus == (it.wmState.focusedStackId == stack.rootTaskId))
+    }
+
+    @JvmOverloads
+    fun waitFor(
+        message: String = "",
+        waitCondition: (Dump) -> Boolean
+    ): Boolean = Condition.waitFor<Dump>(message, retryLimit = numRetries,
+        retryIntervalMs = retryIntervalMs) {
+        val state = computeState()
+        waitCondition.invoke(state)
+    }
+
+    /**
+     * @return true if should wait for valid stacks state.
+     */
+    private fun shouldWaitForValidStacks(state: Dump): Boolean {
+        if (state.wmState.stackCount == 0) {
+            Log.i(LOG_TAG, "***stackCount=0")
+            return true
+        }
+        if (!state.wmState.keyguardControllerState.isKeyguardShowing &&
+            state.wmState.resumedActivities.isEmpty()) {
+            if (!state.wmState.keyguardControllerState.isKeyguardShowing) {
+                Log.i(LOG_TAG, "***resumedActivitiesCount=0")
+            } else {
+                Log.i(LOG_TAG, "***isKeyguardShowing=true")
+            }
+            return true
+        }
+        if (state.wmState.focusedActivity.isEmpty()) {
+            Log.i(LOG_TAG, "***focusedActivity=null")
+            return true
+        }
+        return false
+    }
+
+    /**
+     * @return true if should wait for some activities to become visible.
+     */
+    private fun shouldWaitForActivities(
+        state: Dump,
+        vararg waitForActivitiesVisible: WaitForValidActivityState
+    ): Boolean {
+        if (waitForActivitiesVisible.isEmpty()) {
+            return false
+        }
+        // If the caller is interested in waiting for some particular activity windows to be
+        // visible before compute the state. Check for the visibility of those activity windows
+        // and for placing them in correct stacks (if requested).
+        var allActivityWindowsVisible = true
+        var tasksInCorrectStacks = true
+        for (activityState in waitForActivitiesVisible) {
+            val matchingWindowStates = state.wmState.getMatchingVisibleWindowState(
+                activityState.windowName ?: "")
+            val activityWindowVisible = matchingWindowStates.isNotEmpty()
+
+            if (!activityWindowVisible) {
+                Log.i(LOG_TAG, "Activity window not visible: ${activityState.windowName}")
+                allActivityWindowsVisible = false
+            } else if (activityState.activityName != null &&
+                !state.wmState.isActivityVisible(activityState.activityName.toActivityName())) {
+                Log.i(LOG_TAG, "Activity not visible: ${activityState.activityName}")
+                allActivityWindowsVisible = false
+            } else {
+                // Check if window is already the correct state requested by test.
+                var windowInCorrectState = false
+                for (ws in matchingWindowStates) {
+                    if (activityState.stackId != ActivityTaskManager.INVALID_STACK_ID &&
+                        ws.stackId != activityState.stackId) {
+                        continue
+                    }
+                    if (!ws.isWindowingModeCompatible(activityState.windowingMode)) {
+                        continue
+                    }
+                    if (activityState.activityType != WindowConfiguration.ACTIVITY_TYPE_UNDEFINED &&
+                        ws.activityType != activityState.activityType) {
+                        continue
+                    }
+                    windowInCorrectState = true
+                    break
+                }
+                if (!windowInCorrectState) {
+                    Log.i(LOG_TAG, "Window in incorrect stack: $activityState")
+                    tasksInCorrectStacks = false
+                }
+            }
+        }
+        return !allActivityWindowsVisible || !tasksInCorrectStacks
+    }
+
+    /**
+     * @return true if should wait for the valid windows state.
+     */
+    private fun shouldWaitForWindows(state: Dump): Boolean {
+        return when {
+            state.wmState.frontWindow == null -> {
+                Log.i(LOG_TAG, "***frontWindow=null")
+                true
+            }
+            state.wmState.focusedWindow.isEmpty() -> {
+                Log.i(LOG_TAG, "***focusedWindow=null")
+                true
+            }
+            state.wmState.focusedApp.isEmpty() -> {
+                Log.i(LOG_TAG, "***focusedApp=null")
+                true
+            }
+            else -> false
+        }
+    }
+
+    private fun shouldWaitForValidityCheck(state: Dump): Boolean {
+        return !state.wmState.isComplete()
+    }
+
+    /**
+     * Waits until the IME window and layer are visible
+     */
+    @JvmOverloads
+    fun waitImeWindowShown(displayId: Int = Display.DEFAULT_DISPLAY): Boolean =
+        waitFor("IME window shown") {
+            val imeSurfaceShown = it.wmState.inputMethodWindowState?.isSurfaceShown == true
+            val imeDisplay = it.wmState.inputMethodWindowState?.displayId
+            val imeLayerShown = it.layerState.isVisible(IME_LAYER_NAME)
+            val result = imeSurfaceShown && imeLayerShown && imeDisplay == displayId
+
+            Log.v(LOG_TAG, "Current: $result " +
+                "imeSurfaceShown($imeSurfaceShown) " +
+                "imeLayerShown($imeLayerShown) " +
+                "imeDisplay($imeDisplay)")
+            result
+        }
+
+    /**
+     * Waits until the IME layer is no longer visible. Cannot wait for the window as
+     * its visibility information is updated at a later state and is not reliable in
+     * the trace
+     */
+    fun waitImeWindowGone(): Boolean =
+        waitFor("IME window gone") {
+            val imeLayerShown = it.layerState.isVisible(IME_LAYER_NAME)
+            Log.v(LOG_TAG, "imeLayerShown($imeLayerShown)")
+            !imeLayerShown
+        }
+
+    /**
+     * Obtains a [WindowContainer] from the current device state, or null if the WindowContainer
+     * doesn't exist
+     */
+    fun getWindow(activity: ComponentName): WindowState? {
+        val windowName = activity.toWindowName()
+        return this.currentState.wmState.windowStates
+            .firstOrNull { it.title == windowName }
+    }
+
+    /**
+     * Obtains the region of a window in the state, or an empty [Rect] is there are none
+     */
+    fun getWindowRegion(activity: ComponentName): Region {
+        val window = getWindow(activity)
+        return window?.frameRegion?.toAndroidRegion() ?: Region()
+    }
+
+    companion object {
+        @VisibleForTesting
+        const val NAV_BAR_WINDOW_NAME = "NavigationBar0"
+        @VisibleForTesting
+        const val STATUS_BAR_WINDOW_NAME = "StatusBar"
+        @VisibleForTesting
+        const val NAV_BAR_LAYER_NAME = "$NAV_BAR_WINDOW_NAME#0"
+        @VisibleForTesting
+        const val STATUS_BAR_LAYER_NAME = "$STATUS_BAR_WINDOW_NAME#0"
+        @VisibleForTesting
+        const val ROTATION_LAYER_NAME = "RotationLayer#0"
+        @VisibleForTesting
+        const val BLACK_SURFACE_LAYER_NAME = "BackColorSurface#0"
+        @VisibleForTesting
+        const val IME_LAYER_NAME = "InputMethod#0"
+        @VisibleForTesting
+        const val SPLASH_SCREEN_NAME = "Splash Screen"
+        @VisibleForTesting
+        const val SNAPSHOT_WINDOW_NAME = "SnapshotStartingWindow"
+    }
+
+    data class Dump(
+        /**
+         * Window manager state
+         */
+        @JvmField val wmState: WindowManagerState,
+        /**
+         * Layers state
+         */
+        @JvmField val layerState: LayerTraceEntry
+    )
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/traces/parser/windowmanager/WindowManagerTraceParser.kt b/libraries/flicker/src/com/android/server/wm/traces/parser/windowmanager/WindowManagerTraceParser.kt
new file mode 100644
index 0000000..718b0b3
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/traces/parser/windowmanager/WindowManagerTraceParser.kt
@@ -0,0 +1,554 @@
+/*
+ * 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.server.wm.traces.parser.windowmanager
+
+import android.app.nano.WindowConfigurationProto
+import android.content.nano.ConfigurationProto
+import android.graphics.nano.RectProto
+import android.util.Log
+import android.view.nano.ViewProtoEnums
+import android.view.nano.WindowLayoutParamsProto
+import com.android.server.wm.traces.common.windowmanager.windows.Configuration
+import com.android.server.wm.traces.common.Rect
+import com.android.server.wm.traces.common.windowmanager.windows.WindowConfiguration
+import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace
+import com.android.server.wm.traces.common.windowmanager.WindowManagerState
+import com.android.server.wm.traces.common.windowmanager.windows.Activity
+import com.android.server.wm.traces.common.windowmanager.windows.ActivityTask
+import com.android.server.wm.traces.common.windowmanager.windows.ConfigurationContainer
+import com.android.server.wm.traces.common.windowmanager.windows.DisplayArea
+import com.android.server.wm.traces.common.windowmanager.windows.DisplayContent
+import com.android.server.wm.traces.common.windowmanager.windows.KeyguardControllerState
+import com.android.server.wm.traces.common.windowmanager.windows.RootWindowContainer
+import com.android.server.wm.traces.common.windowmanager.windows.WindowContainer
+import com.android.server.wm.traces.common.windowmanager.windows.WindowManagerPolicy
+import com.android.server.wm.traces.common.windowmanager.windows.WindowState
+import com.android.server.wm.traces.common.windowmanager.windows.WindowToken
+import com.android.server.wm.nano.ActivityRecordProto
+import com.android.server.wm.nano.AppTransitionProto
+import com.android.server.wm.nano.ConfigurationContainerProto
+import com.android.server.wm.nano.DisplayAreaProto
+import com.android.server.wm.nano.DisplayContentProto
+import com.android.server.wm.nano.KeyguardControllerProto
+import com.android.server.wm.nano.WindowManagerPolicyProto
+import com.android.server.wm.nano.RootWindowContainerProto
+import com.android.server.wm.nano.TaskProto
+import com.android.server.wm.nano.WindowContainerChildProto
+import com.android.server.wm.nano.WindowContainerProto
+import com.android.server.wm.nano.WindowManagerServiceDumpProto
+import com.android.server.wm.nano.WindowManagerTraceFileProto
+import com.android.server.wm.nano.WindowStateProto
+import com.android.server.wm.nano.WindowTokenProto
+import com.android.server.wm.traces.common.Bounds
+import com.android.server.wm.traces.common.windowmanager.windows.WindowLayoutParams
+import com.android.server.wm.traces.parser.LOG_TAG
+import com.google.protobuf.nano.InvalidProtocolBufferNanoException
+import java.nio.file.Path
+import kotlin.system.measureTimeMillis
+
+object WindowManagerTraceParser {
+    private const val TRANSIT_ACTIVITY_OPEN = "TRANSIT_ACTIVITY_OPEN"
+    private const val TRANSIT_ACTIVITY_CLOSE = "TRANSIT_ACTIVITY_CLOSE"
+    private const val TRANSIT_TASK_OPEN = "TRANSIT_TASK_OPEN"
+    private const val TRANSIT_TASK_CLOSE = "TRANSIT_TASK_CLOSE"
+    private const val TRANSIT_WALLPAPER_OPEN = "TRANSIT_WALLPAPER_OPEN"
+    private const val TRANSIT_WALLPAPER_CLOSE = "TRANSIT_WALLPAPER_CLOSE"
+    private const val TRANSIT_WALLPAPER_INTRA_OPEN = "TRANSIT_WALLPAPER_INTRA_OPEN"
+    private const val TRANSIT_WALLPAPER_INTRA_CLOSE = "TRANSIT_WALLPAPER_INTRA_CLOSE"
+    private const val TRANSIT_KEYGUARD_GOING_AWAY = "TRANSIT_KEYGUARD_GOING_AWAY"
+    private const val TRANSIT_KEYGUARD_GOING_AWAY_ON_WALLPAPER =
+        "TRANSIT_KEYGUARD_GOING_AWAY_ON_WALLPAPER"
+    private const val TRANSIT_KEYGUARD_OCCLUDE = "TRANSIT_KEYGUARD_OCCLUDE"
+    private const val TRANSIT_KEYGUARD_UNOCCLUDE = "TRANSIT_KEYGUARD_UNOCCLUDE"
+    private const val TRANSIT_TRANSLUCENT_ACTIVITY_OPEN = "TRANSIT_TRANSLUCENT_ACTIVITY_OPEN"
+    private const val TRANSIT_TRANSLUCENT_ACTIVITY_CLOSE = "TRANSIT_TRANSLUCENT_ACTIVITY_CLOSE"
+
+    /**
+     * Parses [WindowManagerTraceFileProto] from [data] and uses the proto to generates
+     * a list of trace entries.
+     *
+     * @param data binary proto data
+     * @param source Path to source of data for additional debug information
+     * @param checksum File SHA512 checksum
+     */
+    @JvmOverloads
+    @JvmStatic
+    fun parseFromTrace(
+        data: ByteArray?,
+        source: Path? = null,
+        checksum: String = ""
+    ): WindowManagerTrace {
+        val fileProto: WindowManagerTraceFileProto
+        try {
+            measureTimeMillis {
+                fileProto = WindowManagerTraceFileProto.parseFrom(data)
+            }.also {
+                Log.v(LOG_TAG, "Parsing proto (WM Trace): ${it}ms")
+            }
+        } catch (e: InvalidProtocolBufferNanoException) {
+            throw RuntimeException(e)
+        }
+        return parseFromTrace(fileProto, source, checksum)
+    }
+
+    /**
+     * Uses the proto to generates a list of trace entries.
+     *
+     * @param proto Parsed proto data
+     * @param source Path to source of data for additional debug information
+     * @param checksum File SHA512 checksum
+     */
+    @JvmOverloads
+    @JvmStatic
+    fun parseFromTrace(
+        proto: WindowManagerTraceFileProto,
+        source: Path? = null,
+        checksum: String = ""
+    ): WindowManagerTrace {
+        val entries = mutableListOf<WindowManagerState>()
+        var traceParseTime = 0L
+        for (entryProto in proto.entry) {
+            val entryParseTime = measureTimeMillis {
+                val entry = newTraceEntry(entryProto.windowManagerService,
+                    entryProto.elapsedRealtimeNanos, entryProto.where)
+                entries.add(entry)
+            }
+            traceParseTime += entryParseTime
+        }
+
+        Log.v(LOG_TAG, "Parsing duration (WM Trace): ${traceParseTime}ms " +
+            "(avg ${traceParseTime / entries.size}ms per entry)")
+        return WindowManagerTrace(entries, source?.toAbsolutePath()?.toString()
+            ?: "", checksum)
+    }
+
+    /**
+     * Parses [WindowManagerServiceDumpProto] from [proto] dump and uses the proto to generates
+     * a list of trace entries.
+     *
+     * @param proto Parsed proto data
+     */
+    @JvmStatic
+    fun parseFromDump(proto: WindowManagerServiceDumpProto): WindowManagerTrace {
+        return WindowManagerTrace(
+            listOf(newTraceEntry(proto, timestamp = 0, where = "")),
+            source = "",
+            sourceChecksum = "")
+    }
+
+    /**
+     * Parses [WindowManagerServiceDumpProto] from [data] and uses the proto to generates
+     * a list of trace entries.
+     *
+     * @param data binary proto data
+     */
+    @JvmStatic
+    fun parseFromDump(data: ByteArray?): WindowManagerTrace {
+        val fileProto = try {
+            WindowManagerServiceDumpProto.parseFrom(data)
+        } catch (e: InvalidProtocolBufferNanoException) {
+            throw RuntimeException(e)
+        }
+        return WindowManagerTrace(parseFromDump(fileProto), source = "", sourceChecksum = "")
+    }
+
+    private fun newTraceEntry(
+        proto: WindowManagerServiceDumpProto,
+        timestamp: Long,
+        where: String
+    ): WindowManagerState {
+        return WindowManagerState(
+            where = where,
+            policy = newWindowManagerPolicy(proto.policy),
+            focusedApp = proto.focusedApp,
+            focusedDisplayId = proto.focusedDisplayId,
+            focusedWindow = proto.focusedWindow?.title ?: "",
+            inputMethodWindowAppToken = if (proto.inputMethodWindow != null) {
+                Integer.toHexString(proto.inputMethodWindow.hashCode)
+            } else {
+                ""
+            },
+            isHomeRecentsComponent = proto.rootWindowContainer.isHomeRecentsComponent,
+            isDisplayFrozen = proto.displayFrozen,
+            pendingActivities = proto.rootWindowContainer.pendingActivities
+                .map { it.title }.toTypedArray(),
+            root = newRootWindowContainer(proto.rootWindowContainer),
+            keyguardControllerState = newKeyguardControllerState(
+                proto.rootWindowContainer.keyguardController),
+            timestamp = timestamp
+        )
+    }
+
+    private fun newWindowManagerPolicy(proto: WindowManagerPolicyProto): WindowManagerPolicy? {
+        return WindowManagerPolicy(
+            focusedAppToken = proto.focusedAppToken ?: "",
+            forceStatusBar = proto.forceStatusBar,
+            forceStatusBarFromKeyguard = proto.forceStatusBarFromKeyguard,
+            keyguardDrawComplete = proto.keyguardDrawComplete,
+            keyguardOccluded = proto.keyguardOccluded,
+            keyguardOccludedChanged = proto.keyguardOccludedChanged,
+            keyguardOccludedPending = proto.keyguardOccludedPending,
+            lastSystemUiFlags = proto.lastSystemUiFlags,
+            orientation = proto.orientation,
+            rotation = proto.rotation,
+            rotationMode = proto.rotationMode,
+            screenOnFully = proto.screenOnFully,
+            windowManagerDrawComplete = proto.windowManagerDrawComplete
+        )
+    }
+
+    private fun newRootWindowContainer(proto: RootWindowContainerProto): RootWindowContainer {
+        return RootWindowContainer(
+            newWindowContainer(
+                proto.windowContainer,
+                proto.windowContainer.children
+                    .mapNotNull { p -> newWindowContainerChild(p, isActivityInTree = false) }
+            ) ?: error("Window container should not be null")
+        )
+    }
+
+    private fun newKeyguardControllerState(
+        proto: KeyguardControllerProto?
+    ): KeyguardControllerState {
+        return KeyguardControllerState(
+            isAodShowing = proto?.aodShowing ?: false,
+            isKeyguardShowing = proto?.keyguardShowing ?: false,
+            keyguardOccludedStates = proto?.keyguardOccludedStates
+                ?.map { it.displayId to it.keyguardOccluded }
+                ?.toMap()
+                ?: emptyMap()
+        )
+    }
+
+    private fun newWindowContainerChild(
+        proto: WindowContainerChildProto,
+        isActivityInTree: Boolean
+    ): WindowContainer? {
+        return newDisplayContent(proto.displayContent, isActivityInTree)
+            ?: newDisplayArea(proto.displayArea, isActivityInTree)
+            ?: newTask(proto.task, isActivityInTree) ?: newActivity(proto.activity)
+            ?: newWindowToken(proto.windowToken, isActivityInTree)
+            ?: newWindowState(proto.window, isActivityInTree)
+            ?: newWindowContainer(proto.windowContainer, children = emptyList())
+    }
+
+    private fun newDisplayContent(
+        proto: DisplayContentProto?,
+        isActivityInTree: Boolean
+    ): DisplayContent? {
+        return if (proto == null) {
+            null
+        } else {
+            DisplayContent(
+                id = proto.id,
+                focusedRootTaskId = proto.focusedRootTaskId,
+                resumedActivity = proto.resumedActivity?.title ?: "",
+                singleTaskInstance = proto.singleTaskInstance,
+                _defaultPinnedStackBounds = proto.pinnedTaskController?.defaultBounds?.toRect(),
+                _pinnedStackMovementBounds = proto.pinnedTaskController?.movementBounds?.toRect(),
+                displayRect = Rect(0, 0, proto.displayInfo?.logicalWidth
+                    ?: 0, proto.displayInfo?.logicalHeight ?: 0),
+                appRect = Rect(0, 0, proto.displayInfo?.appWidth ?: 0,
+                    proto.displayInfo?.appHeight
+                        ?: 0),
+                dpi = proto.dpi,
+                flags = proto.displayInfo?.flags ?: 0,
+                _stableBounds = proto.displayFrames?.stableBounds?.toRect(),
+                surfaceSize = proto.surfaceSize,
+                focusedApp = proto.focusedApp,
+                lastTransition = appTransitionToString(
+                    proto.appTransition?.lastUsedAppTransition ?: 0),
+                appTransitionState = appStateToString(
+                    proto.appTransition?.appTransitionState ?: 0),
+                rotation = proto.displayRotation?.rotation ?: 0,
+                lastOrientation = proto.displayRotation?.lastOrientation ?: 0,
+                windowContainer = newWindowContainer(
+                    proto.rootDisplayArea.windowContainer,
+                    proto.rootDisplayArea.windowContainer.children
+                        .mapNotNull { p -> newWindowContainerChild(p, isActivityInTree) },
+                    nameOverride = proto.displayInfo?.name ?: ""
+                ) ?: error("Window container should not be null")
+            )
+        }
+    }
+
+    private fun newDisplayArea(proto: DisplayAreaProto?, isActivityInTree: Boolean): DisplayArea? {
+        return if (proto == null) {
+            null
+        } else {
+            DisplayArea(
+                isTaskDisplayArea = proto.isTaskDisplayArea,
+                windowContainer = newWindowContainer(
+                    proto.windowContainer,
+                    proto.windowContainer.children
+                        .mapNotNull { p -> newWindowContainerChild(p, isActivityInTree) }
+                ) ?: error("Window container should not be null")
+            )
+        }
+    }
+
+    private fun newTask(proto: TaskProto?, isActivityInTree: Boolean): ActivityTask? {
+        return if (proto == null) {
+            null
+        } else {
+            ActivityTask(
+                activityType = proto.activityType,
+                isFullscreen = proto.fillsParent,
+                bounds = proto.bounds.toRect(),
+                taskId = proto.id,
+                rootTaskId = proto.rootTaskId,
+                displayId = proto.displayId,
+                _lastNonFullscreenBounds = proto.lastNonFullscreenBounds?.toRect(),
+                realActivity = proto.realActivity,
+                origActivity = proto.origActivity,
+                resizeMode = proto.resizeMode,
+                _resumedActivity = proto.resumedActivity?.title ?: "",
+                animatingBounds = proto.animatingBounds,
+                surfaceWidth = proto.surfaceWidth,
+                surfaceHeight = proto.surfaceHeight,
+                createdByOrganizer = proto.createdByOrganizer,
+                minWidth = proto.minWidth,
+                minHeight = proto.minHeight,
+                windowContainer = newWindowContainer(
+                    proto.windowContainer,
+                    proto.windowContainer.children
+                        .mapNotNull { p -> newWindowContainerChild(p, isActivityInTree) }
+                ) ?: error("Window container should not be null")
+            )
+        }
+    }
+
+    private fun newActivity(proto: ActivityRecordProto?): Activity? {
+        return if (proto == null) {
+            null
+        } else {
+            Activity(
+                name = proto.name,
+                state = proto.state,
+                visible = proto.visible,
+                frontOfTask = proto.frontOfTask,
+                procId = proto.procId,
+                isTranslucent = proto.translucent,
+                windowContainer = newWindowContainer(
+                    proto.windowToken.windowContainer,
+                    proto.windowToken.windowContainer.children
+                        .mapNotNull { p -> newWindowContainerChild(p, isActivityInTree = true) }
+                ) ?: error("Window container should not be null")
+            )
+        }
+    }
+
+    private fun newWindowToken(proto: WindowTokenProto?, isActivityInTree: Boolean): WindowToken? {
+        return if (proto == null) {
+            null
+        } else {
+            WindowToken(
+                newWindowContainer(
+                    proto.windowContainer,
+                    proto.windowContainer.children
+                        .mapNotNull { p -> newWindowContainerChild(p, isActivityInTree) }
+                ) ?: error("Window container should not be null")
+            )
+        }
+    }
+
+    private fun newWindowState(proto: WindowStateProto?, isActivityInTree: Boolean): WindowState? {
+        return if (proto == null) {
+            null
+        } else {
+            val identifierName = proto.windowContainer.identifier?.title ?: ""
+            WindowState(
+                attributes = newWindowLayerParams(proto.attributes),
+                displayId = proto.displayId,
+                stackId = proto.stackId,
+                layer = proto.animator?.surface?.layer ?: 0,
+                isSurfaceShown = proto.animator?.surface?.shown ?: false,
+                windowType = when {
+                    identifierName.startsWith(WindowState.STARTING_WINDOW_PREFIX) ->
+                        WindowState.WINDOW_TYPE_STARTING
+                    proto.animatingExit -> WindowState.WINDOW_TYPE_EXITING
+                    identifierName.startsWith(WindowState.DEBUGGER_WINDOW_PREFIX) ->
+                        WindowState.WINDOW_TYPE_STARTING
+                    else -> 0
+                },
+                requestedSize = Bounds(proto.requestedWidth, proto.requestedHeight),
+                surfacePosition = proto.surfacePosition?.toRect(),
+                _frame = proto.windowFrames?.frame?.toRect(),
+                _containingFrame = proto.windowFrames?.containingFrame?.toRect(),
+                _parentFrame = proto.windowFrames?.parentFrame?.toRect(),
+                _contentFrame = proto.windowFrames?.contentFrame?.toRect(),
+                _contentInsets = proto.windowFrames?.contentInsets?.toRect(),
+                _surfaceInsets = proto.surfaceInsets?.toRect(),
+                _givenContentInsets = proto.givenContentInsets?.toRect(),
+                _crop = proto.animator?.lastClipRect?.toRect(),
+                windowContainer = newWindowContainer(
+                    proto.windowContainer,
+                    proto.windowContainer.children
+                        .mapNotNull { p -> newWindowContainerChild(p, isActivityInTree) },
+                    nameOverride = when {
+                        // Existing code depends on the prefix being removed
+                        identifierName.startsWith(WindowState.STARTING_WINDOW_PREFIX) ->
+                            identifierName.substring(WindowState.STARTING_WINDOW_PREFIX.length)
+                        identifierName.startsWith(WindowState.DEBUGGER_WINDOW_PREFIX) ->
+                            identifierName.substring(WindowState.DEBUGGER_WINDOW_PREFIX.length)
+                        else -> identifierName
+                    }
+                ) ?: error("Window container should not be null"),
+                isAppWindow = isActivityInTree
+            )
+        }
+    }
+
+    private fun newWindowLayerParams(proto: WindowLayoutParamsProto?): WindowLayoutParams {
+        return WindowLayoutParams(
+            type = proto?.type ?: 0,
+            x = proto?.x ?: 0,
+            y = proto?.y ?: 0,
+            width = proto?.width ?: 0,
+            height = proto?.height ?: 0,
+            horizontalMargin = proto?.horizontalMargin ?: 0f,
+            verticalMargin = proto?.verticalMargin ?: 0f,
+            gravity = proto?.gravity ?: 0,
+            softInputMode = proto?.softInputMode ?: 0,
+            format = proto?.format ?: 0,
+            windowAnimations = proto?.windowAnimations ?: 0,
+            alpha = proto?.alpha ?: 0f,
+            screenBrightness = proto?.screenBrightness ?: 0f,
+            buttonBrightness = proto?.buttonBrightness ?: 0f,
+            rotationAnimation = proto?.rotationAnimation ?: 0,
+            preferredRefreshRate = proto?.preferredRefreshRate ?: 0f,
+            preferredDisplayModeId = proto?.preferredDisplayModeId ?: 0,
+            hasSystemUiListeners = proto?.hasSystemUiListeners ?: false,
+            inputFeatureFlags = proto?.inputFeatureFlags ?: 0,
+            userActivityTimeout = proto?.userActivityTimeout ?: 0,
+            colorMode = proto?.colorMode ?: 0,
+            flags = proto?.flags ?: 0,
+            privateFlags = proto?.privateFlags ?: 0,
+            systemUiVisibilityFlags = proto?.systemUiVisibilityFlags ?: 0,
+            subtreeSystemUiVisibilityFlags = proto?.subtreeSystemUiVisibilityFlags ?: 0,
+            appearance = proto?.appearance ?: 0,
+            behavior = proto?.behavior ?: 0,
+            fitInsetsTypes = proto?.fitInsetsTypes ?: 0,
+            fitInsetsSides = proto?.fitInsetsSides ?: 0,
+            fitIgnoreVisibility = proto?.fitIgnoreVisibility ?: false
+        )
+    }
+
+    private fun newConfigurationContainer(
+        proto: ConfigurationContainerProto?
+    ): ConfigurationContainer {
+        return ConfigurationContainer(
+            overrideConfiguration = newConfiguration(proto?.overrideConfiguration),
+            fullConfiguration = newConfiguration(proto?.fullConfiguration),
+            mergedOverrideConfiguration = newConfiguration(proto?.mergedOverrideConfiguration)
+        )
+    }
+
+    private fun newConfiguration(proto: ConfigurationProto?): Configuration? {
+        return if (proto == null) {
+            null
+        } else {
+            Configuration(
+                windowConfiguration = if (proto.windowConfiguration != null) {
+                    newWindowConfiguration(proto.windowConfiguration)
+                } else {
+                    null
+                },
+                densityDpi = proto.densityDpi,
+                orientation = proto.orientation,
+                screenHeightDp = proto.screenHeightDp,
+                screenWidthDp = proto.screenWidthDp,
+                smallestScreenWidthDp = proto.smallestScreenWidthDp,
+                screenLayout = proto.screenLayout,
+                uiMode = proto.uiMode
+            )
+        }
+    }
+
+    private fun newWindowConfiguration(proto: WindowConfigurationProto): WindowConfiguration {
+        return WindowConfiguration(
+            _appBounds = proto.appBounds?.toRect(),
+            _bounds = proto.bounds?.toRect(),
+            _maxBounds = proto.maxBounds?.toRect(),
+            windowingMode = proto.windowingMode,
+            activityType = proto.activityType
+        )
+    }
+
+    private fun newWindowContainer(
+        proto: WindowContainerProto?,
+        children: List<WindowContainer>,
+        nameOverride: String? = null
+    ): WindowContainer? {
+        return if (proto == null) {
+            null
+        } else {
+            WindowContainer(
+                title = nameOverride ?: proto.identifier?.title ?: "",
+                token = proto.identifier?.hashCode?.toString(16) ?: "",
+                orientation = proto.orientation,
+                _isVisible = proto.visible,
+                configurationContainer = newConfigurationContainer(
+                    proto.configurationContainer),
+                children.toTypedArray()
+            )
+        }
+    }
+
+    private fun appTransitionToString(transition: Int): String {
+        return when (transition) {
+            ViewProtoEnums.TRANSIT_UNSET -> "TRANSIT_UNSET"
+            ViewProtoEnums.TRANSIT_NONE -> "TRANSIT_NONE"
+            ViewProtoEnums.TRANSIT_ACTIVITY_OPEN -> TRANSIT_ACTIVITY_OPEN
+            ViewProtoEnums.TRANSIT_ACTIVITY_CLOSE -> TRANSIT_ACTIVITY_CLOSE
+            ViewProtoEnums.TRANSIT_TASK_OPEN -> TRANSIT_TASK_OPEN
+            ViewProtoEnums.TRANSIT_TASK_CLOSE -> TRANSIT_TASK_CLOSE
+            ViewProtoEnums.TRANSIT_TASK_TO_FRONT -> "TRANSIT_TASK_TO_FRONT"
+            ViewProtoEnums.TRANSIT_TASK_TO_BACK -> "TRANSIT_TASK_TO_BACK"
+            ViewProtoEnums.TRANSIT_WALLPAPER_CLOSE -> TRANSIT_WALLPAPER_CLOSE
+            ViewProtoEnums.TRANSIT_WALLPAPER_OPEN -> TRANSIT_WALLPAPER_OPEN
+            ViewProtoEnums.TRANSIT_WALLPAPER_INTRA_OPEN -> TRANSIT_WALLPAPER_INTRA_OPEN
+            ViewProtoEnums.TRANSIT_WALLPAPER_INTRA_CLOSE -> TRANSIT_WALLPAPER_INTRA_CLOSE
+            ViewProtoEnums.TRANSIT_TASK_OPEN_BEHIND -> "TRANSIT_TASK_OPEN_BEHIND"
+            ViewProtoEnums.TRANSIT_ACTIVITY_RELAUNCH -> "TRANSIT_ACTIVITY_RELAUNCH"
+            ViewProtoEnums.TRANSIT_DOCK_TASK_FROM_RECENTS -> "TRANSIT_DOCK_TASK_FROM_RECENTS"
+            ViewProtoEnums.TRANSIT_KEYGUARD_GOING_AWAY -> TRANSIT_KEYGUARD_GOING_AWAY
+            ViewProtoEnums.TRANSIT_KEYGUARD_GOING_AWAY_ON_WALLPAPER ->
+                TRANSIT_KEYGUARD_GOING_AWAY_ON_WALLPAPER
+            ViewProtoEnums.TRANSIT_KEYGUARD_OCCLUDE -> TRANSIT_KEYGUARD_OCCLUDE
+            ViewProtoEnums.TRANSIT_KEYGUARD_UNOCCLUDE -> TRANSIT_KEYGUARD_UNOCCLUDE
+            ViewProtoEnums.TRANSIT_TRANSLUCENT_ACTIVITY_OPEN ->
+                TRANSIT_TRANSLUCENT_ACTIVITY_OPEN
+            ViewProtoEnums.TRANSIT_TRANSLUCENT_ACTIVITY_CLOSE ->
+                TRANSIT_TRANSLUCENT_ACTIVITY_CLOSE
+            ViewProtoEnums.TRANSIT_CRASHING_ACTIVITY_CLOSE -> "TRANSIT_CRASHING_ACTIVITY_CLOSE"
+            else -> error("Invalid lastUsedAppTransition")
+        }
+    }
+
+    private fun appStateToString(appState: Int): String {
+        return when (appState) {
+            AppTransitionProto.APP_STATE_IDLE -> "APP_STATE_IDLE"
+            AppTransitionProto.APP_STATE_READY -> "APP_STATE_READY"
+            AppTransitionProto.APP_STATE_RUNNING -> "APP_STATE_RUNNING"
+            AppTransitionProto.APP_STATE_TIMEOUT -> "APP_STATE_TIMEOUT"
+            else -> error("Invalid AppTransitionState")
+        }
+    }
+
+    private fun RectProto.toRect() = Rect(this.left, this.top, this.right, this.bottom)
+}
\ No newline at end of file
diff --git a/libraries/flicker/test/Android.bp b/libraries/flicker/test/Android.bp
index f62e23d..1af44fe 100644
--- a/libraries/flicker/test/Android.bp
+++ b/libraries/flicker/test/Android.bp
@@ -14,6 +14,7 @@
 // limitations under the License.
 //
 
+
 package {
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
@@ -21,17 +22,15 @@
 android_test {
     name: "FlickerLibTest",
     // sign this with platform cert, so this test is allowed to call private platform apis
+    manifest: "AndroidManifest.xml",
+    test_config: "AndroidTest.xml",
     certificate: "platform",
     platform_apis: true,
-    test_suites: ["tests"],
-    srcs: ["src/**/*.java"],
+    test_suites: ["device-tests"],
+    srcs: ["src/**/*.kt"],
     libs: ["android.test.runner"],
     static_libs: [
-        "androidx.test.rules",
-        "platform-test-annotations",
-        "truth-prebuilt",
-        "platformprotosnano",
-        "layersprotosnano",
         "flickerlib",
+        "launcher-aosp-tapl"
     ],
 }
diff --git a/libraries/flicker/test/AndroidManifest.xml b/libraries/flicker/test/AndroidManifest.xml
index 4d05839..20b8f0a 100644
--- a/libraries/flicker/test/AndroidManifest.xml
+++ b/libraries/flicker/test/AndroidManifest.xml
@@ -6,17 +6,23 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="com.android.server.wm.flicker">
 
-    <uses-sdk android:minSdkVersion="27" android:targetSdkVersion="27"/>
+    <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29"/>
     <!-- Read and write traces from external storage -->
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <!-- Allow the test to write directly to /sdcard/ -->
+    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
     <!-- Capture screen contents -->
     <uses-permission android:name="android.permission.ACCESS_SURFACE_FLINGER" />
     <!-- Run layers trace -->
     <uses-permission android:name="android.permission.HARDWARE_TEST"/>
+    <!-- Capture screen recording -->
+    <uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT"/>
     <!-- Enable / Disable tracing !-->
     <uses-permission android:name="android.permission.DUMP" />
-    <application android:label="FlickerLibTest">
+    <!-- Allow the test to write directly to /sdcard/ -->
+    <application android:label="FlickerLibTest"
+                 android:requestLegacyExternalStorage="true">
         <uses-library android:name="android.test.runner"/>
     </application>
 
diff --git a/libraries/flicker/test/AndroidTest.xml b/libraries/flicker/test/AndroidTest.xml
index 99e45ee..74eb643 100644
--- a/libraries/flicker/test/AndroidTest.xml
+++ b/libraries/flicker/test/AndroidTest.xml
@@ -3,17 +3,21 @@
  * Copyright 2018 Google Inc. All Rights Reserved.
  -->
 <configuration description="Config for WindowManager Flicker Tests">
+    <option name="test-tag" value="FlickerLibTest" />
     <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
         <!-- keeps the screen on during tests -->
         <option name="screen-always-on" value="on" />
         <!-- prevents the phone from restarting -->
         <option name="force-skip-system-props" value="true" />
+        <!-- restart launcher to activate TAPL -->
+        <option name="run-command" value="setprop ro.test_harness 1 ; am force-stop com.google.android.apps.nexuslauncher" />
     </target_preparer>
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="cleanup-apks" value="true"/>
         <option name="test-file-name" value="FlickerLibTest.apk"/>
     </target_preparer>
     <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+        <option name="exclude-annotation" value="androidx.test.filters.FlakyTest" />
         <option name="package" value="com.android.server.wm.flicker"/>
         <option name="hidden-api-checks" value="false" />
     </test>
diff --git a/libraries/flicker/test/assets/testdata/layers_trace_backcolorsurface.pb b/libraries/flicker/test/assets/testdata/layers_trace_backcolorsurface.pb
new file mode 100644
index 0000000..d19d33d
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/layers_trace_backcolorsurface.pb
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/layers_trace_emptyregion.pb b/libraries/flicker/test/assets/testdata/layers_trace_emptyregion.pb
index 98ee6f3..3df8cc7 100644
--- a/libraries/flicker/test/assets/testdata/layers_trace_emptyregion.pb
+++ b/libraries/flicker/test/assets/testdata/layers_trace_emptyregion.pb
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/layers_trace_invalid_visible_layers.pb b/libraries/flicker/test/assets/testdata/layers_trace_invalid_visible_layers.pb
new file mode 100644
index 0000000..b35c1b5
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/layers_trace_invalid_visible_layers.pb
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/layers_trace_launch_split_screen.pb b/libraries/flicker/test/assets/testdata/layers_trace_launch_split_screen.pb
new file mode 100644
index 0000000..f64374a
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/layers_trace_launch_split_screen.pb
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/layers_trace_no_hwc_composition.pb b/libraries/flicker/test/assets/testdata/layers_trace_no_hwc_composition.pb
new file mode 100644
index 0000000..d6d4c84
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/layers_trace_no_hwc_composition.pb
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/layers_trace_snapshot_visible.pb b/libraries/flicker/test/assets/testdata/layers_trace_snapshot_visible.pb
new file mode 100755
index 0000000..923757e
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/layers_trace_snapshot_visible.pb
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/layers_trace_valid_visible_layers.pb b/libraries/flicker/test/assets/testdata/layers_trace_valid_visible_layers.pb
new file mode 100644
index 0000000..0d62ae4
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/layers_trace_valid_visible_layers.pb
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/one_visible_layer_launcher_trace.pb b/libraries/flicker/test/assets/testdata/one_visible_layer_launcher_trace.pb
new file mode 100755
index 0000000..86390c7
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/one_visible_layer_launcher_trace.pb
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/wm_trace_activity_transition.pb b/libraries/flicker/test/assets/testdata/wm_trace_activity_transition.pb
new file mode 100644
index 0000000..1e8ae77
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/wm_trace_activity_transition.pb
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/wm_trace_dump.pb b/libraries/flicker/test/assets/testdata/wm_trace_dump.pb
new file mode 100644
index 0000000..30ffd14
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/wm_trace_dump.pb
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/wm_trace_ime.pb b/libraries/flicker/test/assets/testdata/wm_trace_ime.pb
new file mode 100755
index 0000000..f33c19b
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/wm_trace_ime.pb
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/wm_trace_open_and_close_chrome.pb b/libraries/flicker/test/assets/testdata/wm_trace_open_and_close_chrome.pb
new file mode 100644
index 0000000..4bc9084
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/wm_trace_open_and_close_chrome.pb
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/wm_trace_open_app_cold.pb b/libraries/flicker/test/assets/testdata/wm_trace_open_app_cold.pb
new file mode 100755
index 0000000..b6366b8
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/wm_trace_open_app_cold.pb
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/wm_trace_open_recents.pb b/libraries/flicker/test/assets/testdata/wm_trace_open_recents.pb
new file mode 100644
index 0000000..7bf23e8
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/wm_trace_open_recents.pb
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/wm_trace_openchrome.pb b/libraries/flicker/test/assets/testdata/wm_trace_openchrome.pb
index b3f3170..50f45df 100644
--- a/libraries/flicker/test/assets/testdata/wm_trace_openchrome.pb
+++ b/libraries/flicker/test/assets/testdata/wm_trace_openchrome.pb
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/wm_trace_resumed_activities_in_stack.pb b/libraries/flicker/test/assets/testdata/wm_trace_resumed_activities_in_stack.pb
new file mode 100644
index 0000000..2fdab51
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/wm_trace_resumed_activities_in_stack.pb
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/wm_trace_rotation.pb b/libraries/flicker/test/assets/testdata/wm_trace_rotation.pb
new file mode 100644
index 0000000..481e3f9
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/wm_trace_rotation.pb
Binary files differ
diff --git a/libraries/flicker/test/assets/testdata/wm_trace_valid_visible_windows.pb b/libraries/flicker/test/assets/testdata/wm_trace_valid_visible_windows.pb
new file mode 100644
index 0000000..5cee11d
--- /dev/null
+++ b/libraries/flicker/test/assets/testdata/wm_trace_valid_visible_windows.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
deleted file mode 100644
index 6974d60..0000000
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/AssertionsCheckerTest.java
+++ /dev/null
@@ -1,204 +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;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.server.wm.flicker.Assertions.Result;
-
-import org.junit.FixMethodOrder;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.MethodSorters;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Contains {@link AssertionsChecker} tests. To run this test: {@code atest
- * FlickerLibTest:AssertionsCheckerTest}
- */
-@RunWith(AndroidJUnit4.class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-public class AssertionsCheckerTest {
-
-    /**
-     * Returns a list of SimpleEntry objects with {@code data} and incremental timestamps starting
-     * at 0.
-     */
-    private static List<SimpleEntry> getTestEntries(int... data) {
-        List<SimpleEntry> entries = new ArrayList<>();
-        for (int i = 0; i < data.length; i++) {
-            entries.add(new SimpleEntry(i, data[i]));
-        }
-        return entries;
-    }
-
-    @Test
-    public void canCheckAllEntries() {
-        AssertionsChecker<SimpleEntry> checker = new AssertionsChecker<>();
-        checker.add(SimpleEntry::isData42, "isData42");
-
-        List<Result> failures = checker.test(getTestEntries(1, 1, 1, 1, 1));
-
-        assertThat(failures).hasSize(5);
-    }
-
-    @Test
-    public void canCheckFirstEntry() {
-        AssertionsChecker<SimpleEntry> checker = new AssertionsChecker<>();
-        checker.checkFirstEntry();
-        checker.add(SimpleEntry::isData42, "isData42");
-
-        List<Result> failures = checker.test(getTestEntries(1, 1, 1, 1, 1));
-
-        assertThat(failures).hasSize(1);
-        assertThat(failures.get(0).timestamp).isEqualTo(0);
-    }
-
-    @Test
-    public void canCheckLastEntry() {
-        AssertionsChecker<SimpleEntry> checker = new AssertionsChecker<>();
-        checker.checkLastEntry();
-        checker.add(SimpleEntry::isData42, "isData42");
-
-        List<Result> failures = checker.test(getTestEntries(1, 1, 1, 1, 1));
-
-        assertThat(failures).hasSize(1);
-        assertThat(failures.get(0).timestamp).isEqualTo(4);
-    }
-
-    @Test
-    public void canCheckRangeOfEntries() {
-        AssertionsChecker<SimpleEntry> checker = new AssertionsChecker<>();
-        checker.filterByRange(1, 2);
-        checker.add(SimpleEntry::isData42, "isData42");
-
-        List<Result> failures = checker.test(getTestEntries(1, 42, 42, 1, 1));
-
-        assertThat(failures).hasSize(0);
-    }
-
-    @Test
-    public void emptyRangePasses() {
-        AssertionsChecker<SimpleEntry> checker = new AssertionsChecker<>();
-        checker.filterByRange(9, 10);
-        checker.add(SimpleEntry::isData42, "isData42");
-
-        List<Result> failures = checker.test(getTestEntries(1, 1, 1, 1, 1));
-
-        assertThat(failures).isEmpty();
-    }
-
-    @Test
-    public void canCheckChangingAssertions() {
-        AssertionsChecker<SimpleEntry> checker = new AssertionsChecker<>();
-        checker.add(SimpleEntry::isData42, "isData42");
-        checker.add(SimpleEntry::isData0, "isData0");
-        checker.checkChangingAssertions();
-
-        List<Result> failures = checker.test(getTestEntries(42, 0, 0, 0, 0));
-
-        assertThat(failures).isEmpty();
-    }
-
-    @Test
-    public void canCheckChangingAssertions_withNoAssertions() {
-        AssertionsChecker<SimpleEntry> checker = new AssertionsChecker<>();
-        checker.checkChangingAssertions();
-
-        List<Result> failures = checker.test(getTestEntries(42, 0, 0, 0, 0));
-
-        assertThat(failures).isEmpty();
-    }
-
-    @Test
-    public void canCheckChangingAssertions_withSingleAssertion() {
-        AssertionsChecker<SimpleEntry> checker = new AssertionsChecker<>();
-        checker.add(SimpleEntry::isData42, "isData42");
-        checker.checkChangingAssertions();
-
-        List<Result> failures = checker.test(getTestEntries(42, 42, 42, 42, 42));
-
-        assertThat(failures).isEmpty();
-    }
-
-    @Test
-    public void canFailCheckChangingAssertions_ifStartingAssertionFails() {
-        AssertionsChecker<SimpleEntry> checker = new AssertionsChecker<>();
-        checker.add(SimpleEntry::isData42, "isData42");
-        checker.add(SimpleEntry::isData0, "isData0");
-        checker.checkChangingAssertions();
-
-        List<Result> failures = checker.test(getTestEntries(0, 0, 0, 0, 0));
-
-        assertThat(failures).hasSize(1);
-    }
-
-    @Test
-    public void canFailCheckChangingAssertions_ifStartingAssertionAlwaysPasses() {
-        AssertionsChecker<SimpleEntry> checker = new AssertionsChecker<>();
-        checker.add(SimpleEntry::isData42, "isData42");
-        checker.add(SimpleEntry::isData0, "isData0");
-        checker.checkChangingAssertions();
-
-        List<Result> failures = checker.test(getTestEntries(0, 0, 0, 0, 0));
-
-        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;
-
-        SimpleEntry(long timestamp, int data) {
-            this.mTimestamp = timestamp;
-            this.mData = data;
-        }
-
-        @Override
-        public long getTimestamp() {
-            return mTimestamp;
-        }
-
-        Result isData42() {
-            return new Result(this.mData == 42, this.mTimestamp, "is42", "!is42");
-        }
-
-        Result isData0() {
-            return new Result(this.mData == 0, this.mTimestamp, "is42", "!is0");
-        }
-    }
-}
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/AssertionsCheckerTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/AssertionsCheckerTest.kt
new file mode 100644
index 0000000..f82d6df
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/AssertionsCheckerTest.kt
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2021 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
+
+import com.android.server.wm.flicker.assertions.AssertionsChecker
+import com.android.server.wm.flicker.assertions.FlickerSubject
+import com.android.server.wm.flicker.traces.FlickerFailureStrategy
+import com.android.server.wm.flicker.traces.FlickerSubjectException
+import com.android.server.wm.traces.common.ITraceEntry
+import com.google.common.truth.FailureMetadata
+import com.google.common.truth.StandardSubjectBuilder
+import com.google.common.truth.Subject
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+
+/**
+ * Contains [AssertionsChecker] tests. To run this test: `atest
+ * FlickerLibTest:AssertionsCheckerTest`
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class AssertionsCheckerTest {
+    @Test
+    fun emptyRangePasses() {
+        val checker = AssertionsChecker<SimpleEntrySubject>()
+        checker.add("isData42") { it.isData42() }
+        checker.test(emptyList())
+    }
+
+    @Test
+    fun canCheckChangingAssertions() {
+        val checker = AssertionsChecker<SimpleEntrySubject>()
+        checker.add("isData42") { it.isData42() }
+        checker.add("isData0") { it.isData0() }
+        checker.test(getTestEntries(42, 0, 0, 0, 0))
+    }
+
+    @Test
+    fun canCheckChangingAssertions_withNoAssertions() {
+        val checker = AssertionsChecker<SimpleEntrySubject>()
+        checker.test(getTestEntries(42, 0, 0, 0, 0))
+    }
+
+    @Test
+    fun canCheckChangingAssertions_withSingleAssertion() {
+        val checker = AssertionsChecker<SimpleEntrySubject>()
+        checker.add("isData42") { it.isData42() }
+        checker.test(getTestEntries(42, 42, 42, 42, 42))
+    }
+
+    @Test
+    fun canFailCheckChangingAssertions_ifStartingAssertionFails() {
+        val checker = AssertionsChecker<SimpleEntrySubject>()
+        checker.add("isData42") { it.isData42() }
+        checker.add("isData0") { it.isData0() }
+        try {
+            checker.test(getTestEntries(0, 0, 0, 0, 0))
+        } catch (failure: Throwable) {
+            require(failure is FlickerSubjectException) { "Unknown failure $failure" }
+            assertFailure(failure.cause)
+                .factValue("expected").isEqualTo("42")
+            assertFailure(failure.cause)
+                .factValue("but was").isEqualTo("0")
+        }
+    }
+
+    @Test
+    fun canCheckChangingAssertions_skipUntilFirstSuccess() {
+        val checker = AssertionsChecker<SimpleEntrySubject>()
+        checker.skipUntilFirstAssertion()
+        checker.add("isData42") { it.isData42() }
+        checker.add("isData0") { it.isData0() }
+        checker.test(getTestEntries(0, 42, 0, 0, 0))
+    }
+
+    @Test
+    fun canFailCheckChangingAssertions_ifStartingAssertionAlwaysPasses() {
+        val checker = AssertionsChecker<SimpleEntrySubject>()
+        checker.add("isData42") { it.isData42() }
+        checker.add("isData0") { it.isData0() }
+        try {
+            checker.test(getTestEntries(42, 42, 42, 42, 42))
+        } catch (failure: Throwable) {
+            require(failure is FlickerSubjectException) { "Unknown failure $failure" }
+            assertFailure(failure.cause)
+                .hasMessageThat()
+                .contains("Assertion never became false: isData42")
+        }
+    }
+
+    @Test
+    fun canFailCheckChangingAssertions_ifUsingCompoundAssertion() {
+        val checker = AssertionsChecker<SimpleEntrySubject>()
+        checker.add("isData42/0") { it.isData42().isData0() }
+        try {
+            checker.test(getTestEntries(0, 0, 0, 0, 0))
+        } catch (failure: Throwable) {
+            require(failure is FlickerSubjectException) { "Unknown failure $failure" }
+            assertFailure(failure.cause)
+                .factValue("expected").isEqualTo("42")
+            assertFailure(failure.cause)
+                .factValue("but was").isEqualTo("0")
+        }
+    }
+
+    private class SimpleEntrySubject(
+        failureMetadata: FailureMetadata,
+        private val entry: SimpleEntry
+    ) : FlickerSubject(failureMetadata, entry) {
+        override val defaultFacts: String = "SimpleEntry(${entry.mData})"
+        override fun clone(): FlickerSubject {
+            return SimpleEntrySubject(fm, entry)
+        }
+
+        fun isData42() = apply {
+            check("is42").that(entry.mData).isEqualTo(42)
+        }
+
+        fun isData0() = apply {
+            check("is0").that(entry.mData).isEqualTo(0)
+        }
+
+        companion object {
+            /**
+             * Boiler-plate Subject.Factory for LayersTraceSubject
+             */
+            private val FACTORY: Factory<Subject, SimpleEntry> =
+                Factory { fm, subject -> SimpleEntrySubject(fm, subject) }
+
+            /**
+             * User-defined entry point
+             */
+            @JvmStatic
+            fun assertThat(entry: SimpleEntry): SimpleEntrySubject {
+                val strategy = FlickerFailureStrategy()
+                val subject = StandardSubjectBuilder.forCustomFailureStrategy(strategy)
+                    .about(FACTORY)
+                    .that(entry) as SimpleEntrySubject
+                strategy.init(subject)
+                return subject
+            }
+        }
+    }
+
+    data class SimpleEntry(override val timestamp: Long, val mData: Int) : ITraceEntry
+
+    companion object {
+        /**
+         * Returns a list of SimpleEntry objects with `data` and incremental timestamps starting
+         * at 0.
+         */
+        private fun getTestEntries(vararg data: Int): List<SimpleEntrySubject> =
+                data.indices.map { SimpleEntrySubject
+                    .assertThat(SimpleEntry(it.toLong(), data[it])) }
+    }
+}
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/AssertionsTest.java b/libraries/flicker/test/src/com/android/server/wm/flicker/AssertionsTest.java
deleted file mode 100644
index b22532b..0000000
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/AssertionsTest.java
+++ /dev/null
@@ -1,64 +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;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import androidx.test.runner.AndroidJUnit4;
-import com.android.server.wm.flicker.Assertions.Result;
-import org.junit.FixMethodOrder;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.MethodSorters;
-
-/**
- * Contains {@link Assertions} tests. To run this test: {@code atest FlickerLibTest:AssertionsTest}
- */
-@RunWith(AndroidJUnit4.class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-public class AssertionsTest {
-    @Test
-    public void traceEntryAssertionCanNegateResult() {
-        Assertions.TraceAssertion<Integer> assertNumEquals42 = getIntegerTraceEntryAssertion();
-
-        assertThat(assertNumEquals42.apply(1).success).isFalse();
-        assertThat(assertNumEquals42.negate().apply(1).success).isTrue();
-
-        assertThat(assertNumEquals42.apply(42).success).isTrue();
-        assertThat(assertNumEquals42.negate().apply(42).success).isFalse();
-    }
-
-    @Test
-    public void resultCanBeNegated() {
-        String reason = "Everything is fine!";
-        Result result = new Result(true, 0, "TestAssert", reason);
-        Result negatedResult = result.negate();
-
-        assertThat(negatedResult.success).isFalse();
-        assertThat(negatedResult.reason).isEqualTo(reason);
-        assertThat(negatedResult.assertionName).isEqualTo("!TestAssert");
-    }
-
-    private Assertions.TraceAssertion<Integer> getIntegerTraceEntryAssertion() {
-        return (num) -> {
-            if (num == 42) {
-                return new Result(true, "Num equals 42");
-            }
-            return new Result(false, "Num doesn't equal 42, actual:" + num);
-        };
-    }
-}
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/EventLogSubjectTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/EventLogSubjectTest.kt
new file mode 100644
index 0000000..a894632
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/EventLogSubjectTest.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2021 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
+
+import com.android.server.wm.flicker.traces.eventlog.EventLogSubject
+import com.android.server.wm.flicker.traces.eventlog.FocusEvent
+import org.junit.Test
+
+/**
+ * Contains [EventLogSubject] tests. To run this test: `atest
+ * FlickerLibTest:EventLogSubjectTest`
+ */
+class EventLogSubjectTest {
+    @Test
+    fun canDetectFocusChanges() {
+        val builder = FlickerRunResult.Builder()
+        builder.eventLog =
+                listOf(FocusEvent(0, "WinB", FocusEvent.Focus.GAINED, "test"),
+                        FocusEvent(0, "test WinA window", FocusEvent.Focus.LOST, "test"),
+                        FocusEvent(0, "WinB", FocusEvent.Focus.LOST, "test"),
+                        FocusEvent(0, "test WinC", FocusEvent.Focus.GAINED, "test"))
+        val result = builder.buildEventLogResult().eventLogSubject
+        require(result != null) { "Event log subject was not built" }
+        result.focusChanges(arrayOf("WinA", "WinB", "WinC"))
+                .forAllEntries()
+        result.focusChanges(arrayOf("WinA", "WinB")).forAllEntries()
+        result.focusChanges(arrayOf("WinB", "WinC")).forAllEntries()
+        result.focusChanges(arrayOf("WinA")).forAllEntries()
+        result.focusChanges(arrayOf("WinB")).forAllEntries()
+        result.focusChanges(arrayOf("WinC")).forAllEntries()
+    }
+
+    @Test
+    fun canDetectFocusDoesNotChange() {
+        val result = FlickerRunResult.Builder().buildEventLogResult().eventLogSubject
+        require(result != null) { "Event log subject was not built" }
+        result.focusDoesNotChange().forAllEntries()
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/FlickerDSLTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/FlickerDSLTest.kt
new file mode 100644
index 0000000..dd8d15e
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/FlickerDSLTest.kt
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2021 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
+
+import android.app.Instrumentation
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.server.wm.flicker.assertions.AssertionData
+import com.android.server.wm.flicker.assertions.FlickerSubject
+import com.android.server.wm.flicker.dsl.AssertionTag
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.traces.eventlog.EventLogSubject
+import com.android.server.wm.flicker.traces.layers.LayerTraceEntrySubject
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerStateSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
+import com.google.common.truth.Truth
+import org.junit.Assert
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+import kotlin.reflect.KClass
+
+/**
+ * Contains [Flicker] and [FlickerBuilder] tests.
+ *
+ * To run this test: `atest FlickerLibTest:FlickerDSLTest`
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class FlickerDSLTest {
+    private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val TAG = "tag"
+    private var executed = false
+
+    private fun runFlicker(assertion: AssertionData) {
+        executed = false
+        val builder = FlickerBuilder(instrumentation)
+        builder.transitions {
+            withTag(TAG) {
+                device.pressHome()
+            }
+        }
+        val flicker = builder.build()
+        flicker.execute()
+        flicker.checkAssertion(assertion)
+        Truth.assertWithMessage("Assertion was not executed")
+            .that(executed)
+            .isTrue()
+    }
+
+    private fun validateAssertion(
+        assertion: AssertionData,
+        expectedSubjectClass: KClass<out FlickerSubject>,
+        expectedTag: String
+    ) {
+        Truth.assertWithMessage("Unexpected subject type")
+            .that(assertion.expectedSubjectClass)
+            .isEqualTo(expectedSubjectClass)
+        Truth.assertWithMessage("Unexpected tag")
+            .that(assertion.tag)
+            .isEqualTo(expectedTag)
+    }
+
+    @Test
+    fun checkBuiltWMStartAssertion() {
+        val assertion = FlickerTestParameter.buildWmStartAssertion { executed = true }
+        validateAssertion(assertion, WindowManagerStateSubject::class, AssertionTag.START)
+        runFlicker(assertion)
+    }
+
+    @Test
+    fun checkBuiltWMEndAssertion() {
+        val assertion = FlickerTestParameter.buildWmEndAssertion { executed = true }
+        validateAssertion(assertion, WindowManagerStateSubject::class, AssertionTag.END)
+        runFlicker(assertion)
+    }
+
+    @Test
+    fun checkBuiltWMAssertion() {
+        val assertion = FlickerTestParameter.buildWMAssertion { executed = true }
+        validateAssertion(assertion, WindowManagerTraceSubject::class, AssertionTag.ALL)
+        runFlicker(assertion)
+    }
+
+    @Test
+    fun checkBuiltWMTagAssertion() {
+        val assertion = FlickerTestParameter.buildWMTagAssertion(TAG) { executed = true }
+        validateAssertion(assertion, WindowManagerStateSubject::class, TAG)
+        runFlicker(assertion)
+    }
+
+    @Test
+    fun checkBuiltLayersStartAssertion() {
+        val assertion = FlickerTestParameter.buildLayersStartAssertion { executed = true }
+        validateAssertion(assertion, LayerTraceEntrySubject::class, AssertionTag.START)
+        runFlicker(assertion)
+    }
+
+    @Test
+    fun checkBuiltLayersEndAssertion() {
+        val assertion = FlickerTestParameter.buildLayersEndAssertion { executed = true }
+        validateAssertion(assertion, LayerTraceEntrySubject::class, AssertionTag.END)
+        runFlicker(assertion)
+    }
+
+    @Test
+    fun checkBuiltLayersAssertion() {
+        val assertion = FlickerTestParameter.buildLayersAssertion { executed = true }
+        validateAssertion(assertion, LayersTraceSubject::class, AssertionTag.ALL)
+        runFlicker(assertion)
+    }
+
+    @Test
+    fun checkBuiltLayersTagAssertion() {
+        val assertion = FlickerTestParameter.buildLayersTagAssertion(TAG) { executed = true }
+        validateAssertion(assertion, LayerTraceEntrySubject::class, TAG)
+        runFlicker(assertion)
+    }
+
+    @Test
+    fun checkBuiltEventLogAssertion() {
+        val assertion = FlickerTestParameter.buildEventLogAssertion { executed = true }
+        validateAssertion(assertion, EventLogSubject::class, AssertionTag.ALL)
+        runFlicker(assertion)
+    }
+
+    @Test
+    fun supportDuplicatedTag() {
+        var count = 0
+        val assertion = FlickerTestParameter.buildWMTagAssertion(TAG) {
+            count++
+        }
+
+        val builder = FlickerBuilder(instrumentation).apply {
+            transitions {
+                this.createTag(TAG)
+                this.withTag(TAG) {
+                    this.device.pressHome()
+                }
+            }
+        }
+        val flicker = builder.build().execute()
+
+        flicker.checkAssertion(assertion)
+
+        Truth.assertWithMessage("Should have asserted $TAG 2x")
+            .that(count)
+            .isEqualTo(2)
+    }
+
+    @Test
+    fun preventInvalidTagNames() {
+        try {
+            val builder = FlickerBuilder(instrumentation).apply {
+                transitions {
+                    this.createTag("inv lid")
+                }
+            }
+            builder.build().execute()
+            Assert.fail("Should not have allowed invalid tag name")
+        } catch (e: Exception) {
+            Truth.assertWithMessage("Did not validate tag name")
+                .that(e.cause?.message)
+                .contains("The test tag inv lid can not contain spaces")
+        }
+    }
+
+    @Test
+    fun assertCreatedTags() {
+        val builder = FlickerBuilder(instrumentation).apply {
+            transitions {
+                this.createTag(TAG)
+                device.pressHome()
+            }
+        }
+        val flicker = builder.build()
+        val passAssertion = FlickerTestParameter.buildWMTagAssertion(TAG) {
+            this.isNotEmpty()
+        }
+        val ignoredAssertion = FlickerTestParameter.buildWMTagAssertion("invalid") {
+            fail("`Invalid` tag was not created, so it should not " +
+                "have been asserted")
+        }
+        flicker.checkAssertion(passAssertion)
+        flicker.checkAssertion(ignoredAssertion)
+    }
+
+    @Test
+    fun detectEmptyResults() {
+        try {
+            FlickerBuilder(instrumentation).build().execute()
+            Assert.fail("Should not have allowed empty transition")
+        } catch (e: Exception) {
+            Truth.assertWithMessage("Flicker did not warn of empty transitions")
+                .that(e.message)
+                .contains("A flicker test must include transitions to run")
+        }
+    }
+
+    @Test
+    fun detectCrashedTransition() {
+        val exceptionMessage = "Crashed transition"
+        val builder = FlickerBuilder(instrumentation)
+        builder.transitions { error("Crashed transition") }
+        val flicker = builder.build()
+        try {
+            flicker.execute()
+            Assert.fail("Should have raised an exception with message $exceptionMessage")
+        } catch (e: Throwable) {
+            Truth.assertWithMessage("Incorrect exception message")
+                .that(e.message)
+                .contains("Unable to execute transition")
+            Truth.assertWithMessage("Test exception does not contain original crash message")
+                .that(e.cause?.message)
+                .contains(exceptionMessage)
+        }
+    }
+}
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/FlickerTestParameterFactoryTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/FlickerTestParameterFactoryTest.kt
new file mode 100644
index 0000000..4ce608e
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/FlickerTestParameterFactoryTest.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2021 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
+
+import android.app.Instrumentation
+import android.view.Surface
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+
+/**
+ * Contains [FlickerTestParameterFactory] tests.
+ *
+ * To run this test: `atest FlickerLibTest:FlickerTestFactoryRunnerTest`
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class FlickerTestParameterFactoryTest {
+    private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val defaultRotations = listOf(Surface.ROTATION_0, Surface.ROTATION_90)
+    private val testFactory = FlickerTestParameterFactory.getInstance()
+
+    private fun FlickerBuilder.setDefaultTestCfg(cfg: Map<String, Any?>) = apply {
+        withTestName { "${cfg.startRotationName}_${cfg.endRotationName}_" }
+    }
+
+    private fun validateRotationTest(
+        actual: Map<String, Any?>,
+        rotations: List<Int> = defaultRotations
+    ) {
+        assertWithMessage("Rotation tests should not have the same start and end rotation")
+            .that(actual.startRotation).isNotEqualTo(actual.endRotation)
+        assertWithMessage("Invalid start rotation value ${actual.startRotation}")
+            .that(actual.startRotation).isIn(rotations)
+        assertWithMessage("Invalid end rotation value ${actual.endRotation}")
+            .that(actual.endRotation).isIn(rotations)
+    }
+
+    private fun validateTest(
+        actual: Map<String, Any?>,
+        rotations: List<Int> = defaultRotations
+    ) {
+        assertWithMessage("Tests should have the same start and end rotation")
+            .that(actual.startRotation).isEqualTo(actual.endRotation)
+        assertWithMessage("Invalid rotation value ${actual.startRotation}")
+            .that(actual.startRotation).isIn(rotations)
+    }
+
+    @Test
+    fun checkBuildTest() {
+        val actual = testFactory.getConfigNonRotationTests()
+        assertWithMessage("Flicker should create tests for 0 and 90 degrees")
+            .that(actual).hasSize(4)
+    }
+
+    @Test
+    fun checkBuildRotationTest() {
+        val actual = testFactory.getConfigRotationTests()
+        assertWithMessage("Flicker should create tests for 0 and 90 degrees")
+            .that(actual).hasSize(4)
+    }
+
+    @Test
+    fun checkBuildCustomRotationsTest() {
+        val rotations = listOf(Surface.ROTATION_0, Surface.ROTATION_90, Surface.ROTATION_180,
+                Surface.ROTATION_270)
+        val actual = testFactory.getConfigRotationTests(
+            supportedRotations = rotations)
+        // Should have config for each rotation pair
+        assertWithMessage("Flicker should create tests for 0/90/180/270 degrees")
+            .that(actual).hasSize(24)
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/LayerSubjectTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/LayerSubjectTest.kt
new file mode 100644
index 0000000..cb99b65
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/LayerSubjectTest.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2021 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
+
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject.Companion.assertThat
+import com.android.server.wm.traces.common.Bounds
+import com.google.common.truth.Truth
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+
+/**
+ * Contains [LayerSubject] tests. To run this test:
+ * `atest FlickerLibTest:LayerSubjectTest`
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class LayerSubjectTest {
+    @Test
+    fun exceptionContainsDebugInfo() {
+        val layersTraceEntries = readLayerTraceFromFile("layers_trace_emptyregion.pb")
+        val error = assertThrows(AssertionError::class.java) {
+            assertThat(layersTraceEntries)
+                .first()
+                .layer("ImaginaryLayer", 0)
+                .exists()
+        }
+        Truth.assertThat(error).hasMessageThat().contains("Trace:")
+        Truth.assertThat(error).hasMessageThat().contains("Path: ")
+        Truth.assertThat(error).hasMessageThat().contains("Entry:")
+        Truth.assertThat(error).hasMessageThat().contains("Frame:")
+        Truth.assertThat(error).hasMessageThat().contains("Layer:")
+    }
+
+    @Test
+    fun canTestAssertionsOnLayer() {
+        val layersTraceEntries = readLayerTraceFromFile("layers_trace_emptyregion.pb")
+        assertThat(layersTraceEntries)
+            .layer("SoundVizWallpaperV2", 26033)
+            .hasBufferSize(Bounds(1440, 2960))
+            .hasScalingMode(0)
+
+        assertThat(layersTraceEntries)
+            .layer("DoesntExist", 1)
+            .doesNotExist()
+    }
+}
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/LayerTraceEntrySubjectTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/LayerTraceEntrySubjectTest.kt
new file mode 100644
index 0000000..76375b8
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/LayerTraceEntrySubjectTest.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2021 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
+
+import android.graphics.Region
+import com.android.server.wm.flicker.traces.layers.LayerTraceEntrySubject
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.google.common.truth.Truth
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+
+/**
+ * Contains [LayerTraceEntrySubject] tests. To run this test: `atest
+ * FlickerLibTest:LayersTraceTest`
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class LayerTraceEntrySubjectTest {
+    @Test
+    fun exceptionContainsDebugInfo() {
+        val layersTraceEntries = readLayerTraceFromFile("layers_trace_emptyregion.pb")
+        val error = assertThrows(AssertionError::class.java) {
+            LayersTraceSubject.assertThat(layersTraceEntries)
+                .first()
+                .contains("ImaginaryLayer")
+        }
+        Truth.assertThat(error).hasMessageThat().contains("Trace:")
+        Truth.assertThat(error).hasMessageThat().contains("Path: ")
+        Truth.assertThat(error).hasMessageThat().contains("Entry:")
+    }
+
+    @Test
+    fun testCanInspectBeginning() {
+        val layersTraceEntries = readLayerTraceFromFile("layers_trace_launch_split_screen.pb")
+        LayerTraceEntrySubject.assertThat(layersTraceEntries.entries.first())
+            .isVisible("NavigationBar0#0")
+            .notContains("DockedStackDivider#0")
+            .isVisible("NexusLauncherActivity#0")
+    }
+
+    @Test
+    fun testCanInspectEnd() {
+        val layersTraceEntries = readLayerTraceFromFile("layers_trace_launch_split_screen.pb")
+        LayerTraceEntrySubject.assertThat(layersTraceEntries.entries.last())
+            .isVisible("NavigationBar0#0")
+            .isVisible("DockedStackDivider#0")
+    }
+
+    // b/75276931
+    @Test
+    fun canDetectUncoveredRegion() {
+        val trace = readLayerTraceFromFile("layers_trace_emptyregion.pb")
+        val expectedRegion = Region(0, 0, 1440, 2960)
+        val error = assertThrows(AssertionError::class.java) {
+            LayersTraceSubject.assertThat(trace).entry(935346112030)
+                .visibleRegion()
+                .coversAtLeast(expectedRegion)
+        }
+        assertFailure(error)
+            .factValue("Region to test")
+            .contains("SkRegion((0,0,1440,2960))")
+
+        assertFailure(error)
+            .factValue("Uncovered region")
+            .contains("SkRegion((0,1440,1440,2960))")
+    }
+
+    // Visible region tests
+    @Test
+    fun canTestLayerVisibleRegion_layerDoesNotExist() {
+        val imaginaryLayer = "ImaginaryLayer"
+        val trace = readLayerTraceFromFile("layers_trace_emptyregion.pb")
+        val expectedVisibleRegion = Region(0, 0, 1, 1)
+        val error = assertThrows(AssertionError::class.java) {
+            LayersTraceSubject.assertThat(trace).entry(937229257165)
+                .visibleRegion(imaginaryLayer)
+                .coversExactly(expectedVisibleRegion)
+        }
+        assertFailure(error)
+            .factValue("Could not find")
+            .isEqualTo(imaginaryLayer)
+    }
+
+    @Test
+    fun canTestLayerVisibleRegion_layerDoesNotHaveExpectedVisibleRegion() {
+        val trace = readLayerTraceFromFile("layers_trace_emptyregion.pb")
+        val expectedVisibleRegion = Region(0, 0, 1, 1)
+        val error = assertThrows(AssertionError::class.java) {
+            LayersTraceSubject.assertThat(trace).entry(937126074082)
+                .visibleRegion("DockedStackDivider#0")
+                .coversExactly(expectedVisibleRegion)
+        }
+        assertFailure(error)
+            .factValue("Covered region")
+            .contains("SkRegion()")
+    }
+
+    @Test
+    fun canTestLayerVisibleRegion_layerIsHiddenByParent() {
+        val trace = readLayerTraceFromFile("layers_trace_emptyregion.pb")
+        val expectedVisibleRegion = Region(0, 0, 1, 1)
+        val error = assertThrows(AssertionError::class.java) {
+            LayersTraceSubject.assertThat(trace).entry(935346112030)
+                .visibleRegion("SimpleActivity#0")
+                .coversExactly(expectedVisibleRegion)
+        }
+        assertFailure(error)
+            .factValue("Covered region")
+            .contains("SkRegion()")
+    }
+
+    @Test
+    fun canTestLayerVisibleRegion_incorrectRegionSize() {
+        val trace = readLayerTraceFromFile("layers_trace_emptyregion.pb")
+        val expectedVisibleRegion = Region(0, 0, 1440, 99)
+        val error = assertThrows(AssertionError::class.java) {
+            LayersTraceSubject.assertThat(trace).entry(937126074082)
+                .visibleRegion("StatusBar")
+                .coversExactly(expectedVisibleRegion)
+        }
+        assertFailure(error)
+            .factValue("Region to test")
+            .contains("SkRegion((0,0,1440,99))")
+    }
+
+    @Test
+    fun canTestLayerVisibleRegion() {
+        val trace = readLayerTraceFromFile("layers_trace_launch_split_screen.pb")
+        val expectedVisibleRegion = Region(0, 0, 1080, 145)
+        LayersTraceSubject.assertThat(trace).entry(90480846872160)
+            .visibleRegion("StatusBar")
+            .coversExactly(expectedVisibleRegion)
+    }
+
+    @Test
+    fun canTestLayerVisibleRegion_layerIsNotVisible() {
+        val trace = readLayerTraceFromFile("layers_trace_invalid_layer_visibility.pb")
+        val error = assertThrows(AssertionError::class.java) {
+            LayersTraceSubject.assertThat(trace).entry(252794268378458)
+                .isVisible("com.android.server.wm.flicker.testapp")
+        }
+        assertFailure(error)
+            .factValue("Is Invisible")
+            .contains("Bounds is 0x0")
+    }
+
+    @Test
+    fun testCanParseWithoutHWC_visibleRegion() {
+        val layersTrace = readLayerTraceFromFile("layers_trace_no_hwc_composition.pb")
+        val entry = LayersTraceSubject.assertThat(layersTrace)
+            .entry(238517209878020)
+
+        entry.visibleRegion(useCompositionEngineRegionOnly = false)
+            .coversExactly(Region(0, 0, 1440, 2960))
+
+        entry.visibleRegion("InputMethod#0", useCompositionEngineRegionOnly = false)
+            .coversExactly(Region(0, 171, 1440, 2960))
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/LayersTraceEntryTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/LayersTraceEntryTest.kt
new file mode 100644
index 0000000..616fc47
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/LayersTraceEntryTest.kt
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2021 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
+
+import com.android.server.wm.traces.common.layers.LayerTraceEntry
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject.Companion.assertThat
+import com.google.common.truth.Truth
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+import kotlin.AssertionError
+
+/**
+ * Contains [LayerTraceEntry] tests. To run this test: `atest
+ * FlickerLibTest:LayersTraceTest`
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class LayersTraceEntryTest {
+    @Test
+    fun exceptionContainsDebugInfo() {
+        val layersTraceEntries = readLayerTraceFromFile("layers_trace_emptyregion.pb")
+        val error = assertThrows(AssertionError::class.java) {
+            assertThat(layersTraceEntries)
+                .first()
+                .contains("ImaginaryLayer")
+        }
+        Truth.assertThat(error).hasMessageThat().contains("Trace:")
+        Truth.assertThat(error).hasMessageThat().contains("Path: ")
+        Truth.assertThat(error).hasMessageThat().contains("Entry:")
+    }
+
+    @Test
+    fun canParseAllLayers() {
+        val trace = readLayerTraceFromFile("layers_trace_emptyregion.pb")
+        Truth.assertThat(trace.entries).isNotEmpty()
+        Truth.assertThat(trace.first().timestamp).isEqualTo(922839428857)
+        Truth.assertThat(trace.last().timestamp).isEqualTo(941432656959)
+        Truth.assertThat(trace.last().flattenedLayers).asList().hasSize(57)
+    }
+
+    @Test
+    fun canParseVisibleLayersLauncher() {
+        val trace = readLayerTraceFromFile("layers_trace_launch_split_screen.pb")
+        val visibleLayers = trace.getEntry(90480846872160).visibleLayers
+        val msg = "Visible Layers:\n" + visibleLayers.joinToString("\n") { "\t" + it.name }
+        Truth.assertWithMessage(msg).that(visibleLayers).asList().hasSize(6)
+        Truth.assertThat(msg).contains("ScreenDecorOverlay#0")
+        Truth.assertThat(msg).contains("ScreenDecorOverlayBottom#0")
+        Truth.assertThat(msg).contains("NavigationBar0#0")
+        Truth.assertThat(msg).contains("ImageWallpaper#0")
+        Truth.assertThat(msg).contains("StatusBar#0")
+        Truth.assertThat(msg).contains("NexusLauncherActivity#0")
+    }
+
+    @Test
+    fun canParseVisibleLayersSplitScreen() {
+        val trace = readLayerTraceFromFile("layers_trace_launch_split_screen.pb")
+        val visibleLayers = trace.getEntry(90493757372977).visibleLayers
+        val msg = "Visible Layers:\n" + visibleLayers.joinToString("\n") { "\t" + it.name }
+        Truth.assertWithMessage(msg).that(visibleLayers).asList().hasSize(7)
+        Truth.assertThat(msg).contains("ScreenDecorOverlayBottom#0")
+        Truth.assertThat(msg).contains("ScreenDecorOverlay#0")
+        Truth.assertThat(msg).contains("NavigationBar0#0")
+        Truth.assertThat(msg).contains("StatusBar#0")
+        Truth.assertThat(msg).contains("DockedStackDivider#0")
+        Truth.assertThat(msg).contains("ConversationListActivity#0")
+        Truth.assertThat(msg).contains("GoogleDialtactsActivity#0")
+    }
+
+    @Test
+    fun canParseVisibleLayersInTransition() {
+        val trace = readLayerTraceFromFile("layers_trace_launch_split_screen.pb")
+        val visibleLayers = trace.getEntry(90488463619533).visibleLayers
+        val msg = "Visible Layers:\n" + visibleLayers.joinToString("\n") { "\t" + it.name }
+        Truth.assertWithMessage(msg).that(visibleLayers).asList().hasSize(10)
+        Truth.assertThat(msg).contains("ScreenDecorOverlayBottom#0")
+        Truth.assertThat(msg).contains("ScreenDecorOverlay#0")
+        Truth.assertThat(msg).contains("NavigationBar0#0")
+        Truth.assertThat(msg).contains("StatusBar#0")
+        Truth.assertThat(msg).contains("DockedStackDivider#0")
+        Truth.assertThat(msg).contains("SnapshotStartingWindow for taskId=21 - " +
+            "task-snapshot-surface#0")
+        Truth.assertThat(msg).contains("SnapshotStartingWindow for taskId=21")
+        Truth.assertThat(msg).contains("NexusLauncherActivity#0")
+        Truth.assertThat(msg).contains("ImageWallpaper#0")
+        Truth.assertThat(msg).contains("ConversationListActivity#0")
+    }
+
+    @Test
+    fun canParseLayerHierarchy() {
+        val trace = readLayerTraceFromFile("layers_trace_emptyregion.pb")
+        Truth.assertThat(trace.entries).isNotEmpty()
+        Truth.assertThat(trace.entries.first().timestamp).isEqualTo(922839428857)
+        Truth.assertThat(trace.entries.last().timestamp).isEqualTo(941432656959)
+        Truth.assertThat(trace.entries.first().flattenedLayers).asList().hasSize(57)
+        val layers = trace.entries.first().rootLayers
+        Truth.assertThat(layers[0].children).hasSize(3)
+        Truth.assertThat(layers[1].children).isEmpty()
+    }
+
+    // b/76099859
+    @Test
+    fun canDetectOrphanLayers() {
+        try {
+            readLayerTraceFromFile("layers_trace_orphanlayers.pb", ignoreOrphanLayers = false)
+            error("Failed to detect orphaned layers.")
+        } catch (exception: RuntimeException) {
+            Truth.assertThat(exception.message)
+                .contains("Failed to parse layers trace. Found orphan layer with id = 49 with" +
+                    " parentId = 1006")
+        }
+    }
+
+    @Test
+    fun testCanParseNonCroppedLayerWithHWC() {
+        val layerName = "BackColorSurface#0"
+        val layersTrace = readLayerTraceFromFile("layers_trace_backcolorsurface.pb")
+        val entry = layersTrace.getEntry(131954021476)
+        Truth.assertWithMessage("$layerName should not be visible")
+            .that(entry.visibleLayers.map { it.name })
+            .doesNotContain(layerName)
+        val layer = entry.flattenedLayers.first { it.name == layerName }
+        Truth.assertWithMessage("$layerName should be invisible because of HWC region")
+            .that(layer.visibilityReason)
+            .contains("Visible region calculated by Composition Engine is empty")
+    }
+
+    @Test
+    fun testCanParseNonCroppedLayerWithoutHWC() {
+        val layersTrace = readLayerTraceFromFile("layers_trace_no_hwc_composition.pb")
+        val entry = layersTrace.getEntry(238517209878020)
+        Truth.assertWithMessage("IME should be visible")
+            .that(entry.visibleLayers.map { it.name })
+            .contains("InputMethod#0")
+
+        val messagesApp = "com.google.android.apps.messaging/" +
+            "com.google.android.apps.messaging.ui.ConversationListActivity#0"
+        Truth.assertWithMessage("Messages app should not be visible")
+            .that(entry.visibleLayers.map { it.name })
+            .doesNotContain(messagesApp)
+
+        Truth.assertWithMessage("Should have visible layers in all trace entries")
+            .that(entry.flattenedLayers.map { it.name })
+            .doesNotContain(messagesApp)
+    }
+}
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
deleted file mode 100644
index 2ccae42..0000000
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/LayersTraceSubjectTest.java
+++ /dev/null
@@ -1,140 +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;
-
-import static com.android.server.wm.flicker.LayersTraceSubject.assertThat;
-import static com.android.server.wm.flicker.TestFileUtils.readTestFile;
-
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import static org.junit.Assert.fail;
-
-import android.graphics.Rect;
-
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.FixMethodOrder;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-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
- * FlickerLibTest:LayersTraceSubjectTest}
- */
-@RunWith(AndroidJUnit4.class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-public class LayersTraceSubjectTest {
-    private static final Rect displayRect = new Rect(0, 0, 1440, 2880);
-
-    private static LayersTrace readLayerTraceFromFile(String relativePath) {
-        try {
-            return LayersTrace.parseFrom(readTestFile(relativePath), Paths.get(relativePath));
-        } catch (Exception e) {
-            throw new RuntimeException(e);
-        }
-    }
-
-    @Test
-    public void testCanDetectEmptyRegionFromLayerTrace() {
-        LayersTrace layersTraceEntries = readLayerTraceFromFile("layers_trace_emptyregion.pb");
-        try {
-            assertThat(layersTraceEntries).coversRegion(displayRect).forAllEntries();
-            fail("Assertion passed");
-        } catch (AssertionError e) {
-            assertWithMessage("Contains path to trace")
-                    .that(e.getMessage())
-                    .contains("layers_trace_emptyregion.pb");
-            assertWithMessage("Contains timestamp").that(e.getMessage()).contains("0h38m28s8ms");
-            assertWithMessage("Contains assertion function")
-                    .that(e.getMessage())
-                    .contains("coversRegion");
-            assertWithMessage("Contains debug info")
-                    .that(e.getMessage())
-                    .contains("Region to test: " + displayRect);
-            assertWithMessage("Contains debug info")
-                    .that(e.getMessage())
-                    .contains("first empty point: 0, 99");
-        }
-    }
-
-    @Test
-    public void testCanDetectIncorrectVisibilityFromLayerTrace() {
-        LayersTrace layersTraceEntries =
-                readLayerTraceFromFile("layers_trace_invalid_layer_visibility.pb");
-        try {
-            assertThat(layersTraceEntries)
-                    .showsLayer("com.android.server.wm.flicker.testapp")
-                    .then()
-                    .hidesLayer("com.android.server.wm.flicker.testapp")
-                    .forAllEntries();
-            fail("Assertion passed");
-        } catch (AssertionError e) {
-            assertWithMessage("Contains path to trace")
-                    .that(e.getMessage())
-                    .contains("layers_trace_invalid_layer_visibility.pb");
-            assertWithMessage("Contains timestamp")
-                    .that(e.getMessage())
-                    .contains("2d22h13m14s303ms");
-            assertWithMessage("Contains assertion function")
-                    .that(e.getMessage())
-                    .contains("!isVisible");
-            assertWithMessage("Contains debug info")
-                    .that(e.getMessage())
-                    .contains(
-                            "com.android.server.wm.flicker.testapp/com.android.server.wm.flicker.testapp"
-                                    + ".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/LayersTraceSubjectTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/LayersTraceSubjectTest.kt
new file mode 100644
index 0000000..d47820e
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/LayersTraceSubjectTest.kt
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2021 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
+
+import android.graphics.Region
+import androidx.test.filters.FlakyTest
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject.Companion.assertThat
+import com.android.server.wm.traces.common.layers.LayersTrace
+import com.google.common.truth.Truth
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+
+/**
+ * Contains [LayersTraceSubject] tests. To run this test: `atest
+ * FlickerLibTest:LayersTraceSubjectTest`
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class LayersTraceSubjectTest {
+    @Test
+    fun exceptionContainsDebugInfo() {
+        val layersTraceEntries = readLayerTraceFromFile("layers_trace_launch_split_screen.pb")
+        val error = assertThrows(AssertionError::class.java) {
+            assertThat(layersTraceEntries)
+                .isEmpty()
+        }
+        Truth.assertThat(error).hasMessageThat().contains("Trace:")
+        Truth.assertThat(error).hasMessageThat().contains("Path: ")
+        Truth.assertThat(error).hasMessageThat().contains("Start:")
+        Truth.assertThat(error).hasMessageThat().contains("End:")
+    }
+
+    @Test
+    fun testCanDetectEmptyRegionFromLayerTrace() {
+        val layersTraceEntries = readLayerTraceFromFile("layers_trace_emptyregion.pb")
+        try {
+            assertThat(layersTraceEntries)
+                .coversAtLeast(DISPLAY_REGION)
+                .forAllEntries()
+            error("Assertion should not have passed")
+        } catch (e: Throwable) {
+            assertFailure(e).factValue("Region to test").contains(DISPLAY_REGION.toString())
+            assertFailure(e).factValue("Uncovered region").contains("SkRegion((0,1440,1440,2880))")
+        }
+    }
+
+    @Test
+    fun testCanInspectBeginning() {
+        val layersTraceEntries = readLayerTraceFromFile("layers_trace_launch_split_screen.pb")
+        assertThat(layersTraceEntries)
+            .first()
+            .isVisible("NavigationBar0#0")
+            .notContains("DockedStackDivider#0")
+            .isVisible("NexusLauncherActivity#0")
+    }
+
+    @Test
+    fun testCanInspectEnd() {
+        val layersTraceEntries = readLayerTraceFromFile("layers_trace_launch_split_screen.pb")
+        assertThat(layersTraceEntries)
+            .last()
+            .isVisible("NavigationBar0#0")
+            .isVisible("DockedStackDivider#0")
+    }
+
+    @Test
+    fun testCanDetectChangingAssertions() {
+        val layersTraceEntries = readLayerTraceFromFile("layers_trace_launch_split_screen.pb")
+        assertThat(layersTraceEntries)
+            .isVisible("NavigationBar0#0")
+            .notContains("DockedStackDivider#0")
+            .then()
+            .isVisible("NavigationBar0#0")
+            .isInvisible("DockedStackDivider#0")
+            .then()
+            .isVisible("NavigationBar0#0")
+            .isVisible("DockedStackDivider#0")
+            .forAllEntries()
+    }
+
+    @FlakyTest
+    @Test
+    fun testCanDetectIncorrectVisibilityFromLayerTrace() {
+        val layersTraceEntries = readLayerTraceFromFile("layers_trace_invalid_layer_visibility.pb")
+        val error = assertThrows(AssertionError::class.java) {
+            assertThat(layersTraceEntries)
+                .isVisible("com.android.server.wm.flicker.testapp")
+                .then()
+                .isInvisible("com.android.server.wm.flicker.testapp")
+                .forAllEntries()
+        }
+
+        assertFailure(error)
+            .hasMessageThat()
+            .contains("layers_trace_invalid_layer_visibility.pb")
+        assertFailure(error)
+            .hasMessageThat()
+            .contains("2d22h13m14s303ms")
+        assertFailure(error)
+            .hasMessageThat()
+            .contains("!isVisible")
+        assertFailure(error)
+            .hasMessageThat()
+            .contains("com.android.server.wm.flicker.testapp/" +
+                "com.android.server.wm.flicker.testapp.SimpleActivity#0 is visible")
+    }
+
+    @Test
+    fun testCanDetectInvalidVisibleLayerForMoreThanOneConsecutiveEntry() {
+        val layersTraceEntries = readLayerTraceFromFile("layers_trace_invalid_visible_layers.pb")
+        val error = assertThrows(AssertionError::class.java) {
+            assertThat(layersTraceEntries)
+                .visibleLayersShownMoreThanOneConsecutiveEntry()
+                .forAllEntries()
+            error("Assertion should not have passed")
+        }
+
+        Truth.assertThat(error).hasMessageThat().contains("2d18h35m56s397ms")
+        assertFailure(error)
+            .hasMessageThat()
+            .contains("StatusBar#0")
+        assertFailure(error)
+            .hasMessageThat()
+            .contains("is not visible for 2 entries")
+    }
+
+    private fun testCanDetectVisibleLayersMoreThanOneConsecutiveEntry(trace: LayersTrace) {
+        assertThat(trace)
+            .visibleLayersShownMoreThanOneConsecutiveEntry()
+            .forAllEntries()
+    }
+
+    @Test
+    fun testCanDetectVisibleLayersMoreThanOneConsecutiveEntry() {
+        testCanDetectVisibleLayersMoreThanOneConsecutiveEntry(
+            readLayerTraceFromFile("layers_trace_snapshot_visible.pb"))
+    }
+
+    @Test
+    fun testCanIgnoreLayerEqualNameInVisibleLayersMoreThanOneConsecutiveEntry() {
+        val layersTraceEntries = readLayerTraceFromFile(
+                "layers_trace_invalid_visible_layers.pb")
+        assertThat(layersTraceEntries)
+                .visibleLayersShownMoreThanOneConsecutiveEntry(listOf("StatusBar#0"))
+                .forAllEntries()
+    }
+
+    @Test
+    fun testCanIgnoreLayerShorterNameInVisibleLayersMoreThanOneConsecutiveEntry() {
+        val layersTraceEntries = readLayerTraceFromFile(
+                "one_visible_layer_launcher_trace.pb")
+        assertThat(layersTraceEntries)
+                .visibleLayersShownMoreThanOneConsecutiveEntry(listOf("Launcher"))
+                .forAllEntries()
+    }
+
+    private fun detectRootLayer(fileName: String) {
+        val layersTrace = readLayerTraceFromFile(fileName)
+        for (entry in layersTrace.entries) {
+            val rootLayers = entry.rootLayers
+            Truth.assertWithMessage("Does not have any root layer")
+                    .that(rootLayers.size)
+                    .isGreaterThan(0)
+            val firstParentId = rootLayers.first().parentId
+            Truth.assertWithMessage("Has multiple root layers")
+                    .that(rootLayers.all { it.parentId == firstParentId })
+                    .isTrue()
+        }
+    }
+
+    @Test
+    fun testCanDetectRootLayer() {
+        detectRootLayer("layers_trace_root.pb")
+    }
+
+    @Test
+    fun testCanDetectRootLayerAOSP() {
+        detectRootLayer("layers_trace_root_aosp.pb")
+    }
+
+    companion object {
+        private val DISPLAY_REGION = Region(0, 0, 1440, 2880)
+    }
+}
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/LayersTraceTest.java b/libraries/flicker/test/src/com/android/server/wm/flicker/LayersTraceTest.java
deleted file mode 100644
index 8900623..0000000
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/LayersTraceTest.java
+++ /dev/null
@@ -1,240 +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;
-
-import static com.android.server.wm.flicker.TestFileUtils.readTestFile;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import static org.junit.Assert.fail;
-
-import android.content.Context;
-import android.graphics.Point;
-import android.graphics.Rect;
-import android.view.WindowManager;
-
-import androidx.test.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.FixMethodOrder;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.MethodSorters;
-
-import java.util.List;
-import java.util.stream.Collectors;
-
-/**
- * Contains {@link LayersTrace} tests. To run this test: {@code atest
- * FlickerLibTest:LayersTraceTest}
- */
-@RunWith(AndroidJUnit4.class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-public class LayersTraceTest {
-    private static LayersTrace readLayerTraceFromFile(String relativePath) {
-        try {
-            return LayersTrace.parseFrom(readTestFile(relativePath));
-        } catch (Exception e) {
-            throw new RuntimeException(e);
-        }
-    }
-
-    private static Rect getDisplayBounds() {
-        Point display = new Point();
-        WindowManager wm =
-                (WindowManager)
-                        InstrumentationRegistry.getContext()
-                                .getSystemService(Context.WINDOW_SERVICE);
-        wm.getDefaultDisplay().getRealSize(display);
-        return new Rect(0, 0, display.x, display.y);
-    }
-
-    @Test
-    public void canParseAllLayers() {
-        LayersTrace trace = readLayerTraceFromFile("layers_trace_emptyregion.pb");
-        assertThat(trace.getEntries()).isNotEmpty();
-        assertThat(trace.getEntries().get(0).getTimestamp()).isEqualTo(2307984557311L);
-        assertThat(trace.getEntries().get(trace.getEntries().size() - 1).getTimestamp())
-                .isEqualTo(2308521813510L);
-        List<LayersTrace.Layer> flattenedLayers = trace.getEntries().get(0).asFlattenedLayers();
-        String msg =
-                "Layers:\n"
-                        + flattenedLayers
-                                .stream()
-                                .map(layer -> layer.mProto.name)
-                                .collect(Collectors.joining("\n\t"));
-        assertWithMessage(msg).that(flattenedLayers).hasSize(47);
-    }
-
-    @Test
-    public void canParseVisibleLayers() {
-        LayersTrace trace = readLayerTraceFromFile("layers_trace_emptyregion.pb");
-        assertThat(trace.getEntries()).isNotEmpty();
-        assertThat(trace.getEntries().get(0).getTimestamp()).isEqualTo(2307984557311L);
-        assertThat(trace.getEntries().get(trace.getEntries().size() - 1).getTimestamp())
-                .isEqualTo(2308521813510L);
-        List<LayersTrace.Layer> flattenedLayers = trace.getEntries().get(0).asFlattenedLayers();
-        List<LayersTrace.Layer> visibleLayers =
-                flattenedLayers
-                        .stream()
-                        .filter(layer -> layer.isVisible() && !layer.isHiddenByParent())
-                        .collect(Collectors.toList());
-
-        String msg =
-                "Visible Layers:\n"
-                        + visibleLayers
-                                .stream()
-                                .map(layer -> layer.mProto.name)
-                                .collect(Collectors.joining("\n\t"));
-
-        assertWithMessage(msg).that(visibleLayers).hasSize(9);
-    }
-
-    @Test
-    public void canParseLayerHierarchy() {
-        LayersTrace trace = readLayerTraceFromFile("layers_trace_emptyregion.pb");
-        assertThat(trace.getEntries()).isNotEmpty();
-        assertThat(trace.getEntries().get(0).getTimestamp()).isEqualTo(2307984557311L);
-        assertThat(trace.getEntries().get(trace.getEntries().size() - 1).getTimestamp())
-                .isEqualTo(2308521813510L);
-        List<LayersTrace.Layer> layers = trace.getEntries().get(0).getRootLayers();
-        assertThat(layers).hasSize(2);
-        assertThat(layers.get(0).mChildren).hasSize(layers.get(0).mProto.children.length);
-        assertThat(layers.get(1).mChildren).hasSize(layers.get(1).mProto.children.length);
-    }
-
-    // b/76099859
-    @Test
-    public void canDetectOrphanLayers() {
-        try {
-            readLayerTraceFromFile("layers_trace_orphanlayers.pb");
-            fail("Failed to detect orphaned layers.");
-        } catch (RuntimeException exception) {
-            assertThat(exception.getMessage())
-                    .contains(
-                            "Failed to parse layers trace. Found orphan layers "
-                                    + "with parent layer id:1006 : 49");
-        }
-    }
-
-    // b/75276931
-    @Test
-    public void canDetectUncoveredRegion() {
-        LayersTrace trace = readLayerTraceFromFile("layers_trace_emptyregion.pb");
-        LayersTrace.Entry entry = trace.getEntry(2308008331271L);
-
-        Assertions.Result result = entry.coversRegion(getDisplayBounds());
-
-        assertThat(result.failed()).isTrue();
-        assertThat(result.reason).contains("Region to test: Rect(0, 0 - 1440, 2880)");
-        assertThat(result.reason).contains("first empty point: 0, 99");
-        assertThat(result.reason).contains("visible regions:");
-        assertWithMessage("Reason contains list of visible regions")
-                .that(result.reason)
-                .contains("StatusBar#0Rect(0, 0 - 1440, 98");
-    }
-
-    // Visible region tests
-    @Test
-    public void canTestLayerVisibleRegion_layerDoesNotExist() {
-        LayersTrace trace = readLayerTraceFromFile("layers_trace_emptyregion.pb");
-        LayersTrace.Entry entry = trace.getEntry(2308008331271L);
-
-        final Rect expectedVisibleRegion = new Rect(0, 0, 1, 1);
-        Assertions.Result result = entry.hasVisibleRegion("ImaginaryLayer", expectedVisibleRegion);
-
-        assertThat(result.failed()).isTrue();
-        assertThat(result.reason).contains("Could not find ImaginaryLayer");
-    }
-
-    @Test
-    public void canTestLayerVisibleRegion_layerDoesNotHaveExpectedVisibleRegion() {
-        LayersTrace trace = readLayerTraceFromFile("layers_trace_emptyregion.pb");
-        LayersTrace.Entry entry = trace.getEntry(2307993020072L);
-
-        final Rect expectedVisibleRegion = new Rect(0, 0, 1, 1);
-        Assertions.Result result =
-                entry.hasVisibleRegion("NexusLauncherActivity#2", expectedVisibleRegion);
-
-        assertThat(result.failed()).isTrue();
-        assertThat(result.reason)
-                .contains(
-                        "Layer com.google.android.apps.nexuslauncher/com.google.android.apps"
-                                + ".nexuslauncher.NexusLauncherActivity#2 is invisible: activeBuffer=null"
-                                + " type != ColorLayer flags=1 (FLAG_HIDDEN set) visible region is empty");
-    }
-
-    @Test
-    public void canTestLayerVisibleRegion_layerIsHiddenByParent() {
-        LayersTrace trace = readLayerTraceFromFile("layers_trace_emptyregion.pb");
-        LayersTrace.Entry entry = trace.getEntry(2308455948035L);
-
-        final Rect expectedVisibleRegion = new Rect(0, 0, 1, 1);
-        Assertions.Result result =
-                entry.hasVisibleRegion(
-                        "SurfaceView - com.android.chrome/com.google.android.apps.chrome.Main",
-                        expectedVisibleRegion);
-
-        assertThat(result.failed()).isTrue();
-        assertThat(result.reason)
-                .contains(
-                        "Layer SurfaceView - com.android.chrome/com.google.android.apps.chrome.Main#0 is "
-                                + "hidden by parent: com.android.chrome/com.google.android.apps.chrome"
-                                + ".Main#0");
-    }
-
-    @Test
-    public void canTestLayerVisibleRegion_incorrectRegionSize() {
-        LayersTrace trace = readLayerTraceFromFile("layers_trace_emptyregion.pb");
-        LayersTrace.Entry entry = trace.getEntry(2308008331271L);
-
-        final Rect expectedVisibleRegion = new Rect(0, 0, 1440, 99);
-        Assertions.Result result = entry.hasVisibleRegion("StatusBar", expectedVisibleRegion);
-
-        assertThat(result.failed()).isTrue();
-        assertThat(result.reason)
-                .contains(
-                        "StatusBar#0 has visible "
-                                + "region:Rect(0, 0 - 1440, 98) expected:Rect(0, 0 - 1440, 99)");
-    }
-
-    @Test
-    public void canTestLayerVisibleRegion() {
-        LayersTrace trace = readLayerTraceFromFile("layers_trace_emptyregion.pb");
-        LayersTrace.Entry entry = trace.getEntry(2308008331271L);
-
-        final Rect expectedVisibleRegion = new Rect(0, 0, 1440, 98);
-        Assertions.Result result = entry.hasVisibleRegion("StatusBar", expectedVisibleRegion);
-
-        assertThat(result.passed()).isTrue();
-    }
-
-    @Test
-    public void canTestLayerVisibleRegion_layerIsNotVisible() {
-        LayersTrace trace = readLayerTraceFromFile("layers_trace_invalid_layer_visibility.pb");
-        LayersTrace.Entry entry = trace.getEntry(252794268378458L);
-
-        Assertions.Result result = entry.isVisible("com.android.server.wm.flicker.testapp");
-        assertThat(result.failed()).isTrue();
-        assertThat(result.reason)
-                .contains(
-                        "Layer com.android.server.wm.flicker.testapp/com.android.server.wm.flicker"
-                                + ".testapp.SimpleActivity#0 is invisible: type != ColorLayer visible "
-                                + "region is empty");
-    }
-}
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/LayersTraceTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/LayersTraceTest.kt
new file mode 100644
index 0000000..3016a0a
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/LayersTraceTest.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2021 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
+
+import com.android.server.wm.traces.common.layers.LayersTrace
+import com.google.common.truth.Truth
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+
+/**
+ * Contains [LayersTrace] tests. To run this test: `atest
+ * FlickerLibTest:LayersTraceTest`
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class LayersTraceTest {
+    private fun detectRootLayer(fileName: String) {
+        val layersTrace = readLayerTraceFromFile(fileName)
+        for (entry in layersTrace.entries) {
+            val rootLayers = entry.rootLayers
+            Truth.assertWithMessage("Does not have any root layer")
+                .that(rootLayers.size)
+                .isGreaterThan(0)
+            val firstParentId = rootLayers.first().parentId
+            Truth.assertWithMessage("Has multiple root layers")
+                .that(rootLayers.all { it.parentId == firstParentId })
+                .isTrue()
+        }
+    }
+
+    @Test
+    fun testCanDetectRootLayer() {
+        detectRootLayer("layers_trace_root.pb")
+    }
+
+    @Test
+    fun testCanDetectRootLayerAOSP() {
+        detectRootLayer("layers_trace_root_aosp.pb")
+    }
+
+    @Test
+    fun testCanParseTraceWithoutHWC() {
+        val layersTrace = readLayerTraceFromFile("layers_trace_no_hwc_composition.pb")
+        layersTrace.forEach { entry ->
+            Truth.assertWithMessage("Should have visible layers in all trace entries")
+                .that(entry.visibleLayers).asList()
+                .isNotEmpty()
+        }
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/RegionSubjectTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/RegionSubjectTest.kt
new file mode 100644
index 0000000..b5816d2
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/RegionSubjectTest.kt
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2021 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
+
+import com.android.server.wm.flicker.traces.RegionSubject
+import com.android.server.wm.traces.common.Rect
+import com.google.common.truth.Truth
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+
+/**
+ * Contains [RegionSubject] tests. To run this test:
+ * `atest FlickerLibTest:RectSubjectTest`
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class RegionSubjectTest {
+    private fun assertFail(expectedMessage: String, predicate: () -> Any) {
+        val error = assertThrows(AssertionError::class.java) {
+            predicate()
+        }
+        Truth.assertThat(error).hasCauseThat().hasMessageThat().contains(expectedMessage)
+    }
+
+    private fun expectAllFailPositionChange(expectedMessage: String, rectA: Rect, rectB: Rect) {
+        assertFail(expectedMessage) {
+            RegionSubject.assertThat(rectA).isHigher(rectB)
+        }
+        assertFail(expectedMessage) {
+            RegionSubject.assertThat(rectA).isHigherOrEqual(rectB)
+        }
+        assertFail(expectedMessage) {
+            RegionSubject.assertThat(rectA).isLower(rectB)
+        }
+        assertFail(expectedMessage) {
+            RegionSubject.assertThat(rectA).isLowerOrEqual(rectB)
+        }
+    }
+
+    @Test
+    fun detectPositionChangeHigher() {
+        val rectA = Rect(left = 0, top = 0, right = 1, bottom = 1)
+        val rectB = Rect(left = 0, top = 1, right = 1, bottom = 2)
+        RegionSubject.assertThat(rectA).isHigher(rectB)
+        RegionSubject.assertThat(rectA).isHigherOrEqual(rectB)
+        assertFail(RegionSubject.MSG_ERROR_TOP_POSITION) {
+            RegionSubject.assertThat(rectA).isLower(rectB)
+        }
+        assertFail(RegionSubject.MSG_ERROR_TOP_POSITION) {
+            RegionSubject.assertThat(rectA).isLowerOrEqual(rectB)
+        }
+    }
+
+    @Test
+    fun detectPositionChangeLower() {
+        val rectA = Rect(left = 0, top = 2, right = 1, bottom = 3)
+        val rectB = Rect(left = 0, top = 0, right = 1, bottom = 1)
+        RegionSubject.assertThat(rectA).isLower(rectB)
+        RegionSubject.assertThat(rectA).isLowerOrEqual(rectB)
+        assertFail(RegionSubject.MSG_ERROR_TOP_POSITION) {
+            RegionSubject.assertThat(rectA).isHigher(rectB)
+        }
+        assertFail(RegionSubject.MSG_ERROR_TOP_POSITION) {
+            RegionSubject.assertThat(rectA).isHigherOrEqual(rectB)
+        }
+    }
+
+    @Test
+    fun detectPositionChangeEqualHigherLower() {
+        val rectA = Rect(left = 0, top = 1, right = 1, bottom = 0)
+        val rectB = Rect(left = 1, top = 1, right = 2, bottom = 0)
+        RegionSubject.assertThat(rectA).isHigherOrEqual(rectB)
+        RegionSubject.assertThat(rectA).isLowerOrEqual(rectB)
+        assertFail(RegionSubject.MSG_ERROR_TOP_POSITION) {
+            RegionSubject.assertThat(rectA).isHigher(rectB)
+        }
+        assertFail(RegionSubject.MSG_ERROR_TOP_POSITION) {
+            RegionSubject.assertThat(rectA).isLower(rectB)
+        }
+    }
+
+    @Test
+    fun detectPositionChangeInvalid() {
+        val rectA = Rect(left = 0, top = 1, right = 2, bottom = 2)
+        val rectB = Rect(left = 1, top = 1, right = 2, bottom = 2)
+        val rectC = Rect(left = 0, top = 1, right = 3, bottom = 1)
+        expectAllFailPositionChange(RegionSubject.MSG_ERROR_LEFT_POSITION, rectA, rectB)
+        expectAllFailPositionChange(RegionSubject.MSG_ERROR_RIGHT_POSITION, rectA, rectC)
+    }
+
+    @Test
+    fun detectCoversAtLeast() {
+        val rectA = Rect(left = 1, top = 1, right = 2, bottom = 2)
+        val rectB = Rect(left = 0, top = 0, right = 2, bottom = 2)
+        RegionSubject.assertThat(rectA).coversAtLeast(rectA)
+        RegionSubject.assertThat(rectB).coversAtLeast(rectA)
+        assertFail("SkRegion((0,0,2,1)(0,1,1,2))") {
+            RegionSubject.assertThat(rectA).coversAtLeast(rectB)
+        }
+    }
+
+    @Test
+    fun detectCoversAtMost() {
+        val rectA = Rect(left = 1, top = 1, right = 2, bottom = 2)
+        val rectB = Rect(left = 0, top = 0, right = 2, bottom = 2)
+        RegionSubject.assertThat(rectA).coversAtMost(rectA)
+        RegionSubject.assertThat(rectA).coversAtMost(rectB)
+        assertFail("SkRegion((0,0,2,1)(0,1,1,2))") {
+            RegionSubject.assertThat(rectB).coversAtMost(rectA)
+        }
+    }
+
+    @Test
+    fun detectCoversExactly() {
+        val rectA = Rect(left = 1, top = 1, right = 2, bottom = 2)
+        val rectB = Rect(left = 0, top = 0, right = 2, bottom = 2)
+        RegionSubject.assertThat(rectA).coversExactly(rectA)
+        assertFail("SkRegion((0,0,2,1)(0,1,1,2))") {
+            RegionSubject.assertThat(rectA).coversExactly(rectB)
+        }
+    }
+
+    @Test
+    fun detectOverlaps() {
+        val rectA = Rect(left = 1, top = 1, right = 2, bottom = 2)
+        val rectB = Rect(left = 0, top = 0, right = 2, bottom = 2)
+        val rectC = Rect(left = 2, top = 2, right = 3, bottom = 3)
+        RegionSubject.assertThat(rectA).overlaps(rectB)
+        RegionSubject.assertThat(rectB).overlaps(rectA)
+        assertFail("Overlap region: SkRegion()") {
+            RegionSubject.assertThat(rectA).overlaps(rectC)
+        }
+    }
+
+    @Test
+    fun detectsNotOverlaps() {
+        val rectA = Rect(left = 1, top = 1, right = 2, bottom = 2)
+        val rectB = Rect(left = 2, top = 2, right = 3, bottom = 3)
+        val rectC = Rect(left = 0, top = 0, right = 2, bottom = 2)
+        RegionSubject.assertThat(rectA).notOverlaps(rectB)
+        RegionSubject.assertThat(rectB).notOverlaps(rectA)
+        assertFail("SkRegion((1,1,2,2))") {
+            RegionSubject.assertThat(rectA).notOverlaps(rectC)
+        }
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/TestFileUtils.java b/libraries/flicker/test/src/com/android/server/wm/flicker/TestFileUtils.java
deleted file mode 100644
index 3260823..0000000
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/TestFileUtils.java
+++ /dev/null
@@ -1,30 +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;
-
-import android.content.Context;
-import androidx.test.InstrumentationRegistry;
-import com.google.common.io.ByteStreams;
-import java.io.InputStream;
-
-/** Helper functions for test file resources. */
-class TestFileUtils {
-    static byte[] readTestFile(String relativePath) throws Exception {
-        Context context = InstrumentationRegistry.getContext();
-        InputStream in = context.getResources().getAssets().open("testdata/" + relativePath);
-        return ByteStreams.toByteArray(in);
-    }
-}
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
deleted file mode 100644
index 10562d9..0000000
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/TransitionRunnerTest.java
+++ /dev/null
@@ -1,269 +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;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.inOrder;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-
-import android.os.Environment;
-
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.server.wm.flicker.TransitionRunner.TransitionBuilder;
-import com.android.server.wm.flicker.TransitionRunner.TransitionResult;
-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 org.junit.Before;
-import org.junit.FixMethodOrder;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.MethodSorters;
-import org.mockito.InOrder;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.nio.file.Paths;
-import java.util.List;
-
-/** Contains {@link TransitionRunner} tests. {@code atest FlickerLibTest:TransitionRunnerTest} */
-@RunWith(AndroidJUnit4.class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-public class TransitionRunnerTest {
-    @Mock private SimpleUiTransitions mTransitionsMock;
-    @Mock private ScreenRecorder mScreenRecorderMock;
-    @Mock private WindowManagerTraceMonitor mWindowManagerTraceMonitorMock;
-    @Mock private LayersTraceMonitor mLayersTraceMonitorMock;
-    @Mock private WindowAnimationFrameStatsMonitor mWindowAnimationFrameStatsMonitor;
-    @InjectMocks private TransitionBuilder mTransitionBuilder = TransitionRunner.newBuilder();
-
-    @Before
-    public void init() {
-        MockitoAnnotations.initMocks(this);
-    }
-
-    @Test
-    public void transitionsRunInOrder() {
-        TransitionRunner.newBuilder()
-                .runBeforeAll(mTransitionsMock::turnOnDevice)
-                .runBefore(mTransitionsMock::openApp)
-                .run(mTransitionsMock::performMagic)
-                .runAfter(mTransitionsMock::closeApp)
-                .runAfterAll(mTransitionsMock::cleanUpTracks)
-                .skipLayersTrace()
-                .skipWindowManagerTrace()
-                .build()
-                .run();
-
-        InOrder orderVerifier = inOrder(mTransitionsMock);
-        orderVerifier.verify(mTransitionsMock).turnOnDevice();
-        orderVerifier.verify(mTransitionsMock).openApp();
-        orderVerifier.verify(mTransitionsMock).performMagic();
-        orderVerifier.verify(mTransitionsMock).closeApp();
-        orderVerifier.verify(mTransitionsMock).cleanUpTracks();
-    }
-
-    @Test
-    public void canCombineTransitions() {
-        TransitionRunner.newBuilder()
-                .runBeforeAll(mTransitionsMock::turnOnDevice)
-                .runBeforeAll(mTransitionsMock::turnOnDevice)
-                .runBefore(mTransitionsMock::openApp)
-                .runBefore(mTransitionsMock::openApp)
-                .run(mTransitionsMock::performMagic)
-                .run(mTransitionsMock::performMagic)
-                .runAfter(mTransitionsMock::closeApp)
-                .runAfter(mTransitionsMock::closeApp)
-                .runAfterAll(mTransitionsMock::cleanUpTracks)
-                .runAfterAll(mTransitionsMock::cleanUpTracks)
-                .skipLayersTrace()
-                .skipWindowManagerTrace()
-                .build()
-                .run();
-
-        final int wantedNumberOfInvocations = 2;
-        verify(mTransitionsMock, times(wantedNumberOfInvocations)).turnOnDevice();
-        verify(mTransitionsMock, times(wantedNumberOfInvocations)).openApp();
-        verify(mTransitionsMock, times(wantedNumberOfInvocations)).performMagic();
-        verify(mTransitionsMock, times(wantedNumberOfInvocations)).closeApp();
-        verify(mTransitionsMock, times(wantedNumberOfInvocations)).cleanUpTracks();
-    }
-
-    @Test
-    public void emptyTransitionPasses() {
-        List<TransitionResult> results =
-                TransitionRunner.newBuilder()
-                        .skipLayersTrace()
-                        .skipWindowManagerTrace()
-                        .build()
-                        .run()
-                        .getResults();
-        assertThat(results).hasSize(1);
-        assertThat(results.get(0).layersTraceExists()).isFalse();
-        assertThat(results.get(0).windowManagerTraceExists()).isFalse();
-        assertThat(results.get(0).screenCaptureVideoExists()).isFalse();
-    }
-
-    @Test
-    public void canRepeatTransitions() {
-        final int wantedNumberOfInvocations = 10;
-        TransitionRunner.newBuilder()
-                .runBeforeAll(mTransitionsMock::turnOnDevice)
-                .runBefore(mTransitionsMock::openApp)
-                .run(mTransitionsMock::performMagic)
-                .runAfter(mTransitionsMock::closeApp)
-                .runAfterAll(mTransitionsMock::cleanUpTracks)
-                .repeat(wantedNumberOfInvocations)
-                .skipLayersTrace()
-                .skipWindowManagerTrace()
-                .build()
-                .run();
-        verify(mTransitionsMock).turnOnDevice();
-        verify(mTransitionsMock, times(wantedNumberOfInvocations)).openApp();
-        verify(mTransitionsMock, times(wantedNumberOfInvocations)).performMagic();
-        verify(mTransitionsMock, times(wantedNumberOfInvocations)).closeApp();
-        verify(mTransitionsMock).cleanUpTracks();
-    }
-
-    private void emptyTask() {}
-
-    @Test
-    public void canCaptureWindowManagerTrace() {
-        mTransitionBuilder
-                .run(this::emptyTask)
-                .includeJankyRuns()
-                .skipLayersTrace()
-                .withTag("mCaptureWmTraceTransitionRunner")
-                .build()
-                .run();
-        InOrder orderVerifier = inOrder(mWindowManagerTraceMonitorMock);
-        orderVerifier.verify(mWindowManagerTraceMonitorMock).start();
-        orderVerifier.verify(mWindowManagerTraceMonitorMock).stop();
-        orderVerifier
-                .verify(mWindowManagerTraceMonitorMock)
-                .save("mCaptureWmTraceTransitionRunner", 0);
-        orderVerifier.verify(mWindowManagerTraceMonitorMock).getChecksum();
-        verifyNoMoreInteractions(mWindowManagerTraceMonitorMock);
-    }
-
-    @Test
-    public void canCaptureLayersTrace() {
-        mTransitionBuilder
-                .run(this::emptyTask)
-                .includeJankyRuns()
-                .skipWindowManagerTrace()
-                .withTag("mCaptureLayersTraceTransitionRunner")
-                .build()
-                .run();
-        InOrder orderVerifier = inOrder(mLayersTraceMonitorMock);
-        orderVerifier.verify(mLayersTraceMonitorMock).start();
-        orderVerifier.verify(mLayersTraceMonitorMock).stop();
-        orderVerifier
-                .verify(mLayersTraceMonitorMock)
-                .save("mCaptureLayersTraceTransitionRunner", 0);
-        orderVerifier.verify(mLayersTraceMonitorMock).getChecksum();
-        verifyNoMoreInteractions(mLayersTraceMonitorMock);
-    }
-
-    @Test
-    public void canRecordEachRun() {
-        mTransitionBuilder
-                .run(this::emptyTask)
-                .withTag("mRecordEachRun")
-                .recordEachRun()
-                .includeJankyRuns()
-                .skipLayersTrace()
-                .skipWindowManagerTrace()
-                .repeat(2)
-                .build()
-                .run();
-        InOrder orderVerifier = inOrder(mScreenRecorderMock);
-        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() {
-        doReturn(
-                        Paths.get(
-                                Environment.getExternalStorageDirectory().getAbsolutePath(),
-                                "mRecordAllRuns.mp4"))
-                .when(mScreenRecorderMock)
-                .save("mRecordAllRuns");
-        mTransitionBuilder
-                .run(this::emptyTask)
-                .recordAllRuns()
-                .includeJankyRuns()
-                .skipLayersTrace()
-                .skipWindowManagerTrace()
-                .withTag("mRecordAllRuns")
-                .repeat(2)
-                .build()
-                .run();
-        InOrder orderVerifier = inOrder(mScreenRecorderMock);
-        orderVerifier.verify(mScreenRecorderMock).start();
-        orderVerifier.verify(mScreenRecorderMock).stop();
-        orderVerifier.verify(mScreenRecorderMock).save("mRecordAllRuns");
-        verifyNoMoreInteractions(mScreenRecorderMock);
-    }
-
-    @Test
-    public void canSkipJankyRuns() {
-        doReturn(false)
-                .doReturn(true)
-                .doReturn(false)
-                .when(mWindowAnimationFrameStatsMonitor)
-                .jankyFramesDetected();
-        List<TransitionResult> results =
-                mTransitionBuilder
-                        .run(this::emptyTask)
-                        .skipLayersTrace()
-                        .skipWindowManagerTrace()
-                        .repeat(3)
-                        .build()
-                        .run()
-                        .getResults();
-        assertThat(results).hasSize(2);
-    }
-
-    public static class SimpleUiTransitions {
-        public void turnOnDevice() {}
-
-        public void openApp() {}
-
-        public void performMagic() {}
-
-        public void closeApp() {}
-
-        public void cleanUpTracks() {}
-    }
-}
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/TransitionRunnerTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/TransitionRunnerTest.kt
new file mode 100644
index 0000000..e6b5883
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/TransitionRunnerTest.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2021 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
+
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.google.common.truth.Truth
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+
+/**
+ * Contains [TransitionRunnerTest] and [TransitionRunnerCached] tests.
+ *
+ * To run this test: `atest FlickerLibTest:TransitionRunnerTest`
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class TransitionRunnerTest {
+    private val instrumentation = InstrumentationRegistry.getInstrumentation()
+
+    @Test
+    fun canRunTransition() {
+        val runner = TransitionRunner()
+        var executed = false
+        val flicker = FlickerBuilder(instrumentation)
+            .apply {
+                transitions {
+                    executed = true
+                }
+            }.build(runner)
+        Truth.assertThat(executed).isFalse()
+        val result = runner.execute(flicker)
+        Truth.assertThat(executed).isTrue()
+        Truth.assertThat(result.error).isNull()
+        Truth.assertThat(result.runs).hasSize(4)
+    }
+
+    @Test
+    fun canRunTransitionCached() {
+        val runner = TransitionRunnerCached()
+        var executed = false
+        val flicker = FlickerBuilder(instrumentation)
+            .apply {
+                transitions {
+                    executed = true
+                }
+            }.build(runner)
+        val result = runner.execute(flicker)
+        executed = false
+        val cachedResult = runner.execute(flicker)
+        Truth.assertThat(executed).isFalse()
+        Truth.assertThat(cachedResult).isEqualTo(result)
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/UiDeviceExtensionsTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/UiDeviceExtensionsTest.kt
new file mode 100644
index 0000000..8814113
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/UiDeviceExtensionsTest.kt
@@ -0,0 +1,87 @@
+/*
+ * 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.server.wm.flicker
+
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.server.wm.traces.parser.DeviceStateDump
+import com.android.server.wm.traces.parser.FLAG_STATE_DUMP_FLAG_LAYERS
+import com.android.server.wm.traces.parser.FLAG_STATE_DUMP_FLAG_WM
+import com.android.server.wm.traces.parser.WmStateDumpFlags
+import com.android.server.wm.traces.parser.getCurrentState
+import com.android.server.wm.traces.parser.getCurrentStateDump
+import com.google.common.truth.Truth
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+
+/**
+ * Contains [UiDeviceExtensions] tests.
+ *
+ * To run this test: `atest FlickerLibTest:UiDeviceExtensionsTest`
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class UiDeviceExtensionsTest {
+    private fun getCurrState(
+        @WmStateDumpFlags dumpFlags: Int = FLAG_STATE_DUMP_FLAG_WM.or(FLAG_STATE_DUMP_FLAG_LAYERS)
+    ): Pair<ByteArray, ByteArray> {
+        val instrumentation = InstrumentationRegistry.getInstrumentation()
+        return getCurrentState(instrumentation.uiAutomation, dumpFlags)
+    }
+
+    private fun getCurrStateDump(
+        @WmStateDumpFlags dumpFlags: Int = FLAG_STATE_DUMP_FLAG_WM.or(FLAG_STATE_DUMP_FLAG_LAYERS)
+    ): DeviceStateDump {
+        val instrumentation = InstrumentationRegistry.getInstrumentation()
+        return getCurrentStateDump(instrumentation.uiAutomation, dumpFlags)
+    }
+
+    @Test
+    fun canFetchCurrentDeviceState() {
+        val currState = this.getCurrState()
+        Truth.assertThat(currState.first).isNotEmpty()
+        Truth.assertThat(currState.second).isNotEmpty()
+    }
+
+    @Test
+    fun canFetchCurrentDeviceStateOnlyWm() {
+        val currStateDump = this.getCurrState(FLAG_STATE_DUMP_FLAG_WM)
+        Truth.assertThat(currStateDump.first).isNotEmpty()
+        Truth.assertThat(currStateDump.second).isEmpty()
+        val currState = this.getCurrStateDump(FLAG_STATE_DUMP_FLAG_WM)
+        Truth.assertThat(currState.wmTrace).isNotNull()
+        Truth.assertThat(currState.layersTrace).isNull()
+    }
+
+    @Test
+    fun canFetchCurrentDeviceStateOnlyLayers() {
+        val currStateDump = this.getCurrState(FLAG_STATE_DUMP_FLAG_LAYERS)
+        Truth.assertThat(currStateDump.first).isEmpty()
+        Truth.assertThat(currStateDump.second).isNotEmpty()
+        val currState = this.getCurrStateDump(FLAG_STATE_DUMP_FLAG_LAYERS)
+        Truth.assertThat(currState.wmTrace).isNull()
+        Truth.assertThat(currState.layersTrace).isNotNull()
+    }
+
+    @Test
+    fun canParseCurrentDeviceState() {
+        val currState = this.getCurrStateDump()
+        Truth.assertThat(currState.wmTrace?.entries).hasSize(1)
+        Truth.assertThat(currState.wmTrace?.entries?.first()?.windowStates).isNotEmpty()
+        Truth.assertThat(currState.layersTrace?.entries).hasSize(1)
+        Truth.assertThat(currState.layersTrace?.entries?.first()?.flattenedLayers).isNotEmpty()
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/Utils.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/Utils.kt
new file mode 100644
index 0000000..bc376b2
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/Utils.kt
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2021 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
+
+import android.content.Context
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.server.wm.flicker.traces.FlickerSubjectException
+import com.android.server.wm.traces.common.layers.LayersTrace
+import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace
+import com.android.server.wm.traces.parser.layers.LayersTraceParser
+import com.android.server.wm.traces.parser.windowmanager.WindowManagerTraceParser
+import com.google.common.io.ByteStreams
+import com.google.common.truth.ExpectFailure
+import com.google.common.truth.TruthFailureSubject
+import java.nio.file.Paths
+
+internal fun readWmTraceFromFile(relativePath: String): WindowManagerTrace {
+    return try {
+        WindowManagerTraceParser.parseFromTrace(readTestFile(relativePath),
+            source = Paths.get(relativePath))
+    } catch (e: Exception) {
+        throw RuntimeException(e)
+    }
+}
+
+internal fun readWmTraceFromDumpFile(relativePath: String): WindowManagerTrace {
+    return try {
+        WindowManagerTraceParser.parseFromDump(readTestFile(relativePath))
+    } catch (e: Exception) {
+        throw RuntimeException(e)
+    }
+}
+
+internal fun readLayerTraceFromFile(
+    relativePath: String,
+    ignoreOrphanLayers: Boolean = true
+): LayersTrace {
+    return try {
+        LayersTraceParser.parseFromTrace(readTestFile(relativePath),
+            source = Paths.get(relativePath)) { ignoreOrphanLayers }
+    } catch (e: Exception) {
+        throw RuntimeException(e)
+    }
+}
+
+@Throws(Exception::class)
+internal fun readTestFile(relativePath: String): ByteArray {
+    val context: Context = InstrumentationRegistry.getInstrumentation().context
+    val inputStream = context.resources.assets.open("testdata/$relativePath")
+    return ByteStreams.toByteArray(inputStream)
+}
+
+/**
+ * Runs `r` and asserts that an exception of type `expectedThrowable` is thrown.
+ * @param expectedThrowable the type of throwable that is expected to be thrown
+ * @param r the [Runnable] which is run and expected to throw.
+ * @throws AssertionError if `r` does not throw, or throws a runnable that is not an
+ * instance of `expectedThrowable`.
+ */
+// TODO: remove once Android migrates to JUnit 4.13, which provides assertThrows
+fun assertThrows(expectedThrowable: Class<out Throwable>, r: () -> Any): Throwable {
+    try {
+        r()
+    } catch (t: Throwable) {
+        when {
+            expectedThrowable.isInstance(t) -> return t
+            t is Exception ->
+                throw AssertionError("Expected $expectedThrowable, but got ${t.javaClass}", t)
+            // Re-throw Errors and other non-Exception throwables.
+            else -> throw t
+        }
+    }
+    error("Expected $expectedThrowable, but nothing was thrown")
+}
+
+fun assertFailure(failure: Throwable?): TruthFailureSubject {
+    val target = when (failure) {
+        is FlickerSubjectException -> failure.cause
+        is AssertionError -> failure
+        else -> error("Expected assertion error, received $failure")
+    }
+    require(target is AssertionError) { "Unknown failure $target" }
+    return ExpectFailure.assertThat(target)
+}
\ No newline at end of file
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/WindowManagerStateHelperTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/WindowManagerStateHelperTest.kt
new file mode 100644
index 0000000..4c18c68
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/WindowManagerStateHelperTest.kt
@@ -0,0 +1,343 @@
+/*
+ * 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.server.wm.flicker
+
+import android.content.ComponentName
+import android.view.Display
+import android.view.Surface
+import androidx.test.filters.FlakyTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerStateSubject
+import com.android.server.wm.traces.common.Buffer
+import com.android.server.wm.traces.common.Color
+import com.android.server.wm.traces.common.Rect
+import com.android.server.wm.traces.common.RectF
+import com.android.server.wm.traces.common.Region
+import com.android.server.wm.traces.common.layers.Layer
+import com.android.server.wm.traces.common.layers.LayerTraceEntryBuilder
+import com.android.server.wm.traces.common.layers.Transform
+import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace
+import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
+import com.google.common.truth.Truth
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+
+/**
+ * Contains [WindowManagerStateHelper] tests. To run this test: `atest
+ * FlickerLibTest:WindowManagerTraceHelperTest`
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class WindowManagerStateHelperTest {
+    class TestWindowManagerStateHelper(
+        /**
+         * Predicate to supply a new UI information
+         */
+        deviceDumpSupplier: () -> Dump,
+        numRetries: Int = 5,
+        retryIntervalMs: Long = 500L
+    ) : WindowManagerStateHelper(InstrumentationRegistry.getInstrumentation(),
+        deviceDumpSupplier, numRetries, retryIntervalMs) {
+        var wmState = computeState(ignoreInvalidStates = true).wmState
+        override fun computeState(ignoreInvalidStates: Boolean): Dump {
+            val state = super.computeState(ignoreInvalidStates)
+            wmState = state.wmState
+            return state
+        }
+    }
+
+    private fun String.toComponentName() =
+        ComponentName.unflattenFromString(this) ?: error("Unable to extract component name")
+
+    private val chromeComponentName = ("com.android.chrome/org.chromium.chrome.browser" +
+        ".firstrun.FirstRunActivity").toComponentName()
+    private val simpleAppComponentName = "com.android.server.wm.flicker.testapp/.SimpleActivity"
+        .toComponentName()
+
+    private fun createImaginaryLayer(name: String, index: Int, id: Int, parentId: Int): Layer {
+        val transform = Transform(0, Transform.Matrix(0f, 0f, 0f, 0f, 0f, 0f))
+        val rect = RectF(
+            left = index.toFloat(),
+            top = index.toFloat(),
+            right = index.toFloat() + 1,
+            bottom = index.toFloat() + 1
+        )
+        return Layer(
+            name,
+            id,
+            parentId,
+            z = 0,
+            visibleRegion = Region(rect.toRect()),
+            activeBuffer = Buffer(1, 1, 1, 1),
+            flags = 0,
+            _bounds = rect,
+            color = Color(0f, 0f, 0f, 1f),
+            _isOpaque = true,
+            shadowRadius = 0f,
+            cornerRadius = 0f,
+            type = "",
+            _screenBounds = rect,
+            transform = transform,
+            _sourceBounds = rect,
+            currFrame = 0,
+            effectiveScalingMode = 0,
+            bufferTransform = transform,
+            hwcCompositionType = 0,
+            hwcCrop = RectF.EMPTY,
+            hwcFrame = Rect.EMPTY,
+            crop = rect.toRect(),
+            backgroundBlurRadius = 0,
+            isRelativeOf = false,
+            zOrderRelativeOfId = -1
+        )
+    }
+
+    private fun createImaginaryVisibleLayers(names: List<String>): List<Layer> {
+        val root = createImaginaryLayer("root", -1, id = "root".hashCode(), parentId = -1)
+        val layers = mutableListOf(root)
+        names.forEachIndexed { index, name ->
+            layers.add(
+                createImaginaryLayer(name, index, id = name.hashCode(), parentId = root.id)
+            )
+        }
+        return layers
+    }
+
+    private fun WindowManagerTrace.asSupplier(
+        startingTimestamp: Long = 0
+    ): () -> WindowManagerStateHelper.Dump {
+        val iterator = this.dropWhile { it.timestamp < startingTimestamp }.iterator()
+        return {
+            if (iterator.hasNext()) {
+                val wmState = iterator.next()
+                val layerList = mutableListOf(WindowManagerStateHelper.STATUS_BAR_LAYER_NAME,
+                    WindowManagerStateHelper.NAV_BAR_LAYER_NAME)
+
+                if (wmState.inputMethodWindowState?.isSurfaceShown == true) {
+                    layerList.add(WindowManagerStateHelper.IME_LAYER_NAME)
+                }
+                val layerTraceEntry = LayerTraceEntryBuilder(timestamp = 0,
+                    layers = createImaginaryVisibleLayers(layerList)).build()
+                WindowManagerStateHelper.Dump(
+                    wmState,
+                    layerTraceEntry
+                )
+            } else {
+                error("Reached the end of the trace")
+            }
+        }
+    }
+
+    @Test
+    fun canWaitForIme() {
+        val trace = readWmTraceFromFile("wm_trace_ime.pb")
+        val supplier = trace.asSupplier()
+        val helper = TestWindowManagerStateHelper(supplier, numRetries = trace.entries.size,
+            retryIntervalMs = 1)
+        try {
+            WindowManagerStateSubject
+                .assertThat(helper.wmState)
+                .isImeWindowVisible(Display.DEFAULT_DISPLAY)
+            error("IME state should not be available")
+        } catch (e: AssertionError) {
+            helper.waitImeWindowShown(Display.DEFAULT_DISPLAY)
+            WindowManagerStateSubject
+                .assertThat(helper.wmState)
+                .isImeWindowVisible(Display.DEFAULT_DISPLAY)
+        }
+    }
+
+    @Test
+    fun canFailImeNotShown() {
+        val supplier = readWmTraceFromFile("wm_trace_ime.pb").asSupplier()
+        val helper = TestWindowManagerStateHelper(supplier, retryIntervalMs = 1)
+        try {
+            WindowManagerStateSubject
+                .assertThat(helper.wmState)
+                .isImeWindowVisible()
+            error("IME state should not be available")
+        } catch (e: AssertionError) {
+            helper.waitImeWindowShown()
+            WindowManagerStateSubject
+                .assertThat(helper.wmState)
+                .isImeWindowInvisible()
+        }
+    }
+
+    @Test
+    fun canWaitForWindow() {
+        val trace = readWmTraceFromFile("wm_trace_open_app_cold.pb")
+        val supplier = trace.asSupplier()
+        val helper = TestWindowManagerStateHelper(supplier, numRetries = trace.entries.size,
+            retryIntervalMs = 1)
+        try {
+            WindowManagerStateSubject
+                .assertThat(helper.wmState)
+                .contains(simpleAppComponentName)
+            error("Chrome window should not exist in the start of the trace")
+        } catch (e: AssertionError) {
+            helper.waitForVisibleWindow(simpleAppComponentName)
+            WindowManagerStateSubject
+                .assertThat(helper.wmState)
+                .isVisible(simpleAppComponentName)
+        }
+    }
+
+    @Test
+    fun canFailWindowNotShown() {
+        val trace = readWmTraceFromFile("wm_trace_open_app_cold.pb")
+        val supplier = trace.asSupplier()
+        val helper = TestWindowManagerStateHelper(supplier, numRetries = 3, retryIntervalMs = 1)
+        try {
+            WindowManagerStateSubject
+                .assertThat(helper.wmState)
+                .contains(simpleAppComponentName)
+            error("SimpleActivity window should not exist in the start of the trace")
+        } catch (e: AssertionError) {
+            helper.waitForVisibleWindow(simpleAppComponentName)
+            WindowManagerStateSubject
+                .assertThat(helper.wmState)
+                .notContains(simpleAppComponentName)
+        }
+    }
+
+    @Test
+    fun canDetectHomeActivityVisibility() {
+        val trace = readWmTraceFromFile("wm_trace_open_and_close_chrome.pb")
+        val supplier = trace.asSupplier()
+        val helper = TestWindowManagerStateHelper(supplier, numRetries = trace.entries.size,
+            retryIntervalMs = 1)
+        WindowManagerStateSubject
+            .assertThat(helper.wmState)
+            .isHomeActivityVisible()
+        helper.waitForVisibleWindow(chromeComponentName)
+        WindowManagerStateSubject
+            .assertThat(helper.wmState)
+            .isHomeActivityVisible(false)
+        helper.waitForHomeActivityVisible()
+        WindowManagerStateSubject
+            .assertThat(helper.wmState)
+            .isHomeActivityVisible()
+    }
+
+    @Test
+    fun canWaitActivityRemoved() {
+        val trace = readWmTraceFromFile("wm_trace_open_and_close_chrome.pb")
+        val supplier = trace.asSupplier()
+        val helper = TestWindowManagerStateHelper(supplier, numRetries = trace.entries.size,
+            retryIntervalMs = 1)
+        WindowManagerStateSubject
+            .assertThat(helper.wmState)
+            .isHomeActivityVisible()
+            .notContains(chromeComponentName)
+        helper.waitForVisibleWindow(chromeComponentName)
+        WindowManagerStateSubject
+            .assertThat(helper.wmState)
+            .isVisible(chromeComponentName)
+        helper.waitForActivityRemoved(chromeComponentName)
+        WindowManagerStateSubject
+            .assertThat(helper.wmState)
+            .notContains(chromeComponentName)
+            .isHomeActivityVisible()
+    }
+
+    @Test
+    fun canWaitAppStateIdle() {
+        val trace = readWmTraceFromFile("wm_trace_open_and_close_chrome.pb")
+        val supplier = trace.asSupplier(startingTimestamp = 69443911868523)
+        val helper = TestWindowManagerStateHelper(supplier, numRetries = trace.entries.size,
+            retryIntervalMs = 1)
+        try {
+            WindowManagerStateSubject
+                .assertThat(helper.wmState)
+                .isValid()
+            error("Initial state in the trace should not be valid")
+        } catch (e: AssertionError) {
+            Truth.assertWithMessage("App transition never became idle")
+                .that(helper.waitForAppTransitionIdle())
+                .isTrue()
+            WindowManagerStateSubject
+                .assertThat(helper.wmState)
+                .isValid()
+        }
+    }
+
+    @Test
+    fun canWaitForRotation() {
+        val trace = readWmTraceFromFile("wm_trace_rotation.pb")
+        val supplier = trace.asSupplier()
+        val helper = TestWindowManagerStateHelper(supplier, numRetries = trace.entries.size,
+            retryIntervalMs = 1)
+        WindowManagerStateSubject
+            .assertThat(helper.wmState)
+            .hasRotation(Surface.ROTATION_0)
+        helper.waitForRotation(Surface.ROTATION_270)
+        WindowManagerStateSubject
+            .assertThat(helper.wmState)
+            .hasRotation(Surface.ROTATION_270)
+        helper.waitForRotation(Surface.ROTATION_0)
+        WindowManagerStateSubject
+            .assertThat(helper.wmState)
+            .hasRotation(Surface.ROTATION_0)
+    }
+
+    @Test
+    fun canFailRotationNotReached() {
+        val trace = readWmTraceFromFile("wm_trace_rotation.pb")
+        val supplier = trace.asSupplier()
+        val helper = TestWindowManagerStateHelper(supplier, numRetries = trace.entries.size,
+            retryIntervalMs = 1)
+        WindowManagerStateSubject
+            .assertThat(helper.wmState)
+            .hasRotation(Surface.ROTATION_0)
+        try {
+            helper.waitForRotation(Surface.ROTATION_90)
+            error("Should not have reached orientation ${Surface.ROTATION_90}")
+        } catch (e: IllegalStateException) {
+            WindowManagerStateSubject
+                .assertThat(helper.wmState)
+                .isNotRotation(Surface.ROTATION_90)
+                .hasRotation(Surface.ROTATION_0)
+        }
+    }
+
+    @Test
+    fun canDetectResumedActivitiesInStacks() {
+        val trace = readWmTraceFromDumpFile("wm_trace_resumed_activities_in_stack.pb")
+        val entry = trace.first()
+        Truth.assertWithMessage("Trace should have a resumed activity in stacks")
+            .that(entry.resumedActivities)
+            .asList()
+            .hasSize(1)
+    }
+
+    @FlakyTest
+    @Test
+    fun canWaitForRecents() {
+        val trace = readWmTraceFromFile("wm_trace_open_recents.pb")
+        val supplier = trace.asSupplier()
+        val helper = TestWindowManagerStateHelper(supplier, numRetries = trace.entries.size,
+            retryIntervalMs = 1)
+        WindowManagerStateSubject
+            .assertThat(helper.wmState)
+            .isRecentsActivityVisible(visible = false)
+        helper.waitForRecentsActivityVisible()
+        WindowManagerStateSubject
+            .assertThat(helper.wmState)
+            .isRecentsActivityVisible()
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/WindowManagerStateSubjectTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/WindowManagerStateSubjectTest.kt
new file mode 100644
index 0000000..959232a
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/WindowManagerStateSubjectTest.kt
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2021 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
+
+import android.graphics.Region
+import com.android.server.wm.flicker.traces.FlickerSubjectException
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerStateSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject.Companion.assertThat
+import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+import java.lang.AssertionError
+
+/**
+ * Contains [WindowManagerStateSubject] tests. To run this test: `atest
+ * FlickerLibTest:WindowManagerStateSubjectTest`
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class WindowManagerStateSubjectTest {
+    private val trace: WindowManagerTrace by lazy { readWmTraceFromFile("wm_trace_openchrome.pb") }
+
+    @Test
+    fun canDetectAboveAppWindowVisibility_isVisible() {
+        assertThat(trace)
+            .entry(9213763541297)
+            .isAboveAppWindow("NavigationBar")
+            .isAboveAppWindow("ScreenDecorOverlay")
+            .isAboveAppWindow("StatusBar")
+    }
+
+    @Test
+    fun canDetectAboveAppWindowVisibility_isInvisible() {
+        val subject = assertThat(trace).entry(9213763541297)
+        var failure = assertThrows(AssertionError::class.java) {
+            subject.isAboveAppWindow("pip-dismiss-overlay")
+        }
+        assertFailure(failure).factValue("Is Invisible").contains("pip-dismiss-overlay")
+
+        failure = assertThrows(AssertionError::class.java) {
+            subject.isAboveAppWindow("NavigationBar", isVisible = false)
+        }
+        assertFailure(failure).factValue("Is Visible").contains("NavigationBar")
+    }
+
+    @Test
+    fun canDetectWindowCoversAtLeastRegion_exactSize() {
+        val entry = assertThat(trace)
+            .entry(9213763541297)
+
+        entry.frameRegion("StatusBar").coversAtLeast(Region(0, 0, 1440, 171))
+        entry.frameRegion("com.google.android.apps.nexuslauncher")
+            .coversAtLeast(Region(0, 0, 1440, 2960))
+    }
+
+    @Test
+    fun canDetectWindowCoversAtLeastRegion_smallerRegion() {
+        val entry = assertThat(trace)
+            .entry(9213763541297)
+        entry.frameRegion("StatusBar").coversAtLeast(Region(0, 0, 100, 100))
+        entry.frameRegion("com.google.android.apps.nexuslauncher")
+            .coversAtLeast(Region(0, 0, 100, 100))
+    }
+
+    @Test
+    fun canDetectWindowCoversAtLeastRegion_largerRegion() {
+        val subject = assertThat(trace).entry(9213763541297)
+        var failure = assertThrows(FlickerSubjectException::class.java) {
+            subject.frameRegion("StatusBar").coversAtLeast(Region(0, 0, 1441, 171))
+        }
+        assertFailure(failure).factValue("Uncovered region").contains("SkRegion((1440,0,1441,171))")
+
+        failure = assertThrows(FlickerSubjectException::class.java) {
+            subject.frameRegion("com.google.android.apps.nexuslauncher")
+                .coversAtLeast(Region(0, 0, 1440, 2961))
+        }
+        assertFailure(failure).factValue("Uncovered region")
+            .contains("SkRegion((0,2960,1440,2961))")
+    }
+
+    @Test
+    fun canDetectWindowCoversAtMostRegion_extactSize() {
+        val entry = assertThat(trace)
+            .entry(9213763541297)
+        entry.frameRegion("StatusBar").coversAtMost(Region(0, 0, 1440, 171))
+        entry.frameRegion("com.google.android.apps.nexuslauncher")
+            .coversAtMost(Region(0, 0, 1440, 2960))
+    }
+
+    @Test
+    fun canDetectWindowCoversAtMostRegion_smallerRegion() {
+        val subject = assertThat(trace).entry(9213763541297)
+        var failure = assertThrows(FlickerSubjectException::class.java) {
+            subject.frameRegion("StatusBar").coversAtMost(Region(0, 0, 100, 100))
+        }
+        assertFailure(failure).factValue("Out-of-bounds region")
+            .contains("SkRegion((100,0,1440,100)(0,100,1440,171))")
+
+        failure = assertThrows(FlickerSubjectException::class.java) {
+            subject.frameRegion("com.google.android.apps.nexuslauncher")
+                .coversAtMost(Region(0, 0, 100, 100))
+        }
+        assertFailure(failure).factValue("Out-of-bounds region")
+            .contains("SkRegion((100,0,1440,100)(0,100,1440,2960))")
+    }
+
+    @Test
+    fun canDetectWindowCoversAtMostRegion_largerRegion() {
+        val entry = assertThat(trace)
+            .entry(9213763541297)
+
+        entry.frameRegion("StatusBar").coversAtMost(Region(0, 0, 1441, 171))
+        entry.frameRegion("com.google.android.apps.nexuslauncher")
+            .coversAtMost(Region(0, 0, 1440, 2961))
+    }
+
+    @Test
+    fun canDetectBelowAppWindowVisibility() {
+        assertThat(trace)
+            .entry(9213763541297)
+            .containsNonAppWindow("wallpaper")
+    }
+
+    @Test
+    fun canDetectAppWindowVisibility() {
+        assertThat(trace)
+            .entry(9213763541297)
+            .containsAppWindow("com.google.android.apps.nexuslauncher")
+
+        assertThat(trace)
+            .entry(9215551505798)
+            .containsAppWindow("com.android.chrome")
+    }
+
+    @Test
+    fun canFailWithReasonForVisibilityChecks_windowNotFound() {
+        val failure = assertThrows(FlickerSubjectException::class.java) {
+            assertThat(trace)
+                .entry(9213763541297)
+                .containsNonAppWindow("ImaginaryWindow")
+        }
+        assertFailure(failure).factValue("Could not find")
+            .contains("ImaginaryWindow")
+    }
+
+    @Test
+    fun canFailWithReasonForVisibilityChecks_windowNotVisible() {
+        val failure = assertThrows(FlickerSubjectException::class.java) {
+            assertThat(trace)
+                .entry(9213763541297)
+                .containsNonAppWindow("InputMethod")
+        }
+        assertFailure(failure).factValue("Is Invisible")
+            .contains("InputMethod")
+    }
+
+    @Test
+    fun canDetectAppZOrder() {
+        assertThat(trace)
+            .entry(9215551505798)
+            .containsAppWindow("com.google.android.apps.nexuslauncher", isVisible = true)
+            .showsAppWindowOnTop("com.android.chrome")
+    }
+
+    @Test
+    fun canFailWithReasonForZOrderChecks_windowNotOnTop() {
+        val failure = assertThrows(FlickerSubjectException::class.java) {
+            assertThat(trace)
+                .entry(9215551505798)
+                .showsAppWindowOnTop("com.google.android.apps.nexuslauncher")
+        }
+        assertFailure(failure)
+            .factValue("Found")
+            .contains("Splash Screen com.android.chrome")
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/WindowManagerTraceSubjectTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/WindowManagerTraceSubjectTest.kt
new file mode 100644
index 0000000..d44c8ba
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/WindowManagerTraceSubjectTest.kt
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2021 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
+
+import com.android.server.wm.flicker.traces.FlickerSubjectException
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject.Companion.assertThat
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+
+/**
+ * Contains [WindowManagerTraceSubject] tests. To run this test: `atest
+ * FlickerLibTest:WindowManagerTraceSubjectTest`
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class WindowManagerTraceSubjectTest {
+    private val chromeTrace by lazy { readWmTraceFromFile("wm_trace_openchrome.pb") }
+    private val imeTrace by lazy { readWmTraceFromFile("wm_trace_ime.pb") }
+
+    @Test
+    fun testVisibleAppWindowForRange() {
+        assertThat(chromeTrace)
+            .showsAppWindowOnTop("NexusLauncherActivity")
+            .showsAboveAppWindow("ScreenDecorOverlay")
+            .forRange(9213763541297L, 9215536878453L)
+        assertThat(chromeTrace)
+            .showsAppWindowOnTop("com.android.chrome")
+            .showsAppWindow("NexusLauncherActivity")
+            .showsAboveAppWindow("ScreenDecorOverlay")
+            .then()
+            .showsAppWindowOnTop("com.android.chrome")
+            .hidesAppWindow("NexusLauncherActivity")
+            .showsAboveAppWindow("ScreenDecorOverlay")
+            .forRange(9215551505798L, 9216093628925L)
+    }
+
+    @Test
+    fun testCanTransitionInAppWindow() {
+        assertThat(chromeTrace)
+            .showsAppWindowOnTop("NexusLauncherActivity")
+            .showsAboveAppWindow("ScreenDecorOverlay")
+            .then()
+            .showsAppWindowOnTop("com.android.chrome")
+            .showsAboveAppWindow("ScreenDecorOverlay")
+            .forAllEntries()
+    }
+
+    @Test
+    fun testCanInspectBeginning() {
+        assertThat(chromeTrace)
+            .first()
+            .showsAppWindowOnTop("NexusLauncherActivity")
+            .isAboveAppWindow("ScreenDecorOverlay")
+    }
+
+    @Test
+    fun testCanInspectAppWindowOnTop() {
+        assertThat(chromeTrace)
+            .first()
+            .showsAppWindowOnTop("NexusLauncherActivity", "InvalidWindow")
+
+        val failure = assertThrows(FlickerSubjectException::class.java) {
+            assertThat(chromeTrace)
+                .first()
+                .showsAppWindowOnTop("AnotherInvalidWindow", "InvalidWindow")
+                .fail("Could not detect the top app window")
+        }
+        assertFailure(failure).factValue("Could not find").contains("InvalidWindow")
+    }
+
+    @Test
+    fun testCanInspectEnd() {
+        assertThat(chromeTrace)
+            .last()
+            .showsAppWindowOnTop("com.android.chrome")
+            .isAboveAppWindow("ScreenDecorOverlay")
+    }
+
+    @Test
+    fun testCanTransitionNonAppWindow() {
+        assertThat(imeTrace)
+            .skipUntilFirstAssertion()
+            .hidesNonAppWindow("InputMethod")
+            .then()
+            .showsNonAppWindow("InputMethod")
+            .forAllEntries()
+    }
+
+    @Test(expected = AssertionError::class)
+    fun testCanDetectOverlappingWindows() {
+        assertThat(imeTrace)
+            .noWindowsOverlap("InputMethod", "NavigationBar", "ImeActivity")
+            .forAllEntries()
+    }
+
+    @Test
+    fun testCanTransitionAboveAppWindow() {
+        assertThat(imeTrace)
+            .skipUntilFirstAssertion()
+            .hidesAboveAppWindow("InputMethod")
+            .then()
+            .showsAboveAppWindow("InputMethod")
+            .forAllEntries()
+    }
+
+    @Test
+    fun testCanTransitionBelowAppWindow() {
+        val trace = readWmTraceFromFile("wm_trace_open_app_cold.pb")
+        assertThat(trace)
+            .skipUntilFirstAssertion()
+            .showsBelowAppWindow("Wallpaper")
+            .then()
+            .hidesBelowAppWindow("Wallpaper")
+            .forAllEntries()
+    }
+
+    @Test
+    fun testCanDetectVisibleWindowsMoreThanOneConsecutiveEntry() {
+        val trace = readWmTraceFromFile("wm_trace_valid_visible_windows.pb")
+        assertThat(trace).visibleWindowsShownMoreThanOneConsecutiveEntry().forAllEntries()
+    }
+}
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/WindowManagerTraceTest.java b/libraries/flicker/test/src/com/android/server/wm/flicker/WindowManagerTraceTest.java
deleted file mode 100644
index 9bdb3c3..0000000
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/WindowManagerTraceTest.java
+++ /dev/null
@@ -1,115 +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;
-
-import static com.android.server.wm.flicker.TestFileUtils.readTestFile;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.server.wm.flicker.Assertions.Result;
-
-import org.junit.Before;
-import org.junit.FixMethodOrder;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.MethodSorters;
-
-/**
- * Contains {@link WindowManagerTrace} tests. To run this test: {@code atest
- * FlickerLibTest:WindowManagerTraceTest}
- */
-@RunWith(AndroidJUnit4.class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-public class WindowManagerTraceTest {
-    private WindowManagerTrace mTrace;
-
-    private static WindowManagerTrace readWindowManagerTraceFromFile(String relativePath) {
-        try {
-            return WindowManagerTrace.parseFrom(readTestFile(relativePath));
-        } catch (Exception e) {
-            throw new RuntimeException(e);
-        }
-    }
-
-    @Before
-    public void setup() {
-        mTrace = readWindowManagerTraceFromFile("wm_trace_openchrome.pb");
-    }
-
-    @Test
-    public void canParseAllEntries() {
-        assertThat(mTrace.getEntries().get(0).getTimestamp()).isEqualTo(241777211939236L);
-        assertThat(mTrace.getEntries().get(mTrace.getEntries().size() - 1).getTimestamp())
-                .isEqualTo(241779809471942L);
-    }
-
-    @Test
-    public void canDetectAboveAppWindowVisibility() {
-        WindowManagerTrace.Entry entry = mTrace.getEntry(241777211939236L);
-        Result result = entry.isAboveAppWindowVisible("NavigationBar");
-        assertThat(result.passed()).isTrue();
-    }
-
-    @Test
-    public void canDetectBelowAppWindowVisibility() {
-        WindowManagerTrace.Entry entry = mTrace.getEntry(241777211939236L);
-        Result result = entry.isBelowAppWindowVisible("wallpaper");
-        assertThat(result.passed()).isTrue();
-    }
-
-    @Test
-    public void canDetectAppWindowVisibility() {
-        WindowManagerTrace.Entry entry = mTrace.getEntry(241777211939236L);
-        Result result = entry.isAppWindowVisible("com.google.android.apps.nexuslauncher");
-        assertThat(result.passed()).isTrue();
-    }
-
-    @Test
-    public void canFailWithReasonForVisibilityChecks_windowNotFound() {
-        WindowManagerTrace.Entry entry = mTrace.getEntry(241777211939236L);
-        Result result = entry.isAboveAppWindowVisible("ImaginaryWindow");
-        assertThat(result.failed()).isTrue();
-        assertThat(result.reason).contains("ImaginaryWindow cannot be found");
-    }
-
-    @Test
-    public void canFailWithReasonForVisibilityChecks_windowNotVisible() {
-        WindowManagerTrace.Entry entry = mTrace.getEntry(241777211939236L);
-        Result result = entry.isAboveAppWindowVisible("AssistPreviewPanel");
-        assertThat(result.failed()).isTrue();
-        assertThat(result.reason).contains("AssistPreviewPanel is invisible");
-    }
-
-    @Test
-    public void canDetectAppZOrder() {
-        WindowManagerTrace.Entry entry = mTrace.getEntry(241778130296410L);
-        Result result = entry.isVisibleAppWindowOnTop("com.google.android.apps.chrome");
-        assertThat(result.passed()).isTrue();
-    }
-
-    @Test
-    public void canFailWithReasonForZOrderChecks_windowNotOnTop() {
-        WindowManagerTrace.Entry entry = mTrace.getEntry(241778130296410L);
-        Result result = entry.isVisibleAppWindowOnTop("com.google.android.apps.nexuslauncher");
-        assertThat(result.failed()).isTrue();
-        assertThat(result.reason).contains("wanted=com.google.android.apps.nexuslauncher");
-        assertThat(result.reason)
-                .contains("found=com.android.chrome/" + "com.google.android.apps.chrome.Main");
-    }
-}
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/WindowManagerTraceTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/WindowManagerTraceTest.kt
new file mode 100644
index 0000000..8383938
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/WindowManagerTraceTest.kt
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2021 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
+
+import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace
+import com.android.server.wm.traces.common.windowmanager.WindowManagerState
+import com.android.server.wm.traces.common.windowmanager.windows.WindowContainer
+import com.android.server.wm.traces.parser.windowmanager.WindowManagerTraceParser
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+import java.lang.reflect.Modifier
+
+/**
+ * Contains [WindowManagerTrace] tests. To run this test: `atest
+ * FlickerLibTest:WindowManagerTraceTest`
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class WindowManagerTraceTest {
+    private val trace: WindowManagerTrace by lazy {
+        readWmTraceFromFile("wm_trace_openchrome.pb")
+    }
+
+    @Test
+    fun canParseAllEntries() {
+        val firstEntry = trace.entries[0]
+        assertThat(firstEntry.timestamp).isEqualTo(9213763541297L)
+        assertThat(firstEntry.windowStates.size).isEqualTo(10)
+        assertThat(firstEntry.visibleWindows.size).isEqualTo(6)
+        assertThat(trace.entries[trace.entries.size - 1].timestamp)
+                .isEqualTo(9216093628925L)
+    }
+
+    @Test
+    fun canDetectAppWindow() {
+        val appWindows = trace.getEntry(9213763541297L).appWindows
+        assertWithMessage("Unable to detect app windows").that(appWindows.size).isEqualTo(2)
+    }
+
+    @Test
+    fun canParseFromDump() {
+        val trace = try {
+            WindowManagerTraceParser.parseFromDump(
+                readTestFile("wm_trace_dump.pb"))
+        } catch (e: Exception) {
+            throw RuntimeException(e)
+        }
+        assertWithMessage("Unable to parse dump").that(trace.entries).hasSize(1)
+    }
+
+    /**
+     * Access all public methods and invokes all public getters from the object
+     * to check that all lazy properties contain valid values
+     */
+    private fun <T> Class<T>.accessProperties(obj: Any) {
+        val propertyValues = this.declaredFields
+            .filter { Modifier.isPublic(it.modifiers) }
+            .map { kotlin.runCatching { Pair(it.name, it.get(obj)) } }
+            .filter { it.isFailure }
+
+        assertWithMessage("The following properties could not be read: " +
+            propertyValues.joinToString("\n"))
+            .that(propertyValues)
+            .isEmpty()
+
+        val getterValues = this.declaredMethods
+            .filter {
+                Modifier.isPublic(it.modifiers) &&
+                    it.name.startsWith("get") &&
+                    it.parameterCount == 0
+            }
+            .map { kotlin.runCatching { Pair(it.name, it.invoke(obj)) } }
+            .filter { it.isFailure }
+
+        assertWithMessage("The following methods could not be invoked: " +
+            getterValues.joinToString("\n"))
+            .that(getterValues)
+            .isEmpty()
+
+        this.superclass?.accessProperties(obj)
+        if (obj is WindowContainer) {
+            obj.children.forEach { it::class.java.accessProperties(it) }
+        }
+    }
+
+    /**
+     * Tests if all properties of the flicker objects are accessible. This is necessary because
+     * most values are lazy initialized and only trigger errors when being accessed for the
+     * first time.
+     */
+    @Test
+    fun canAccessAllProperties() {
+        arrayOf("wm_trace_activity_transition.pb", "wm_trace_openchrome2.pb").forEach { traceName ->
+            val trace = readWmTraceFromFile(traceName)
+            assertWithMessage("Unable to parse dump")
+                .that(trace.entries.size)
+                .isGreaterThan(1)
+
+            trace.entries.forEach { entry: WindowManagerState ->
+                entry::class.java.accessProperties(entry)
+                entry.displays.forEach { it::class.java.accessProperties(it) }
+            }
+        }
+    }
+
+    @Test
+    fun canDetectValidState() {
+        val entry = trace.getEntry(9213763541297)
+        assertWithMessage("${entry.timestamp}: ${entry.getIsIncompleteReason()}")
+            .that(entry.isIncomplete())
+            .isFalse()
+    }
+
+    @Test
+    fun canDetectInvalidState() {
+        val entry = trace.getEntry(9215511235586)
+        assertWithMessage("${entry.timestamp}: ${entry.getIsIncompleteReason()}")
+            .that(entry.isIncomplete())
+            .isTrue()
+
+        assertThat(entry.getIsIncompleteReason())
+            .contains("No resumed activities found")
+    }
+}
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/WmTraceSubjectTest.java b/libraries/flicker/test/src/com/android/server/wm/flicker/WmTraceSubjectTest.java
deleted file mode 100644
index 481d930..0000000
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/WmTraceSubjectTest.java
+++ /dev/null
@@ -1,58 +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;
-
-import static com.android.server.wm.flicker.TestFileUtils.readTestFile;
-import static com.android.server.wm.flicker.WmTraceSubject.assertThat;
-
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.FixMethodOrder;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.MethodSorters;
-
-/**
- * Contains {@link WmTraceSubject} tests. To run this test: {@code atest
- * FlickerLibTest:WmTraceSubjectTest}
- */
-@RunWith(AndroidJUnit4.class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-public class WmTraceSubjectTest {
-    private static WindowManagerTrace readWmTraceFromFile(String relativePath) {
-        try {
-            return WindowManagerTrace.parseFrom(readTestFile(relativePath));
-        } catch (Exception e) {
-            throw new RuntimeException(e);
-        }
-    }
-
-    @Test
-    public void testCanTransitionInAppWindow() {
-        WindowManagerTrace trace = readWmTraceFromFile("wm_trace_openchrome2.pb");
-
-        assertThat(trace)
-                .showsAppWindowOnTop(
-                        "com.google.android.apps.nexuslauncher/" + ".NexusLauncherActivity")
-                .forRange(174684850717208L, 174685957511016L);
-        assertThat(trace)
-                .showsAppWindowOnTop("com.google.android.apps.nexuslauncher/.NexusLauncherActivity")
-                .then()
-                .showsAppWindowOnTop("com.android.chrome")
-                .forAllEntries();
-    }
-}
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/EventLogMonitorTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/EventLogMonitorTest.kt
new file mode 100644
index 0000000..c6debc5
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/EventLogMonitorTest.kt
@@ -0,0 +1,142 @@
+package com.android.server.wm.flicker.monitor
+
+import android.util.EventLog
+import com.android.server.wm.flicker.FlickerRunResult
+import com.android.server.wm.flicker.traces.eventlog.FocusEvent
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+/**
+ * Contains [EventLogMonitor] tests. To run this test: {@code
+ * atest FlickerLibTest:EventLogMonitorTest}
+ */
+class EventLogMonitorTest {
+    @Test
+    fun canCaptureFocusEventLogs() {
+        val monitor = EventLogMonitor()
+        EventLog.writeEvent(INPUT_FOCUS_TAG /* input_focus */,
+            "Focus entering 111 com.android.phone/" +
+                "com.android.phone.settings.fdn.FdnSetting (server)",
+            "reason=test")
+        EventLog.writeEvent(INPUT_FOCUS_TAG /* input_focus */,
+            "Focus leaving 222 com.google.android.apps.nexuslauncher/" +
+                "com.google.android.apps.nexuslauncher.NexusLauncherActivity (server)",
+            "reason=test")
+        EventLog.writeEvent(INPUT_FOCUS_TAG /* input_focus */,
+            "Focus entering 333 com.android.phone/" +
+                "com.android.phone.settings.fdn.FdnSetting (server)",
+            "reason=test")
+        monitor.start()
+        EventLog.writeEvent(INPUT_FOCUS_TAG /* input_focus */,
+            "Focus leaving 4749f88 com.android.phone/" +
+                "com.android.phone.settings.fdn.FdnSetting (server)",
+            "reason=test")
+        EventLog.writeEvent(INPUT_FOCUS_TAG /* input_focus */,
+            "Focus entering 7c01447 com.android.phone/" +
+                "com.android.phone.settings.fdn.FdnSetting (server)",
+            "reason=test")
+        monitor.stop()
+        EventLog.writeEvent(INPUT_FOCUS_TAG /* input_focus */,
+            "Focus entering 2aa30cd com.android.phone/" +
+                "com.android.phone.settings.fdn.FdnSetting (server)",
+            "reason=test")
+
+        val result = FlickerRunResult.Builder()
+        monitor.save("test", result)
+
+        assertEquals(2, result.eventLog?.size)
+        assertEquals(
+            "4749f88 com.android.phone/com.android.phone.settings.fdn.FdnSetting (server)",
+            result.eventLog?.get(0)?.window)
+        assertEquals(FocusEvent.Focus.LOST, result.eventLog?.get(0)?.focus)
+        assertEquals(
+            "7c01447 com.android.phone/com.android.phone.settings.fdn.FdnSetting (server)",
+            result.eventLog?.get(1)?.window)
+        assertEquals(FocusEvent.Focus.GAINED, result.eventLog?.get(1)?.focus)
+        assertTrue(result.eventLog?.get(0)?.timestamp ?: 0
+            <= result.eventLog?.get(1)?.timestamp ?: 0)
+        assertEquals(result.eventLog?.get(0)?.reason, "test")
+    }
+
+    @Test
+    fun onlyCapturesLastTransition() {
+        val monitor = EventLogMonitor()
+        monitor.start()
+        EventLog.writeEvent(INPUT_FOCUS_TAG /* input_focus */,
+            "Focus leaving 11111 com.android.phone/" +
+                "com.android.phone.settings.fdn.FdnSetting (server)",
+            "reason=test")
+        EventLog.writeEvent(INPUT_FOCUS_TAG /* input_focus */,
+            "Focus entering 22222 com.android.phone/" +
+                "com.android.phone.settings.fdn.FdnSetting (server)",
+            "reason=test")
+        monitor.stop()
+
+        monitor.start()
+        EventLog.writeEvent(INPUT_FOCUS_TAG /* input_focus */,
+            "Focus leaving 479f88 com.android.phone/" +
+                "com.android.phone.settings.fdn.FdnSetting (server)",
+            "reason=test")
+        EventLog.writeEvent(INPUT_FOCUS_TAG /* input_focus */,
+            "Focus entering 7c01447 com.android.phone/" +
+                "com.android.phone.settings.fdn.FdnSetting (server)",
+            "reason=test")
+        monitor.stop()
+
+        val result = FlickerRunResult.Builder()
+        monitor.save("test", result)
+
+        assertEquals(2, result.eventLog?.size)
+        assertEquals("479f88 " +
+            "com.android.phone/" +
+            "com.android.phone.settings.fdn.FdnSetting (server)",
+            result.eventLog?.get(0)?.window)
+        assertEquals(FocusEvent.Focus.LOST, result.eventLog?.get(0)?.focus)
+        assertEquals("7c01447 com.android.phone/" +
+            "com.android.phone.settings.fdn.FdnSetting (server)",
+            result.eventLog?.get(1)?.window)
+        assertEquals(FocusEvent.Focus.GAINED, result.eventLog?.get(1)?.focus)
+        assertTrue(result.eventLog?.get(0)?.timestamp ?: 0
+            <= result.eventLog?.get(1)?.timestamp ?: 0)
+    }
+
+    @Test
+    fun ignoreFocusRequestLogs() {
+        val monitor = EventLogMonitor()
+        monitor.start()
+        EventLog.writeEvent(INPUT_FOCUS_TAG /* input_focus */,
+                "Focus leaving 4749f88 com.android.phone/" +
+                        "com.android.phone.settings.fdn.FdnSetting (server)",
+                "reason=test")
+        EventLog.writeEvent(INPUT_FOCUS_TAG /* input_focus */,
+                "Focus request 111 com.android.phone/" +
+                        "com.android.phone.settings.fdn.FdnSetting (server)",
+                "reason=test")
+        EventLog.writeEvent(INPUT_FOCUS_TAG /* input_focus */,
+                "Focus entering 7c01447 com.android.phone/" +
+                        "com.android.phone.settings.fdn.FdnSetting (server)",
+                "reason=test")
+        monitor.stop()
+
+        val result = FlickerRunResult.Builder()
+        monitor.save("test", result)
+
+        assertEquals(2, result.eventLog?.size)
+        assertEquals(
+                "4749f88 com.android.phone/com.android.phone.settings.fdn.FdnSetting (server)",
+                result.eventLog?.get(0)?.window)
+        assertEquals(FocusEvent.Focus.LOST, result.eventLog?.get(0)?.focus)
+        assertEquals(
+                "7c01447 com.android.phone/com.android.phone.settings.fdn.FdnSetting (server)",
+                result.eventLog?.get(1)?.window)
+        assertEquals(FocusEvent.Focus.GAINED, result.eventLog?.get(1)?.focus)
+        assertTrue(result.eventLog?.get(0)?.timestamp ?: 0
+            <= result.eventLog?.get(1)?.timestamp ?: 0)
+        assertEquals(result.eventLog?.get(0)?.reason, "test")
+    }
+
+    private companion object {
+        const val INPUT_FOCUS_TAG = 62001
+    }
+}
\ No newline at end of file
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
deleted file mode 100644
index 849e593..0000000
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/LayersTraceMonitorTest.java
+++ /dev/null
@@ -1,89 +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 static android.surfaceflinger.nano.Layerstrace.LayersTraceFileProto.MAGIC_NUMBER_H;
-import static android.surfaceflinger.nano.Layerstrace.LayersTraceFileProto.MAGIC_NUMBER_L;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.surfaceflinger.nano.Layerstrace.LayersTraceFileProto;
-
-import androidx.test.runner.AndroidJUnit4;
-
-import com.google.common.io.Files;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.FixMethodOrder;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.MethodSorters;
-
-import java.io.File;
-import java.nio.file.Path;
-
-/**
- * Contains {@link LayersTraceMonitor} tests. To run this test: {@code atest
- * FlickerLibTest:LayersTraceMonitorTest}
- */
-@RunWith(AndroidJUnit4.class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-public class LayersTraceMonitorTest {
-    private LayersTraceMonitor mLayersTraceMonitor;
-
-    @Before
-    public void setup() {
-        mLayersTraceMonitor = new LayersTraceMonitor();
-    }
-
-    @After
-    public void teardown() {
-        mLayersTraceMonitor.stop();
-        mLayersTraceMonitor.getOutputTraceFilePath("captureLayersTrace").toFile().delete();
-    }
-
-    @Test
-    public void canStartLayersTrace() throws Exception {
-        mLayersTraceMonitor.start();
-        assertThat(mLayersTraceMonitor.isEnabled()).isTrue();
-    }
-
-    @Test
-    public void canStopLayersTrace() throws Exception {
-        mLayersTraceMonitor.start();
-        assertThat(mLayersTraceMonitor.isEnabled()).isTrue();
-        mLayersTraceMonitor.stop();
-        assertThat(mLayersTraceMonitor.isEnabled()).isFalse();
-    }
-
-    @Test
-    public void captureLayersTrace() throws Exception {
-        mLayersTraceMonitor.start();
-        mLayersTraceMonitor.stop();
-        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);
-        assertThat(mLayerTraceFileProto.magicNumber)
-                .isEqualTo((long) MAGIC_NUMBER_H << 32 | MAGIC_NUMBER_L);
-    }
-}
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/LayersTraceMonitorTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/LayersTraceMonitorTest.kt
new file mode 100644
index 0000000..8060511
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/LayersTraceMonitorTest.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.server.wm.flicker.monitor
+
+import android.surfaceflinger.nano.Layerstrace
+import androidx.test.uiautomator.UiDevice
+import com.android.server.wm.flicker.FlickerRunResult
+import com.google.common.truth.Truth
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+import java.nio.file.Path
+
+/**
+ * Contains [LayersTraceMonitor] tests. To run this test: `atest
+ * FlickerLibTest:LayersTraceMonitorTest`
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class LayersTraceMonitorTest : TraceMonitorTest<LayersTraceMonitor>() {
+    override fun getMonitor(outputDir: Path) = LayersTraceMonitor(outputDir)
+
+    override fun assertTrace(traceData: ByteArray) {
+        val trace = Layerstrace.LayersTraceFileProto.parseFrom(traceData)
+
+        Truth.assertThat(trace.magicNumber)
+                .isEqualTo(Layerstrace.LayersTraceFileProto.MAGIC_NUMBER_H.toLong() shl 32
+                        or Layerstrace.LayersTraceFileProto.MAGIC_NUMBER_L.toLong())
+    }
+
+    override fun getTraceFile(result: FlickerRunResult): Path? {
+        return result.traceFiles.firstOrNull { it.toString().contains("layers_trace") }
+    }
+
+    @Test
+    fun withSFTracing() {
+        val trace = withSFTracing {
+            val device = UiDevice.getInstance(instrumentation)
+            device.pressHome()
+            device.pressRecentApps()
+        }
+
+        Truth.assertWithMessage("Could not obtain SF trace")
+            .that(trace.entries)
+            .isNotEmpty()
+    }
+}
\ No newline at end of file
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
deleted file mode 100644
index 603590e..0000000
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/ScreenRecorderTest.java
+++ /dev/null
@@ -1,77 +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 static android.os.SystemClock.sleep;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.FixMethodOrder;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-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
- * FlickerLibTest:ScreenRecorderTest}
- */
-@RunWith(AndroidJUnit4.class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-public class ScreenRecorderTest {
-    private static final String TEST_VIDEO_FILENAME = "test.mp4";
-    private ScreenRecorder mScreenRecorder;
-    private Path mSavedVideoPath = null;
-
-    @Before
-    public void setup() {
-        mScreenRecorder = new ScreenRecorder();
-    }
-
-    @After
-    public void teardown() {
-        mScreenRecorder.getPath().toFile().delete();
-        if (mSavedVideoPath != null) {
-            mSavedVideoPath.toFile().delete();
-        }
-    }
-
-    @Test
-    public void videoIsRecorded() {
-        mScreenRecorder.start();
-        sleep(100);
-        mScreenRecorder.stop();
-        File file = mScreenRecorder.getPath().toFile();
-        assertThat(file.exists()).isTrue();
-    }
-
-    @Test
-    public void videoCanBeSaved() {
-        mScreenRecorder.start();
-        sleep(100);
-        mScreenRecorder.stop();
-        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/ScreenRecorderTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/ScreenRecorderTest.kt
new file mode 100644
index 0000000..c8ac97d
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/ScreenRecorderTest.kt
@@ -0,0 +1,82 @@
+/*
+ * 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.server.wm.flicker.monitor
+
+import android.app.Instrumentation
+import android.os.SystemClock
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.server.wm.flicker.FlickerRunResult
+import com.android.server.wm.flicker.getDefaultFlickerOutputDir
+import com.google.common.truth.Truth
+import org.junit.After
+import org.junit.Before
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+import java.nio.file.Files
+
+/**
+ * Contains [ScreenRecorder] tests. To run this test: `atest
+ * FlickerLibTest:ScreenRecorderTest`
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class ScreenRecorderTest {
+    private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    private lateinit var mScreenRecorder: ScreenRecorder
+    @Before
+    fun setup() {
+        val outputDir = getDefaultFlickerOutputDir()
+        mScreenRecorder = ScreenRecorder(outputDir, instrumentation.targetContext)
+    }
+
+    @After
+    fun teardown() {
+        mScreenRecorder.stop()
+        mScreenRecorder.outputPath.toFile().delete()
+    }
+
+    @Test
+    fun videoIsRecorded() {
+        mScreenRecorder.start()
+        SystemClock.sleep(100)
+        mScreenRecorder.stop()
+        val file = mScreenRecorder.outputPath.toFile()
+        Truth.assertWithMessage("Screen recording file not found")
+            .that(file.exists())
+            .isTrue()
+    }
+
+    @Test
+    fun videoCanBeSaved() {
+        mScreenRecorder.start()
+        SystemClock.sleep(100)
+        mScreenRecorder.stop()
+        val builder = FlickerRunResult.Builder()
+        mScreenRecorder.save("test", builder)
+        val traces = builder.buildAll().mapNotNull { result ->
+            result.traceFiles.firstOrNull {
+                it.toString().contains("transition")
+            }
+        }
+        traces.forEach {
+            Truth.assertWithMessage("Trace file $it not found").that(Files.exists(it)).isTrue()
+        }
+        traces.forEach {
+            Files.deleteIfExists(it)
+        }
+    }
+}
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/TraceMonitorTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/TraceMonitorTest.kt
new file mode 100644
index 0000000..39f70a5
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/TraceMonitorTest.kt
@@ -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 com.android.server.wm.flicker.monitor
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.server.wm.flicker.FlickerRunResult
+import com.android.server.wm.flicker.getDefaultFlickerOutputDir
+import com.android.server.wm.traces.parser.DeviceStateDump
+import com.google.common.io.Files
+import com.google.common.truth.Truth
+import org.junit.After
+import org.junit.Test
+import java.nio.file.Path
+
+abstract class TraceMonitorTest<T : TransitionMonitor> {
+
+    lateinit var savedTrace: Path
+    abstract fun getMonitor(outputDir: Path): T
+    abstract fun assertTrace(traceData: ByteArray)
+    abstract fun getTraceFile(result: FlickerRunResult): Path?
+
+    protected val instrumentation = InstrumentationRegistry.getInstrumentation()
+    protected val device = UiDevice.getInstance(instrumentation)
+    private val traceMonitor by lazy {
+        val outputDir = getDefaultFlickerOutputDir()
+        getMonitor(outputDir)
+    }
+
+    @After
+    fun teardown() {
+        device.pressHome()
+        if (traceMonitor.isEnabled) {
+            traceMonitor.stop()
+        }
+        if (::savedTrace.isInitialized) {
+            savedTrace.toFile().delete()
+        }
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun canStartLayersTrace() {
+        traceMonitor.start()
+        Truth.assertThat(traceMonitor.isEnabled).isTrue()
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun canStopLayersTrace() {
+        traceMonitor.start()
+        Truth.assertThat(traceMonitor.isEnabled).isTrue()
+        traceMonitor.stop()
+        Truth.assertThat(traceMonitor.isEnabled).isFalse()
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun captureLayersTrace() {
+        traceMonitor.start()
+        traceMonitor.stop()
+        val builder = FlickerRunResult.Builder()
+        traceMonitor.save("capturedTrace", builder)
+        val results = builder.buildAll()
+        Truth.assertWithMessage("Expected 3 results for the trace").that(results).hasSize(3)
+        val result = results.first()
+        savedTrace = getTraceFile(result) ?: error("Could not find saved trace file")
+        val testFile = savedTrace.toFile()
+        Truth.assertThat(testFile.exists()).isTrue()
+        val calculatedChecksum = TraceMonitor.calculateChecksum(savedTrace)
+        Truth.assertThat(calculatedChecksum).isEqualTo(traceMonitor.checksum)
+        val trace = Files.toByteArray(testFile)
+        Truth.assertThat(trace.size).isGreaterThan(0)
+        assertTrace(trace)
+    }
+
+    private fun validateTrace(dump: DeviceStateDump) {
+        Truth.assertWithMessage("Could not obtain SF trace")
+            .that(dump.layersTrace?.entries)
+            .isNotEmpty()
+        Truth.assertWithMessage("Could not obtain WM trace")
+            .that(dump.wmTrace?.entries)
+            .isNotEmpty()
+    }
+
+    @Test
+    fun withTracing() {
+        val trace = withTracing {
+            device.pressHome()
+            device.pressRecentApps()
+        }
+
+        this.validateTrace(trace)
+    }
+
+    @Test
+    fun recordTraces() {
+        val trace = recordTraces {
+            device.pressHome()
+            device.pressRecentApps()
+        }
+
+        val dump = DeviceStateDump.fromTrace(trace.first, trace.second)
+        this.validateTrace(dump)
+    }
+}
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/WindowAnimationFrameStatsMonitorTest.java b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/WindowAnimationFrameStatsMonitorTest.java
deleted file mode 100644
index 5123b87..0000000
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/WindowAnimationFrameStatsMonitorTest.java
+++ /dev/null
@@ -1,69 +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 static androidx.test.InstrumentationRegistry.getInstrumentation;
-
-import static com.android.server.wm.flicker.helpers.AutomationUtils.wakeUpAndGoToHomeScreen;
-
-import android.app.Instrumentation;
-import android.os.Bundle;
-import android.platform.helpers.IAppHelper;
-
-import androidx.test.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.server.wm.flicker.StandardAppHelper;
-
-import org.junit.Before;
-import org.junit.FixMethodOrder;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.MethodSorters;
-
-/**
- * Contains {@link WindowAnimationFrameStatsMonitor} tests. To run this test: {@code atest
- * FlickerLibTest:WindowAnimationFrameStatsMonitorTest}
- */
-@RunWith(AndroidJUnit4.class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-public class WindowAnimationFrameStatsMonitorTest {
-    private WindowAnimationFrameStatsMonitor mWindowAnimationFrameStatsMonitor;
-    private Instrumentation mInstrumentation;
-
-    @Before
-    public void setup() {
-        android.support.test.InstrumentationRegistry.registerInstance(
-                getInstrumentation(), new Bundle());
-        mInstrumentation = InstrumentationRegistry.getInstrumentation();
-        mWindowAnimationFrameStatsMonitor = new WindowAnimationFrameStatsMonitor(mInstrumentation);
-        wakeUpAndGoToHomeScreen();
-    }
-
-    @Test
-    public void captureWindowAnimationFrameStats() {
-        mWindowAnimationFrameStatsMonitor.start();
-        IAppHelper webViewBrowserHelper =
-                new StandardAppHelper(
-                        mInstrumentation,
-                        /* packageName */ "org.chromium.webview_shell",
-                        /* launcherName */ "WebView Browser Tester");
-        webViewBrowserHelper.open();
-        webViewBrowserHelper.exit();
-        mWindowAnimationFrameStatsMonitor.stop();
-    }
-}
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/WindowAnimationFrameStatsMonitorTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/WindowAnimationFrameStatsMonitorTest.kt
new file mode 100644
index 0000000..c45cb12
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/WindowAnimationFrameStatsMonitorTest.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.server.wm.flicker.monitor
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen
+import org.junit.Before
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+
+/**
+ * Contains [WindowAnimationFrameStatsMonitor] tests. To run this test: `atest
+ * FlickerLibTest:WindowAnimationFrameStatsMonitorTest`
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class WindowAnimationFrameStatsMonitorTest {
+    private val instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val frameStatsMonitor = WindowAnimationFrameStatsMonitor(instrumentation)
+    private val uiDevice = UiDevice.getInstance(instrumentation)
+
+    @Before
+    fun setup() {
+        uiDevice.wakeUpAndGoToHomeScreen()
+    }
+
+    @Test
+    fun captureWindowAnimationFrameStats() {
+        frameStatsMonitor.start()
+        uiDevice.pressRecentApps()
+        frameStatsMonitor.stop()
+    }
+}
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
deleted file mode 100644
index b234703..0000000
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/WindowManagerTraceMonitorTest.java
+++ /dev/null
@@ -1,90 +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 static com.android.server.wm.nano.WindowManagerTraceFileProto.MAGIC_NUMBER_H;
-import static com.android.server.wm.nano.WindowManagerTraceFileProto.MAGIC_NUMBER_L;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.server.wm.nano.WindowManagerTraceFileProto;
-
-import com.google.common.io.Files;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.FixMethodOrder;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.MethodSorters;
-
-import java.io.File;
-import java.nio.file.Path;
-
-/**
- * Contains {@link WindowManagerTraceMonitor} tests. To run this test: {@code atest
- * FlickerLibTest:WindowManagerTraceMonitorTest}
- */
-@RunWith(AndroidJUnit4.class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-public class WindowManagerTraceMonitorTest {
-    private WindowManagerTraceMonitor mWindowManagerTraceMonitor;
-
-    @Before
-    public void setup() {
-        mWindowManagerTraceMonitor = new WindowManagerTraceMonitor();
-    }
-
-    @After
-    public void teardown() {
-        mWindowManagerTraceMonitor.stop();
-        mWindowManagerTraceMonitor.getOutputTraceFilePath("captureWindowTrace").toFile().delete();
-    }
-
-    @Test
-    public void canStartWindowTrace() throws Exception {
-        mWindowManagerTraceMonitor.start();
-        assertThat(mWindowManagerTraceMonitor.isEnabled()).isTrue();
-    }
-
-    @Test
-    public void canStopWindowTrace() throws Exception {
-        mWindowManagerTraceMonitor.start();
-        assertThat(mWindowManagerTraceMonitor.isEnabled()).isTrue();
-        mWindowManagerTraceMonitor.stop();
-        assertThat(mWindowManagerTraceMonitor.isEnabled()).isFalse();
-    }
-
-    @Test
-    public void captureWindowTrace() throws Exception {
-        mWindowManagerTraceMonitor.start();
-        mWindowManagerTraceMonitor.stop();
-        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 =
-                WindowManagerTraceFileProto.parseFrom(trace);
-        assertThat(mWindowTraceFileProto.magicNumber)
-                .isEqualTo((long) MAGIC_NUMBER_H << 32 | MAGIC_NUMBER_L);
-    }
-}
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/WindowManagerTraceMonitorTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/WindowManagerTraceMonitorTest.kt
new file mode 100644
index 0000000..1cdf679
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/WindowManagerTraceMonitorTest.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.server.wm.flicker.monitor
+
+import androidx.test.uiautomator.UiDevice
+import com.android.server.wm.nano.WindowManagerTraceFileProto
+import com.android.server.wm.flicker.FlickerRunResult
+import com.google.common.truth.Truth
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+import java.nio.file.Path
+
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class WindowManagerTraceMonitorTest : TraceMonitorTest<WindowManagerTraceMonitor>() {
+
+    override fun getMonitor(outputDir: Path) = WindowManagerTraceMonitor(outputDir)
+
+    override fun assertTrace(traceData: ByteArray) {
+        val trace = WindowManagerTraceFileProto.parseFrom(traceData)
+        Truth.assertThat(trace.magicNumber)
+                .isEqualTo(WindowManagerTraceFileProto.MAGIC_NUMBER_H.toLong() shl 32
+                        or WindowManagerTraceFileProto.MAGIC_NUMBER_L.toLong())
+    }
+
+    override fun getTraceFile(result: FlickerRunResult): Path? {
+        return result.traceFiles.firstOrNull { it.toString().contains("wm_trace") }
+    }
+
+    @Test
+    fun withWMTracing() {
+        val trace = withWMTracing {
+            val device = UiDevice.getInstance(instrumentation)
+            device.pressHome()
+            device.pressRecentApps()
+        }
+
+        Truth.assertWithMessage("Could not obtain WM trace")
+            .that(trace.entries)
+            .isNotEmpty()
+    }
+}
\ No newline at end of file
diff --git a/libraries/health/options/tests/Android.bp b/libraries/health/options/tests/Android.bp
index ae3ca07..8767fba 100644
--- a/libraries/health/options/tests/Android.bp
+++ b/libraries/health/options/tests/Android.bp
@@ -18,7 +18,8 @@
 
 android_test {
     name: "PlatformTestOptionsTests",
-    sdk_version: "24",
+    sdk_version: "30",
+    min_sdk_version: "24",
     srcs: [ "src/**/*.java" ],
     static_libs: [
         "androidx.test.runner",
diff --git a/libraries/health/rules/Android.bp b/libraries/health/rules/Android.bp
index b32b73a..c72e8a8 100644
--- a/libraries/health/rules/Android.bp
+++ b/libraries/health/rules/Android.bp
@@ -26,6 +26,26 @@
         "guava",
         "memory-helper",
         "package-helper",
+        "launcher-aosp-tapl",
+        "flickerlib",
     ],
     srcs: ["src/**/*.java"],
 }
+
+java_library {
+    name: "platform-test-core-rules",
+    sdk_version: "test_current",
+    static_libs: [
+        "androidx.test.runner",
+        "androidx.test.uiautomator",
+        "app-helpers-handheld-interfaces",
+        "guava",
+        "memory-helper",
+        "package-helper",
+        "launcher-aosp-tapl",
+    ],
+    srcs: ["src/**/*.java"],
+    exclude_srcs: [
+        "src/android/platform/test/rule/flicker/**/*.java",
+    ],
+}
diff --git a/libraries/health/rules/src/android/platform/test/rule/FinishActivitiesWithoutProcessKillRule.java b/libraries/health/rules/src/android/platform/test/rule/FinishActivitiesWithoutProcessKillRule.java
new file mode 100644
index 0000000..23467f9
--- /dev/null
+++ b/libraries/health/rules/src/android/platform/test/rule/FinishActivitiesWithoutProcessKillRule.java
@@ -0,0 +1,123 @@
+/*
+ * 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.content.Context;
+import android.content.pm.PackageManager;
+import android.os.SystemClock;
+import android.util.Log;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.test.platform.app.InstrumentationRegistry;
+import org.junit.runner.Description;
+import org.junit.runners.model.InitializationError;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/** This rule finsihes all the activities associated with the given package name without killing
+ *  the process.
+ */
+public class FinishActivitiesWithoutProcessKillRule extends TestWatcher {
+
+    private static final String TAG = FinishActivitiesWithoutProcessKillRule.class.getSimpleName();
+    private static final long TOTAL_TIMEOUT = 10000;
+    private static final long TIMEOUT_INTERVAL = 2000;
+
+    private String mPkgName;
+    private Context mContext;
+    private final Pattern mAppActivityRecordPattern;
+
+    @VisibleForTesting
+    static final String FINISH_ACTIVITY_WITHOUT_PROCESS_KILL =
+            "finish-activity-without-process-kill";
+
+    public FinishActivitiesWithoutProcessKillRule() throws InitializationError {
+        throw new InitializationError("Must supply an application pkg name to"
+                + "finish the associated activities.");
+    }
+
+    public FinishActivitiesWithoutProcessKillRule(String appPackageName) {
+        mPkgName = appPackageName;
+        mContext = InstrumentationRegistry.getInstrumentation().getContext();
+        mAppActivityRecordPattern = Pattern.compile(
+                "ActivityRecord\\{.*"
+                        + appPackageName.replace(".", "\\.")
+                        + "/.*\\st([0-9]+)\\}");
+    }
+
+    @Override
+    protected void starting(Description description) {
+
+        if (!Boolean.parseBoolean(
+                getArguments().getString(FINISH_ACTIVITY_WITHOUT_PROCESS_KILL, "true"))) {
+            return;
+        }
+
+        if (!getAppActivityMatcher().find()) {
+            Log.e(TAG, "No activities associated with the package name is found.");
+            return;
+        }
+
+        // Needed to avoid security exception when changing the App enabled setting.
+        InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .adoptShellPermissionIdentity();
+        try {
+            // This will remove all the activities associated with the package name without
+            // killing the app.
+            mContext.getPackageManager().setApplicationEnabledSetting(mPkgName,
+                    PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
+
+            if (!waitUntilActivitiesRemoved()) {
+                throw new IllegalStateException("Activities not removed successfully.");
+            }
+
+            mContext.getPackageManager().setApplicationEnabledSetting(mPkgName,
+                    PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
+        } finally {
+            InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                    .dropShellPermissionIdentity();
+        }
+
+        super.starting(description);
+    }
+
+    /**
+     * Wait until the activities associated with the package name is removed within given timeout.
+     *
+     * @return true if the activities are removed successfully.
+     */
+    private boolean waitUntilActivitiesRemoved() {
+        long startTime = SystemClock.uptimeMillis();
+        while (SystemClock.uptimeMillis() - startTime <= TOTAL_TIMEOUT) {
+            if (getAppActivityMatcher().find()) {
+                Log.v(TAG, String.format("Waiting for another %f secs", TIMEOUT_INTERVAL / 1000f));
+                SystemClock.sleep(TIMEOUT_INTERVAL);
+            } else {
+                Log.v(TAG,
+                        "Activities associated with the package name were removed successfully.");
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private Matcher getAppActivityMatcher() {
+        return mAppActivityRecordPattern
+                .matcher(executeShellCommand("dumpsys activity activities"));
+    }
+}
diff --git a/libraries/health/rules/src/android/platform/test/rule/LandscapeOrientationRule.java b/libraries/health/rules/src/android/platform/test/rule/LandscapeOrientationRule.java
new file mode 100644
index 0000000..d7bad34
--- /dev/null
+++ b/libraries/health/rules/src/android/platform/test/rule/LandscapeOrientationRule.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2021 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 android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+
+import static org.junit.Assert.assertEquals;
+
+import android.os.RemoteException;
+
+import org.junit.runner.Description;
+
+/**
+ * Locks landscape orientation before running a test and goes back to natural orientation
+ * afterwards.
+ */
+public class LandscapeOrientationRule extends TestWatcher {
+
+    @Override
+    protected void starting(Description description) {
+        try {
+            getUiDevice().setOrientationNatural();
+            int currentOrientation = getContext().getResources().getConfiguration().orientation;
+            if (currentOrientation != ORIENTATION_LANDSCAPE) { // ORIENTATION_PORTRAIT
+                getUiDevice().setOrientationLeft();
+                int rotatedOrientation = getContext().getResources().getConfiguration().orientation;
+                assertEquals(
+                        "Orientation should be landscape",
+                        ORIENTATION_LANDSCAPE,
+                        rotatedOrientation);
+            }
+        } catch (RemoteException e) {
+            String message = "RemoteException when forcing landscape rotation on the device";
+            throw new RuntimeException(message, e);
+        }
+    }
+
+    @Override
+    protected void finished(Description description) {
+        try {
+            if (!getUiDevice().isNaturalOrientation()) {
+                getUiDevice().setOrientationNatural();
+            }
+            getUiDevice().unfreezeRotation();
+        } catch (RemoteException e) {
+            String message = "RemoteException when restoring natural rotation of the device";
+            throw new RuntimeException(message, e);
+        }
+    }
+}
diff --git a/libraries/health/rules/src/android/platform/test/rule/NavigationModeRule.java b/libraries/health/rules/src/android/platform/test/rule/NavigationModeRule.java
new file mode 100644
index 0000000..f747906
--- /dev/null
+++ b/libraries/health/rules/src/android/platform/test/rule/NavigationModeRule.java
@@ -0,0 +1,115 @@
+/*
+ * 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 androidx.test.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.systemui.shared.system.QuickStepContract.NAV_BAR_MODE_2BUTTON_OVERLAY;
+import static com.android.systemui.shared.system.QuickStepContract.NAV_BAR_MODE_3BUTTON_OVERLAY;
+import static com.android.systemui.shared.system.QuickStepContract.NAV_BAR_MODE_GESTURAL_OVERLAY;
+
+import android.content.pm.PackageManager;
+import android.os.SystemClock;
+import android.util.Log;
+
+import androidx.test.uiautomator.UiDevice;
+
+import com.android.launcher3.tapl.LauncherInstrumentation;
+import com.android.systemui.shared.system.QuickStepContract;
+
+import org.junit.runner.Description;
+
+import java.io.IOException;
+
+/** This rule runs the test in the specified navigation mode. */
+public class NavigationModeRule extends TestWatcher {
+    private static final String TAG = "NavigationModeRule";
+
+    private final String mRequestedOverlayPackage;
+    private final String mOriginalOverlayPackage;
+
+    public NavigationModeRule(String requestedOverlayPackage) {
+        mRequestedOverlayPackage = requestedOverlayPackage;
+        mOriginalOverlayPackage =
+                getCurrentOverlayPackage(
+                        LauncherInstrumentation.getCurrentInteractionMode(
+                                getInstrumentation().getContext()));
+    }
+
+    private static boolean packageExists(String overlayPackage) {
+        try {
+            PackageManager pm = getInstrumentation().getContext().getPackageManager();
+            if (pm.getApplicationInfo(overlayPackage, 0 /* flags */) == null) {
+                return false;
+            }
+        } catch (PackageManager.NameNotFoundException e) {
+            return false;
+        }
+        return true;
+    }
+
+    private static boolean setActiveOverlay(String overlayPackage) {
+        if (!packageExists(overlayPackage)) {
+            Log.d(TAG, "setActiveOverlay: " + overlayPackage + " pkg does not exist");
+            return false;
+        }
+
+        Log.d(TAG, "setActiveOverlay: " + overlayPackage + "...");
+        try {
+            UiDevice.getInstance(getInstrumentation()).executeShellCommand(
+                    "cmd overlay enable-exclusive --category " + overlayPackage);
+        } catch (IOException e) {
+            e.printStackTrace();
+            return false;
+        }
+
+        SystemClock.sleep(2000);
+        return true;
+    }
+
+    private static String getCurrentOverlayPackage(int currentInteractionMode) {
+        return QuickStepContract.isGesturalMode(currentInteractionMode)
+                ? NAV_BAR_MODE_GESTURAL_OVERLAY
+                : QuickStepContract.isSwipeUpMode(currentInteractionMode)
+                        ? NAV_BAR_MODE_2BUTTON_OVERLAY
+                        : NAV_BAR_MODE_3BUTTON_OVERLAY;
+    }
+
+    @Override
+    protected void starting(Description description) {
+        if (!mOriginalOverlayPackage.equals(mRequestedOverlayPackage)) {
+            if (!setActiveOverlay(mRequestedOverlayPackage)) {
+                throw new RuntimeException(
+                        "Couldn't set the requested overlay package " + mRequestedOverlayPackage);
+            }
+        }
+
+        super.starting(description);
+    }
+
+    @Override
+    protected void finished(Description description) {
+        super.finished(description);
+
+        if (!mOriginalOverlayPackage.equals(mRequestedOverlayPackage)) {
+            if (!setActiveOverlay(mOriginalOverlayPackage)) {
+                throw new RuntimeException(
+                        "Couldn't set the original overlay package " + mOriginalOverlayPackage);
+            }
+        }
+    }
+}
diff --git a/libraries/health/rules/src/android/platform/test/rule/RemoveAppFromStackRule.java b/libraries/health/rules/src/android/platform/test/rule/RemoveAppFromStackRule.java
new file mode 100644
index 0000000..318af50
--- /dev/null
+++ b/libraries/health/rules/src/android/platform/test/rule/RemoveAppFromStackRule.java
@@ -0,0 +1,74 @@
+/*
+ * 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.util.Log;
+
+import androidx.annotation.VisibleForTesting;
+import org.junit.runner.Description;
+import org.junit.runners.model.InitializationError;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/** This rule removes all the activities associated with the given package name
+ *  from the activity manager stack.
+ */
+public class RemoveAppFromStackRule extends TestWatcher {
+
+    private static final String TAG = RemoveAppFromStackRule.class.getSimpleName();
+
+    private final Pattern mAppActivityRecordPattern;
+
+    @VisibleForTesting
+    static final String REMOVE_APP_FROM_AM_STACK = "rm-app-am-stack";
+
+    public RemoveAppFromStackRule() throws InitializationError {
+        throw new InitializationError("Must supply an application pkg name to"
+                + "remove from am stack.");
+    }
+
+    public RemoveAppFromStackRule(String appPackageName) {
+        mAppActivityRecordPattern =
+                Pattern.compile(
+                        "ActivityRecord\\{.*"
+                                + appPackageName.replace(".", "\\.")
+                                + "/.*\\st([0-9]+)\\}");
+    }
+
+    @Override
+    protected void starting(Description description) {
+        // Check if removing the app from am stack is chosen or not.
+        boolean removeAppFromAm = Boolean
+                .parseBoolean(getArguments().getString(REMOVE_APP_FROM_AM_STACK, "true"));
+        if (!removeAppFromAm) {
+            return;
+        }
+
+        // Remove the stack of the app.
+        final Matcher appActivityMatcher = mAppActivityRecordPattern.matcher(
+                executeShellCommand("dumpsys activity activities"));
+        if (appActivityMatcher.find()) {
+            executeShellCommand("am stack remove " + appActivityMatcher.group(1));
+            Log.v(TAG, "Removed the app from AM stack");
+        } else {
+            Log.e(TAG, "No activities stack associated with the package name is found.");
+        }
+
+        super.starting(description);
+    }
+}
diff --git a/libraries/health/rules/src/android/platform/test/rule/flicker/FlickerRuleBase.java b/libraries/health/rules/src/android/platform/test/rule/flicker/FlickerRuleBase.java
new file mode 100644
index 0000000..aeaceb5
--- /dev/null
+++ b/libraries/health/rules/src/android/platform/test/rule/flicker/FlickerRuleBase.java
@@ -0,0 +1,317 @@
+/*
+ * Copyright (C) 2021 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.flicker;
+
+import android.os.Environment;
+import android.platform.test.rule.TestWatcher;
+import android.util.Log;
+
+import com.android.server.wm.flicker.monitor.LayersTraceMonitor;
+import com.android.server.wm.flicker.monitor.WindowManagerTraceMonitor;
+import com.android.server.wm.traces.common.layers.LayersTrace;
+import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace;
+import com.android.server.wm.traces.parser.layers.LayersTraceParser;
+import com.android.server.wm.traces.parser.windowmanager.WindowManagerTraceParser;
+
+import com.google.common.io.Files;
+import java.util.HashMap;
+import java.util.Map;
+import java.nio.file.Paths;
+import java.io.File;
+import org.junit.runner.Description;
+
+/**
+ * Base class that encapsulates the logic for collecting and parsing
+ * the window manager trace and surface flinger trace.
+ */
+public abstract class FlickerRuleBase extends TestWatcher {
+
+    private static final String TAG = FlickerRuleBase.class.getSimpleName();
+
+    // Suffix is added by the trace monitor at the time of saving the file.
+    private static final String WM_TRACE_FILE_SUFFIX = "wm_trace.pb";
+
+    // Suffix is added by the trace monitor at the time of saving the file.
+    private static final String LAYERS_TRACE_FILE_SUFFIX = "layers_trace.pb";
+
+    // Option to customize the root directory to save the trace files.
+    private static final String TRACE_DIRECTORY_ROOT = "trace-directory-root";
+
+    // To keep track of the method name and the current iteration count.
+    private static Map<String, Integer> mMethodNameCount = new HashMap<>();
+    private static boolean updateMethodCount = true;
+    private boolean mIsWmTraceEnabled = false;
+    private boolean mIsLayersTraceEnabled = false;
+    private WindowManagerTraceMonitor mWmTraceMonitor;
+    private LayersTraceMonitor mLayersTraceMonitor;
+    private String mTraceDirectoryRoot = null;
+    private boolean mIsRuleDisabled= false;
+    private WindowManagerTrace mWindowManagerTrace;
+    private LayersTrace mLayerTrace;
+
+    public FlickerRuleBase() {
+    }
+
+    @Override
+    protected void starting(Description description) {
+        if (!mIsWmTraceEnabled && !mIsLayersTraceEnabled) {
+            Log.v(TAG, "Both surface flinger and layer tracing are disabled.");
+            return;
+        }
+
+        setupRootDirectory();
+
+        // Verify if WM tracing is already started by another window manager based
+        // rule otherwise proceed with starting the WM trace.
+        if (mIsWmTraceEnabled) {
+            if (!mWmTraceMonitor.isEnabled()) {
+                mWmTraceMonitor.start();
+                Log.v(TAG, "WM trace started successfully.");
+            } else {
+                Log.v(TAG, "WM trace already enabled.");
+            }
+        }
+
+        // Verify if layers tracing is already started by another layers based
+        // rule otherwise proceed with starting the layer trace.
+        if (mIsLayersTraceEnabled) {
+            if (!mLayersTraceMonitor.isEnabled()) {
+                mLayersTraceMonitor.start();
+                Log.v(TAG, "Layers trace started successfully.");
+            } else {
+                Log.v(TAG, "Layers trace already enabled.");
+            }
+        }
+
+        // If traces are already started by other window manager or layer based rule
+        // then exit without incrementing the method iteration count.
+        if (!updateMethodCount) {
+            return;
+        }
+
+        // Update the method name with current iteration count.
+        if (mMethodNameCount.containsKey(description.toString())) {
+            mMethodNameCount.put(description.toString(),
+                    mMethodNameCount.get(description.toString()) + 1);
+        } else {
+            mMethodNameCount.put(description.toString(), 1);
+        }
+
+        // Cleanup the trace file from previous test runs.
+        cleanupTraceFiles(description);
+
+        updateMethodCount = false;
+    }
+
+    @Override
+    protected void finished(Description description) {
+
+        updateMethodCount = true;
+
+        // Verify if WM tracing is already stopped by another window manager based
+        // rule. Otherwise proceed with stopping the trace.
+        if (mIsWmTraceEnabled) {
+            if (mWmTraceMonitor.isEnabled()) {
+                mWmTraceMonitor.stop();
+                Log.v(TAG, "WM trace stopped successfully.");
+            } else {
+                Log.v(TAG, "WM trace already stopped.");
+            }
+        }
+
+        if (mIsWmTraceEnabled) {
+            // Verify if WM trace file already exist for the current test. It could have been
+            // created by another Window manager based rule.
+            if (!new File(getFinalTraceFilePath(description, WM_TRACE_FILE_SUFFIX)).exists()) {
+                // Appends the trace file suffix "_wm_trace.pb" and store it under the root
+                // directory.
+                mWmTraceMonitor.save(getFileNamePrefix(description));
+                Log.v(TAG, "WM trace successfully saved in the destination folder.");
+            } else {
+                Log.v(TAG, "WM trace already saved in the destination folder.");
+            }
+            mWindowManagerTrace = getWindowManagerTrace(
+                    getFinalTraceFilePath(description, WM_TRACE_FILE_SUFFIX));
+        }
+
+        // Verify if layers tracing is already stopped by another layers based
+        // rule. Otherwise proceed with stopping the trace.
+        if (mIsLayersTraceEnabled) {
+            if (mLayersTraceMonitor.isEnabled()) {
+                mLayersTraceMonitor.stop();
+                Log.v(TAG, "Layers trace stopped successfully.");
+            } else {
+                Log.v(TAG, "Layers trace already stopped.");
+            }
+        }
+
+        if (mIsLayersTraceEnabled) {
+            // Verify if layer trace file already exist for the current test. It could have been
+            // created by another layers based rule.
+            if (!new File(getFinalTraceFilePath(description, LAYERS_TRACE_FILE_SUFFIX)).exists()) {
+                // Appends the trace file suffix "_layers_trace.pb" and store it under the root
+                // directory.
+                mLayersTraceMonitor.save(getFileNamePrefix(description));
+                Log.v(TAG, "Layers trace successfully saved in the destination folder.");
+            } else {
+                Log.v(TAG, "Layers trace already saved in the destination folder.");
+            }
+            mLayerTrace = getLayersTrace(
+                    getFinalTraceFilePath(description, LAYERS_TRACE_FILE_SUFFIX));
+        }
+
+        validateFlickerConditions();
+    }
+
+    /**
+    * Setup the root directory to save the WM traces and layers trace collected during the test.
+    */
+    private void setupRootDirectory() {
+        mTraceDirectoryRoot = getArguments().getString(TRACE_DIRECTORY_ROOT,
+                Environment.getExternalStorageDirectory().getPath() + "/flicker_trace/");
+        if (!mTraceDirectoryRoot.endsWith("/")) {
+            mTraceDirectoryRoot = new StringBuilder(mTraceDirectoryRoot).append("/").toString();
+        }
+
+        // Create root directory if it does not already exist.
+        File rootDirectory = new File(mTraceDirectoryRoot);
+        if (!rootDirectory.exists()) {
+            if (rootDirectory.mkdirs()) {
+                Log.v(TAG, "Trace root directory created successfully.");
+            } else {
+                throw new RuntimeException(
+                        "Unable to create the trace root directory." + mTraceDirectoryRoot);
+            }
+        } else {
+            Log.v(TAG, "Trace root directory already exists.");
+        }
+
+        if (mIsWmTraceEnabled) {
+            mWmTraceMonitor = new WindowManagerTraceMonitor(Paths.get(mTraceDirectoryRoot));
+        }
+
+        if (mIsLayersTraceEnabled) {
+            mLayersTraceMonitor = new LayersTraceMonitor(Paths.get(mTraceDirectoryRoot));
+        }
+    }
+
+    /**
+     * Remove the WM trace and layers trace files collected from previous test runs.
+     */
+    private void cleanupTraceFiles(Description description) {
+        if (new File(getFinalTraceFilePath(description, WM_TRACE_FILE_SUFFIX)).exists()) {
+            new File(getFinalTraceFilePath(description, WM_TRACE_FILE_SUFFIX)).delete();
+            Log.v(TAG, "Removed the already existing wm trace file.");
+        }
+
+        if (new File(getFinalTraceFilePath(description, LAYERS_TRACE_FILE_SUFFIX)).exists()) {
+            new File(getFinalTraceFilePath(description, LAYERS_TRACE_FILE_SUFFIX)).delete();
+            Log.v(TAG, "Removed the already existing layers trace file.");
+        }
+    }
+
+    /**
+     * Retrieve the path of the trace file in the device.
+     *
+     * @param description
+     * @param suffix
+     * @return path to the trace file.
+     */
+    private String getFinalTraceFilePath(Description description, String suffix) {
+        return String.format(
+                "%s%s_%s", mTraceDirectoryRoot, getFileNamePrefix(description),
+                suffix);
+    }
+
+    /**
+     * Construct file name using the class name, method name and the current iteration count
+     * of the method.
+     *
+     * @param description
+     * @return the file name used to save the WM trace proto file.
+     */
+    private String getFileNamePrefix(Description description) {
+        return description.getClassName() + "_" + description.getMethodName() + "_"
+                + mMethodNameCount.get(description.toString());
+    }
+
+    /**
+     * Parse the window manager trace file.
+     *
+     * @param finalTraceFilePath
+     * @return parsed window manager trace.
+     */
+    private WindowManagerTrace getWindowManagerTrace(String finalTraceFilePath) {
+        Log.v(TAG, "Processing window manager trace file.");
+        try {
+            byte[] wmTraceByteArray = Files.toByteArray(new File(finalTraceFilePath));
+            if (wmTraceByteArray != null) {
+                WindowManagerTrace wmTrace = WindowManagerTraceParser
+                        .parseFromTrace(wmTraceByteArray);
+                return wmTrace;
+            } else {
+                throw new RuntimeException("Window manager trace contents are empty.");
+            }
+        } catch (Exception e) {
+            throw new RuntimeException("Unable to read the proto file." + finalTraceFilePath);
+        }
+    }
+
+    /**
+     * Parse the layers trace file.
+     *
+     * @param finalTraceFilePath
+     * @return parsed layers trace.
+     */
+    private LayersTrace getLayersTrace(String finalTraceFilePath) {
+        Log.v(TAG, "Processing layers trace file.");
+        try {
+            byte[] layersTraceByteArray = Files.toByteArray(new File(finalTraceFilePath));
+            if (layersTraceByteArray != null) {
+                LayersTrace layersTrace = LayersTraceParser
+                        .parseFromTrace(layersTraceByteArray);
+                return layersTrace;
+            } else {
+                throw new RuntimeException("layers trace contents are empty.");
+            }
+        } catch (Exception e) {
+            throw new RuntimeException("Unable to read the proto file." + finalTraceFilePath);
+        }
+    }
+
+    /**
+     * Child class should implement this method to test for flicker conditions.
+     *
+     */
+    protected abstract void validateFlickerConditions();
+
+    protected void enableWmTrace() {
+        mIsWmTraceEnabled = true;
+    }
+
+    protected void enableLayerTrace() {
+        mIsLayersTraceEnabled = true;
+    }
+
+    protected WindowManagerTrace getWindowManagerTrace() {
+        return mWindowManagerTrace;
+    }
+
+    protected LayersTrace getLayersTrace() {
+        return mLayerTrace;
+    }
+}
diff --git a/libraries/health/rules/src/android/platform/test/rule/flicker/LayersFlickerRuleBase.java b/libraries/health/rules/src/android/platform/test/rule/flicker/LayersFlickerRuleBase.java
new file mode 100644
index 0000000..e52c5de
--- /dev/null
+++ b/libraries/health/rules/src/android/platform/test/rule/flicker/LayersFlickerRuleBase.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2021 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.flicker;
+
+import android.os.Environment;
+import android.util.Log;
+
+import com.android.server.wm.traces.common.layers.LayersTrace;
+
+/**
+ * Base class that encapsulates the logic for enabling the layers trace, parsing the
+ * surface flinger trace. Extend this class to add validation for layers trace based
+ * flicker conditions.
+ */
+public abstract class LayersFlickerRuleBase extends FlickerRuleBase {
+
+    private static final String TAG = LayersFlickerRuleBase.class.getSimpleName();
+
+    LayersFlickerRuleBase() {
+        enableLayerTrace();
+        Log.v(TAG, "Enabled the surface flinger trace.");
+    }
+
+    protected void validateFlickerConditions() {
+        validateLayersFlickerConditions(getLayersTrace());
+    }
+
+    /**
+     * Override this method to provide layers trace based flicker validations.
+     *
+     * @param layersTrace
+     */
+    protected abstract void validateLayersFlickerConditions(LayersTrace layersTrace);
+}
diff --git a/libraries/health/rules/src/android/platform/test/rule/flicker/LayersFlickerRuleCommon.java b/libraries/health/rules/src/android/platform/test/rule/flicker/LayersFlickerRuleCommon.java
new file mode 100644
index 0000000..86b2801
--- /dev/null
+++ b/libraries/health/rules/src/android/platform/test/rule/flicker/LayersFlickerRuleCommon.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2021 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.flicker;
+
+import android.util.Log;
+
+import com.android.server.wm.traces.common.layers.LayersTrace;
+
+/**
+ * Rule used for validating the common layers trace based flicker assertions applicable
+ * for all the CUJ's
+ */
+public class LayersFlickerRuleCommon extends LayersFlickerRuleBase {
+
+    private static final String TAG = LayersFlickerRuleCommon.class.getSimpleName();
+
+    protected void validateLayersFlickerConditions(LayersTrace layersTrace) {
+
+        // TODO: b/183516705 Implementation for layer trace based common validations.
+        Log.v(TAG, "Successfully verified the common layers flicker conditions.");
+    }
+}
diff --git a/libraries/health/rules/src/android/platform/test/rule/flicker/WindowManagerFlickerRuleBase.java b/libraries/health/rules/src/android/platform/test/rule/flicker/WindowManagerFlickerRuleBase.java
new file mode 100644
index 0000000..387cfc1
--- /dev/null
+++ b/libraries/health/rules/src/android/platform/test/rule/flicker/WindowManagerFlickerRuleBase.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2021 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.flicker;
+
+import android.os.Environment;
+import android.util.Log;
+
+import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace;
+
+/**
+ * Base class that encapsulates the logic for enabling the window manager trace, parsing the
+ * window manager trace. Extend this class to add validation for window manager trace based
+ * flicker conditions.
+ */
+public abstract class WindowManagerFlickerRuleBase extends FlickerRuleBase {
+
+    private static final String TAG = WindowManagerFlickerRuleBase.class.getSimpleName();
+
+    WindowManagerFlickerRuleBase() {
+        enableWmTrace();
+        Log.v(TAG, "Enabled the window manager trace.");
+    }
+
+    protected void validateFlickerConditions() {
+        validateWMFlickerConditions(getWindowManagerTrace());
+    }
+
+    /**
+     * Override this method to provide window manager trace based flicker validations.
+     *
+     * @param windowManagerTrace
+     */
+    protected abstract void validateWMFlickerConditions(WindowManagerTrace windowManagerTrace);
+}
diff --git a/libraries/health/rules/src/android/platform/test/rule/flicker/WindowManagerFlickerRuleCommon.java b/libraries/health/rules/src/android/platform/test/rule/flicker/WindowManagerFlickerRuleCommon.java
new file mode 100644
index 0000000..032e943
--- /dev/null
+++ b/libraries/health/rules/src/android/platform/test/rule/flicker/WindowManagerFlickerRuleCommon.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2021 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.flicker;
+
+import android.util.Log;
+
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject;
+import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace;
+import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper;
+
+import java.util.LinkedList;
+
+/**
+ * Rule used for validating the common window manager trace based flicker assertions applicable
+ * for all the CUJ's
+ */
+public class WindowManagerFlickerRuleCommon extends WindowManagerFlickerRuleBase {
+
+    private static final String TAG = WindowManagerFlickerRuleCommon.class.getSimpleName();
+
+    protected void validateWMFlickerConditions(WindowManagerTrace wmTrace) {
+        // Verify that there’s an non-app window with names NavigationBar, StatusBar above
+        // the app window and is visible in all winscope log entries.
+        WindowManagerTraceSubject.assertThat(wmTrace)
+                .showsAboveAppWindow(WindowManagerStateHelper.NAV_BAR_WINDOW_NAME)
+                .showsAboveAppWindow(WindowManagerStateHelper.STATUS_BAR_WINDOW_NAME)
+                .forAllEntries();
+
+        // Verify that all visible windows are visible for more than one consecutive entry
+        // in the log entries.
+        WindowManagerTraceSubject.assertThat(wmTrace)
+                .visibleWindowsShownMoreThanOneConsecutiveEntry(new LinkedList<String>())
+                .forAllEntries();
+        Log.v(TAG, "Successfully verified the window manager flicker conditions.");
+    }
+}
diff --git a/libraries/health/rules/tests/src/android/platform/test/rule/FinishActivitiesWithoutProcessKillRuleTest.java b/libraries/health/rules/tests/src/android/platform/test/rule/FinishActivitiesWithoutProcessKillRuleTest.java
new file mode 100644
index 0000000..759cafe
--- /dev/null
+++ b/libraries/health/rules/tests/src/android/platform/test/rule/FinishActivitiesWithoutProcessKillRuleTest.java
@@ -0,0 +1,122 @@
+/*
+ * 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.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 FinishActivitiesWithoutProcessKillRule}
+ */
+@RunWith(JUnit4.class)
+public class FinishActivitiesWithoutProcessKillRuleTest {
+
+    private final static String DUMPSYS_NO_ACTIVITY_OUTPUT = "";
+
+    /**
+     * Tests that this rule will fail to register if no package name is supplied
+     * to finish the activities.
+     */
+    @Test
+    public void testNoAppToFinishActivitiesFails() {
+        try {
+            FinishActivitiesWithoutProcessKillRule rule =
+                    new FinishActivitiesWithoutProcessKillRule();
+            fail("An initialization error should have been thrown, but wasn't.");
+        } catch (InitializationError e) {
+            return;
+        }
+    }
+
+    /**
+     * Tests that this rule will finish the activities associated with the package.
+     */
+    @Test
+    public void testFinishActivities() throws Throwable {
+        TestableRule rule = new TestableRule(new Bundle(), "example.package.name");
+        rule.apply(rule.getTestStatement(), Description.createTestDescription("clzz", "mthd"))
+                .evaluate();
+        assertThat(rule.getOperations()).containsExactly(
+                "dumpsys activity activities", "test")
+                .inOrder();
+    }
+
+    /**
+     * Tests that this rule will finish the activities associated with the package, if
+     * finish all activity without process kill flag is enabled.
+     */
+    @Test
+    public void testDisableFinishActivities() throws Throwable {
+        Bundle bundle = new Bundle();
+        bundle.putString(
+                FinishActivitiesWithoutProcessKillRule.FINISH_ACTIVITY_WITHOUT_PROCESS_KILL,
+                "false");
+
+        TestableRule rule = new TestableRule(bundle, "example.package.name");
+        rule.apply(rule.getTestStatement(), Description.createTestDescription("clzz", "mthd"))
+                .evaluate();
+        assertThat(rule.getOperations()).containsExactly("test").inOrder();
+    }
+
+    private static class TestableRule extends FinishActivitiesWithoutProcessKillRule {
+        private List<String> mOperations = new ArrayList<>();
+        private Bundle mBundle;
+
+        public TestableRule(Bundle bundle, String app) {
+            super(app);
+            mBundle = bundle;
+        }
+
+        @Override
+        protected String executeShellCommand(String cmd) {
+            mOperations.add(cmd);
+            if (cmd.equalsIgnoreCase("dumpsys activity activities")) {
+                return DUMPSYS_NO_ACTIVITY_OUTPUT;
+            }
+            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");
+                }
+            };
+        }
+    }
+}
diff --git a/libraries/health/rules/tests/src/android/platform/test/rule/RemoveAppFromStackRuleTest.java b/libraries/health/rules/tests/src/android/platform/test/rule/RemoveAppFromStackRuleTest.java
new file mode 100644
index 0000000..12e788c
--- /dev/null
+++ b/libraries/health/rules/tests/src/android/platform/test/rule/RemoveAppFromStackRuleTest.java
@@ -0,0 +1,121 @@
+/*
+ * 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.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 RemoveAppFromStackRule}
+ */
+@RunWith(JUnit4.class)
+public class RemoveAppFromStackRuleTest {
+
+    private final static String DUMPSYS_OUTPUT = "ActivityRecord{626fc94 u0"
+            + " example.package.name/com.android.calculator2.Calculator t39}";
+
+    /**
+     * Tests that this rule will fail to register if no package name is supplied
+     * to remove from the am stack.
+     */
+    @Test
+    public void testNoAppToRemoveFromStackFails() {
+        try {
+            RemoveAppFromStackRule rule = new RemoveAppFromStackRule();
+            fail("An initialization error should have been thrown, but wasn't.");
+        } catch (InitializationError e) {
+            return;
+        }
+    }
+
+    /**
+     * Tests that this rule will remove the package name from am stack, if supplied.
+     */
+    @Test
+    public void testRemoveAppFromStack() throws Throwable {
+        TestableRule rule = new TestableRule(new Bundle(), "example.package.name");
+        rule.apply(rule.getTestStatement(), Description.createTestDescription("clzz", "mthd"))
+                .evaluate();
+        assertThat(rule.getOperations()).containsExactly(
+                "dumpsys activity activities", "am stack remove 39", "test")
+                .inOrder();
+    }
+
+    /**
+     * Tests that this rule will not remove the package name from am stack, if
+     * remove app from stack flag is disabled
+     */
+    @Test
+    public void testDisableRemoveAppFromStack() throws Throwable {
+        Bundle noRemoveAppFromStackBundle = new Bundle();
+        noRemoveAppFromStackBundle.putString(RemoveAppFromStackRule.REMOVE_APP_FROM_AM_STACK,
+                "false");
+
+        TestableRule rule = new TestableRule(noRemoveAppFromStackBundle, "example.package.name");
+        rule.apply(rule.getTestStatement(), Description.createTestDescription("clzz", "mthd"))
+                .evaluate();
+        assertThat(rule.getOperations()).containsExactly("test").inOrder();
+    }
+
+    private static class TestableRule extends RemoveAppFromStackRule {
+        private List<String> mOperations = new ArrayList<>();
+        private Bundle mBundle;
+
+        public TestableRule(Bundle bundle, String app) {
+            super(app);
+            mBundle = bundle;
+        }
+
+        @Override
+        protected String executeShellCommand(String cmd) {
+            mOperations.add(cmd);
+            if (cmd.equalsIgnoreCase("dumpsys activity activities")) {
+                return DUMPSYS_OUTPUT;
+            }
+            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");
+                }
+            };
+        }
+    }
+}
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 f757fe0..c3a215b 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
@@ -226,7 +226,10 @@
         context.registerReceiver(receiver, wakeUpActionFilter);
         PendingIntent pendingIntent =
                 PendingIntent.getBroadcast(
-                        context, 0, new Intent(wakeUpAction), PendingIntent.FLAG_UPDATE_CURRENT);
+                        context,
+                        0,
+                        new Intent(wakeUpAction),
+                        PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
 
         alarmManager.setExactAndAllowWhileIdle(
                 AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + durationMs, pendingIntent);
diff --git a/libraries/health/runners/microbenchmark/src/android/platform/test/microbenchmark/Microbenchmark.java b/libraries/health/runners/microbenchmark/src/android/platform/test/microbenchmark/Microbenchmark.java
index b6c6f13..7ad17de 100644
--- a/libraries/health/runners/microbenchmark/src/android/platform/test/microbenchmark/Microbenchmark.java
+++ b/libraries/health/runners/microbenchmark/src/android/platform/test/microbenchmark/Microbenchmark.java
@@ -75,8 +75,10 @@
     @VisibleForTesting static final String MAX_BATTERY_DRAIN_OPTION = "max-battery-drain";
     // Use these options to inject rules at runtime via the command line. For details, please see
     // documentation for DynamicRuleChain.
-    @VisibleForTesting static final String DYNAMIC_OUTER_RULES_OPTION = "outer-rules";
-    @VisibleForTesting static final String DYNAMIC_INNER_RULES_OPTION = "inner-rules";
+    @VisibleForTesting static final String DYNAMIC_OUTER_CLASS_RULES_OPTION = "outer-class-rules";
+    @VisibleForTesting static final String DYNAMIC_INNER_CLASS_RULES_OPTION = "inner-class-rules";
+    @VisibleForTesting static final String DYNAMIC_OUTER_TEST_RULES_OPTION = "outer-test-rules";
+    @VisibleForTesting static final String DYNAMIC_INNER_TEST_RULES_OPTION = "inner-test-rules";
 
     // Options for aligning with the battery charge (coulomb) counter for power tests. We want to
     // start microbenchmarks just after the coulomb counter has decremented to account for the
@@ -283,9 +285,9 @@
         Statement result = statement;
         List<TestRule> testRules = new ArrayList<>();
         // Inner dynamic rules should be included first because RunRules applies rules inside-out.
-        testRules.add(new DynamicRuleChain(DYNAMIC_INNER_RULES_OPTION, mArguments));
+        testRules.add(new DynamicRuleChain(DYNAMIC_INNER_TEST_RULES_OPTION, mArguments));
         testRules.addAll(getTestRules(target));
-        testRules.add(new DynamicRuleChain(DYNAMIC_OUTER_RULES_OPTION, mArguments));
+        testRules.add(new DynamicRuleChain(DYNAMIC_OUTER_TEST_RULES_OPTION, mArguments));
         // Apply legacy MethodRules, if they don't overlap with TestRules.
         for (org.junit.rules.MethodRule each : rules(target)) {
             if (!testRules.contains(each)) {
@@ -297,6 +299,18 @@
         return result;
     }
 
+    /** Add {@link DynamicRuleChain} to existing class rules. */
+    @Override
+    protected List<TestRule> classRules() {
+        List<TestRule> classRules = new ArrayList<>();
+        // Inner dynamic class rules should be included first because RunRules applies rules inside
+        // -out.
+        classRules.add(new DynamicRuleChain(DYNAMIC_INNER_CLASS_RULES_OPTION, mArguments));
+        classRules.addAll(super.classRules());
+        classRules.add(new DynamicRuleChain(DYNAMIC_OUTER_CLASS_RULES_OPTION, mArguments));
+        return classRules;
+    }
+
     /**
      * Combine the {@code #runChild}, {@code #methodBlock}, and final {@code #runLeaf} methods to
      * implement the specific {@code Microbenchmark} test behavior. In particular, (1) keep track of
diff --git a/libraries/health/runners/microbenchmark/tests/src/android/platform/test/microbenchmark/MicrobenchmarkTest.java b/libraries/health/runners/microbenchmark/tests/src/android/platform/test/microbenchmark/MicrobenchmarkTest.java
index 0b44d1a..fa8e64a 100644
--- a/libraries/health/runners/microbenchmark/tests/src/android/platform/test/microbenchmark/MicrobenchmarkTest.java
+++ b/libraries/health/runners/microbenchmark/tests/src/android/platform/test/microbenchmark/MicrobenchmarkTest.java
@@ -30,6 +30,7 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.ClassRule;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TestRule;
@@ -418,53 +419,102 @@
                 .inOrder();
     }
 
-    /**
-     * Test successive iteration will be executed when the terminate on test fail option is
-     * disabled.
-     */
+    /** Test dynamic test rule injection. */
     @Test
-    public void testDynamicRuleInjection() throws InitializationError {
+    public void testDynamicTestRuleInjection() throws InitializationError {
         Bundle args = new Bundle();
         args.putString("iterations", "2");
         args.putString("rename-iterations", "false");
         args.putString("terminate-on-test-fail", "false");
-        args.putString(Microbenchmark.DYNAMIC_INNER_RULES_OPTION, LoggingRule1.class.getName());
-        args.putString(Microbenchmark.DYNAMIC_OUTER_RULES_OPTION, LoggingRule2.class.getName());
+        args.putString(
+                Microbenchmark.DYNAMIC_INNER_TEST_RULES_OPTION, LoggingRule1.class.getName());
+        args.putString(
+                Microbenchmark.DYNAMIC_OUTER_TEST_RULES_OPTION, LoggingRule2.class.getName());
         LoggingMicrobenchmark loggingRunner =
-                new LoggingMicrobenchmark(LoggingTestWithRule.class, args);
+                new LoggingMicrobenchmark(LoggingTestWithRules.class, args);
+        loggingRunner.setOperationLog(sLogs);
+        new JUnitCore().run(loggingRunner);
+        assertThat(sLogs)
+                .containsExactly(
+                        "hardcoded class rule starting",
+                        "logging rule 2 starting",
+                        "hardcoded test rule starting",
+                        "logging rule 1 starting",
+                        "before",
+                        "tight before",
+                        "begin: testMethod("
+                                + "android.platform.test.microbenchmark.MicrobenchmarkTest"
+                                + "$LoggingTestWithRules)",
+                        "test",
+                        "end",
+                        "tight after",
+                        "after",
+                        "logging rule 1 finished",
+                        "hardcoded test rule finished",
+                        "logging rule 2 finished",
+                        "logging rule 2 starting",
+                        "hardcoded test rule starting",
+                        "logging rule 1 starting",
+                        "before",
+                        "tight before",
+                        "begin: testMethod("
+                                + "android.platform.test.microbenchmark.MicrobenchmarkTest"
+                                + "$LoggingTestWithRules)",
+                        "test",
+                        "end",
+                        "tight after",
+                        "after",
+                        "logging rule 1 finished",
+                        "hardcoded test rule finished",
+                        "logging rule 2 finished",
+                        "hardcoded class rule finished")
+                .inOrder();
+    }
+
+    /** Test dynamic class rule injection. */
+    @Test
+    public void testDynamicClassRuleInjection() throws InitializationError {
+        Bundle args = new Bundle();
+        args.putString("iterations", "2");
+        args.putString("rename-iterations", "false");
+        args.putString("terminate-on-test-fail", "false");
+        args.putString(
+                Microbenchmark.DYNAMIC_INNER_CLASS_RULES_OPTION, LoggingRule1.class.getName());
+        args.putString(
+                Microbenchmark.DYNAMIC_OUTER_CLASS_RULES_OPTION, LoggingRule2.class.getName());
+        LoggingMicrobenchmark loggingRunner =
+                new LoggingMicrobenchmark(LoggingTestWithRules.class, args);
         loggingRunner.setOperationLog(sLogs);
         new JUnitCore().run(loggingRunner);
         assertThat(sLogs)
                 .containsExactly(
                         "logging rule 2 starting",
-                        "hardcoded rule starting",
+                        "hardcoded class rule starting",
                         "logging rule 1 starting",
+                        "hardcoded test rule starting",
                         "before",
                         "tight before",
                         "begin: testMethod("
                                 + "android.platform.test.microbenchmark.MicrobenchmarkTest"
-                                + "$LoggingTestWithRule)",
+                                + "$LoggingTestWithRules)",
                         "test",
                         "end",
                         "tight after",
                         "after",
-                        "logging rule 1 finished",
-                        "hardcoded rule finished",
-                        "logging rule 2 finished",
-                        "logging rule 2 starting",
-                        "hardcoded rule starting",
-                        "logging rule 1 starting",
+                        "hardcoded test rule finished",
+                        "hardcoded test rule starting",
                         "before",
                         "tight before",
                         "begin: testMethod("
                                 + "android.platform.test.microbenchmark.MicrobenchmarkTest"
-                                + "$LoggingTestWithRule)",
+                                + "$LoggingTestWithRules)",
                         "test",
                         "end",
                         "tight after",
                         "after",
+                        "hardcoded test rule finished",
                         "logging rule 1 finished",
-                        "hardcoded rule finished",
+                        "hardcoded class rule finished",
                         "logging rule 2 finished")
                 .inOrder();
     }
@@ -565,18 +615,32 @@
         }
     }
 
-    public static class LoggingTestWithRule extends LoggingTest {
+    public static class LoggingTestWithRules extends LoggingTest {
+        @ClassRule
+        public static TestRule hardCodedClassRule =
+                new TestWatcher() {
+                    @Override
+                    public void starting(Description description) {
+                        sLogs.add("hardcoded class rule starting");
+                    }
+
+                    @Override
+                    public void finished(Description description) {
+                        sLogs.add("hardcoded class rule finished");
+                    }
+                };
+
         @Rule
         public TestRule hardCodedRule =
                 new TestWatcher() {
                     @Override
                     public void starting(Description description) {
-                        sLogs.add("hardcoded rule starting");
+                        sLogs.add("hardcoded test rule starting");
                     }
 
                     @Override
                     public void finished(Description description) {
-                        sLogs.add("hardcoded rule finished");
+                        sLogs.add("hardcoded test rule finished");
                     }
                 };
     }
diff --git a/libraries/launcher-helper/Android.bp b/libraries/launcher-helper/Android.bp
index 0461c1a..10a8d22 100644
--- a/libraries/launcher-helper/Android.bp
+++ b/libraries/launcher-helper/Android.bp
@@ -22,7 +22,7 @@
     name: "launcher-helper-lib",
     libs: [
         "ub-uiautomator",
-        "android-support-test",
+        "androidx.test.rules",
         "androidx.test.runner",
         "activity-helper",
     ],
diff --git a/libraries/launcher-helper/src/android/support/test/launcherhelper/AllAppsScreenHelper.java b/libraries/launcher-helper/src/android/support/test/launcherhelper/AllAppsScreenHelper.java
index a9b47ed..c12d945 100644
--- a/libraries/launcher-helper/src/android/support/test/launcherhelper/AllAppsScreenHelper.java
+++ b/libraries/launcher-helper/src/android/support/test/launcherhelper/AllAppsScreenHelper.java
@@ -17,13 +17,14 @@
 package android.support.test.launcherhelper;
 
 import android.app.Instrumentation;
-import android.support.test.InstrumentationRegistry;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.UiDevice;
 import android.support.test.uiautomator.UiObject2;
 import android.support.test.uiautomator.Until;
 import android.system.helpers.ActivityHelper;
 
+import androidx.test.InstrumentationRegistry;
+
 import org.junit.Assert;
 
 public class AllAppsScreenHelper {
diff --git a/libraries/launcher-helper/src/android/support/test/launcherhelper/AospLauncherStrategy.java b/libraries/launcher-helper/src/android/support/test/launcherhelper/AospLauncherStrategy.java
index 10a4930..0cf3b8b 100644
--- a/libraries/launcher-helper/src/android/support/test/launcherhelper/AospLauncherStrategy.java
+++ b/libraries/launcher-helper/src/android/support/test/launcherhelper/AospLauncherStrategy.java
@@ -15,6 +15,7 @@
  */
 package android.support.test.launcherhelper;
 
+import android.os.RemoteException;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.BySelector;
 import android.support.test.uiautomator.Direction;
@@ -34,6 +35,7 @@
     private static final String LAUNCHER_PKG = "com.android.launcher";
     private static final BySelector APPS_CONTAINER =
             By.res(LAUNCHER_PKG, "apps_customize_pane_content");
+    private static final BySelector OVERVIEW = By.res(LAUNCHER_PKG, "overview_panel");
     private static final BySelector WORKSPACE = By.res(LAUNCHER_PKG, "workspace");
     private static final BySelector HOTSEAT = By.res(LAUNCHER_PKG, "hotseat");
     private UiDevice mDevice;
@@ -93,6 +95,51 @@
         return allAppsContainer;
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public void openOverview() {
+        try {
+            mDevice.pressRecentApps();
+        } catch (RemoteException e) {
+            throw new RuntimeException(e);
+        }
+
+        // use a long timeout to wait until recents populated
+        UiObject2 overviewScreen = mDevice.wait(Until.findObject(getOverviewSelector()), 2000);
+        Assert.assertNotNull("openOverview: did not find overview", overviewScreen);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean clearRecentAppsFromOverview() {
+        for (int i = 0; i < 10; i++) {
+            mDevice.swipe(
+                    mDevice.getDisplayWidth() / 2,
+                    mDevice.getDisplayHeight() / 2,
+                    mDevice.getDisplayWidth(),
+                    mDevice.getDisplayHeight() / 2,
+                    5);
+
+            BySelector noRecentItemsSelector = getOverviewSelector().desc("No recent items");
+            UiObject2 noRecentItems = mDevice.wait(Until.findObject(noRecentItemsSelector), 100);
+
+            // If "No recent items"  is displayed, there're no apps to remove
+            if (noRecentItems != null) {
+                return true;
+            }
+
+            // If "Clear all"  button appears, use it
+            BySelector clearAllSelector = By.res(mDevice.getLauncherPackageName(), "clear_all");
+            UiObject2 clearAllButton = mDevice.wait(Until.findObject(clearAllSelector), 100);
+            if (clearAllButton != null) {
+                clearAllButton.click();
+                return true;
+            }
+        }
+
+        return false;
+    }
+
     /**
      * {@inheritDoc}
      */
@@ -213,6 +260,13 @@
         return HOTSEAT;
     }
 
+    /** {@inheritDoc} */
+    /** {@inheritDoc} */
+    @Override
+    public BySelector getOverviewSelector() {
+        return OVERVIEW;
+    }
+
     /**
      * {@inheritDoc}
      */
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 cdcd388..6d46977 100644
--- a/libraries/launcher-helper/src/android/support/test/launcherhelper/AutoLauncherStrategy.java
+++ b/libraries/launcher-helper/src/android/support/test/launcherhelper/AutoLauncherStrategy.java
@@ -73,20 +73,25 @@
             }).collect(Collectors.toMap(data -> (String) data[0], data -> (BySelector) data[1]));
 
     private static final Map<String, BySelector> APP_OPEN_VERIFIERS =
-            Stream.of(new Object[][] {
-                { "Home", By.hasDescendant(By.res(CAR_LENSPICKER, "maps"))
-                            .hasDescendant(By.res(CAR_LENSPICKER, "contextual"))
-                            .hasDescendant(By.res(CAR_LENSPICKER, "playback"))
-                },
-                { "Maps", By.pkg(MAPS_PACKAGE).depth(0) },
-                { "Media", By.pkg(MEDIA_PACKAGE).depth(0) },
-                { "Radio", By.pkg(RADIO_PACKAGE).depth(0) },
-                { "Dial",  By.pkg(DIAL_PACKAGE).depth(0) },
-                { "App Grid", By.res(CAR_LENSPICKER, "apps_grid") },
-                { "Notification", By.res(SYSTEM_UI_PACKAGE, "notifications") },
-                { "Google Assistant", By.pkg(ASSISTANT_PACKAGE) },
-                { "Settings", By.pkg(SETTINGS_PACKAGE).depth(0) },
-            }).collect(Collectors.toMap(data -> (String) data[0], data -> (BySelector) data[1]));
+            Stream.of(
+                            new Object[][] {
+                                {
+                                    "Home",
+                                    By.hasDescendant(By.res(CAR_LENSPICKER, "top_card"))
+                                            .hasDescendant(By.res(CAR_LENSPICKER, "bottom_card"))
+                                },
+                                {"Maps", By.pkg(MAPS_PACKAGE).depth(0)},
+                                {"Media", By.pkg(MEDIA_PACKAGE).depth(0)},
+                                {"Radio", By.pkg(RADIO_PACKAGE).depth(0)},
+                                {"Dial", By.pkg(DIAL_PACKAGE).depth(0)},
+                                {"App Grid", By.res(CAR_LENSPICKER, "apps_grid")},
+                                {"Notification", By.res(SYSTEM_UI_PACKAGE, "notifications")},
+                                {"Google Assistant", By.pkg(ASSISTANT_PACKAGE)},
+                                {"Settings", By.pkg(SETTINGS_PACKAGE).depth(0)},
+                            })
+                    .collect(
+                            Collectors.toMap(
+                                    data -> (String) data[0], data -> (BySelector) data[1]));
 
     protected UiDevice mDevice;
     private Instrumentation mInstrumentation;
@@ -398,6 +403,18 @@
                 "The feature not supported on Auto");
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public void openOverview() {
+        throw new UnsupportedOperationException("The feature not supported on Auto");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean clearRecentAppsFromOverview() {
+        throw new UnsupportedOperationException("The feature not supported on Auto");
+    }
+
     @SuppressWarnings("unused")
     @Override
     public BySelector getAllAppsButtonSelector() {
@@ -454,6 +471,12 @@
                 "The feature not supported on Auto");
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public BySelector getOverviewSelector() {
+        throw new UnsupportedOperationException("The feature not supported on Auto");
+    }
+
     @SuppressWarnings("unused")
     @Override
     public Direction getWorkspaceScrollDirection() {
diff --git a/libraries/launcher-helper/src/android/support/test/launcherhelper/BaseLauncher3Strategy.java b/libraries/launcher-helper/src/android/support/test/launcherhelper/BaseLauncher3Strategy.java
index f428e04..538ed6c 100644
--- a/libraries/launcher-helper/src/android/support/test/launcherhelper/BaseLauncher3Strategy.java
+++ b/libraries/launcher-helper/src/android/support/test/launcherhelper/BaseLauncher3Strategy.java
@@ -17,6 +17,7 @@
 
 import android.graphics.Point;
 import android.os.Build;
+import android.os.RemoteException;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.BySelector;
 import android.support.test.uiautomator.Direction;
@@ -120,6 +121,59 @@
         return allAppsContainer;
     }
 
+    protected boolean isInOverview() {
+        BySelector noRecentItemsSelector = getOverviewSelector().desc("No recent items");
+        UiObject2 noRecentItems = mDevice.findObject(noRecentItemsSelector);
+        return noRecentItems != null;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void openOverview() {
+        if (!isInOverview()) {
+            try {
+                mDevice.pressRecentApps();
+            } catch (RemoteException e) {
+                throw new RuntimeException(e);
+            }
+
+            // use a long timeout to wait until recents populated
+            UiObject2 overviewScreen = mDevice.wait(Until.findObject(getOverviewSelector()), 2000);
+            Assert.assertNotNull("openOverview: did not find overview", overviewScreen);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean clearRecentAppsFromOverview() {
+        for (int i = 0; i < 10; i++) {
+            mDevice.swipe(
+                    mDevice.getDisplayWidth() / 2,
+                    mDevice.getDisplayHeight() / 2,
+                    mDevice.getDisplayWidth(),
+                    mDevice.getDisplayHeight() / 2,
+                    5);
+
+            BySelector noRecentItemsSelector = getOverviewSelector().desc("No recent items");
+            UiObject2 noRecentItems = mDevice.wait(Until.findObject(noRecentItemsSelector), 100);
+
+            // If "No recent items"  is displayed, there're no apps to remove
+            if (noRecentItems != null) {
+                return true;
+            }
+
+            // If "Clear all"  button appears, use it
+            BySelector clearAllSelector = By.res(mDevice.getLauncherPackageName(), "clear_all");
+            UiObject2 clearAllButton = mDevice.wait(Until.findObject(clearAllSelector), 100);
+            if (clearAllButton != null) {
+                clearAllButton.click();
+                return true;
+            }
+        }
+
+        return false;
+    }
+
     /**
      * {@inheritDoc}
      */
@@ -200,6 +254,12 @@
         return By.res(getSupportedLauncherPackage(), "hotseat");
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public BySelector getOverviewSelector() {
+        return By.res(getSupportedLauncherPackage(), "overview_panel");
+    }
+
     /**
      * {@inheritDoc}
      */
diff --git a/libraries/launcher-helper/src/android/support/test/launcherhelper/ILauncherStrategy.java b/libraries/launcher-helper/src/android/support/test/launcherhelper/ILauncherStrategy.java
index 74f4550..bed9cd7 100644
--- a/libraries/launcher-helper/src/android/support/test/launcherhelper/ILauncherStrategy.java
+++ b/libraries/launcher-helper/src/android/support/test/launcherhelper/ILauncherStrategy.java
@@ -27,95 +27,122 @@
  * method.
  */
 public interface ILauncherStrategy {
-    public static final long LAUNCH_FAILED_TIMESTAMP = -1;
+    long LAUNCH_FAILED_TIMESTAMP = -1;
 
     /**
      * Returns the launcher application package that this {@link ILauncherStrategy} can automate
+     *
      * @return
      */
-    public String getSupportedLauncherPackage();
+    String getSupportedLauncherPackage();
 
     /**
      * Injects a {@link UiDevice} instance for UI interactions
+     *
      * @param uiDevice
      */
-    public void setUiDevice(UiDevice uiDevice);
+    void setUiDevice(UiDevice uiDevice);
 
-    /**
-     * Shows the home screen of launcher
-     */
-    public void open();
+    /** Shows the home screen of launcher */
+    void open();
 
     /**
      * Opens the all apps drawer of launcher
+     *
      * @param reset if the all apps drawer should be reset to the beginning
      * @return {@link UiObject2} representation of the all apps drawer
      */
-    public UiObject2 openAllApps(boolean reset);
+    UiObject2 openAllApps(boolean reset);
+
+    /** Opens the overview drawer of launcher */
+    void openOverview();
+
+    /**
+     * Clears the list of recently used apps from the overview drawer
+     *
+     * @return If the list of recent apps is empty
+     */
+    boolean clearRecentAppsFromOverview();
 
     /**
      * Returns a {@link BySelector} describing the button to open the all apps drawer
+     *
      * @return
      */
-    public BySelector getAllAppsButtonSelector();
+    BySelector getAllAppsButtonSelector();
 
     /**
      * Returns a {@link BySelector} describing the all apps drawer
+     *
      * @return
      */
-    public BySelector getAllAppsSelector();
+    BySelector getAllAppsSelector();
 
     /**
      * Retrieves the all apps drawer forward scroll direction as implemented by the launcher
+     *
      * @return
      */
-    public Direction getAllAppsScrollDirection();
+    Direction getAllAppsScrollDirection();
 
     /**
      * Opens the all widgets drawer of launcher
+     *
      * @param reset if the all widgets drawer should be reset to the beginning
      * @return {@link UiObject2} representation of the all widgets drawer
      */
-    public UiObject2 openAllWidgets(boolean reset);
+    UiObject2 openAllWidgets(boolean reset);
 
     /**
      * Returns a {@link BySelector} describing the all widgets drawer
+     *
      * @return
      */
-    public BySelector getAllWidgetsSelector();
+    BySelector getAllWidgetsSelector();
 
     /**
      * Retrieves the all widgets drawer forward scroll direction as implemented by the launcher
+     *
      * @return
      */
-    public Direction getAllWidgetsScrollDirection();
+    Direction getAllWidgetsScrollDirection();
 
     /**
      * Returns a {@link BySelector} describing the home screen workspace
+     *
      * @return
      */
-    public BySelector getWorkspaceSelector();
+    BySelector getWorkspaceSelector();
 
     /**
      * Returns a {@link BySelector} describing the home screen hot seat (app icons at the bottom)
+     *
      * @return
      */
-    public BySelector getHotSeatSelector();
+    BySelector getHotSeatSelector();
+
+    /**
+     * Returns a {@link BySelector} describing the overview screen (recent apps list)
+     *
+     * @return
+     */
+    BySelector getOverviewSelector();
 
     /**
      * Retrieves the home screen workspace forward scroll direction as implemented by the launcher
+     *
      * @return
      */
-    public Direction getWorkspaceScrollDirection();
+    Direction getWorkspaceScrollDirection();
 
     /**
      * Launch the named application
+     *
      * @param appName the name of the application to launch as shown in launcher
      * @param packageName the expected package name to verify that the application has been launched
-     *                    into foreground. If <code>null</code> is provided, no verification is
-     *                    performed.
+     *     into foreground. If <code>null</code> is provided, no verification is performed.
      * @return <code>true</code> if application is verified to be in foreground after launch, or the
-     *   verification is skipped; <code>false</code> otherwise.
+     *     verification is skipped; <code>false</code> otherwise.
      */
-    public long launch(String appName, String packageName);
+    long launch(String appName, String packageName);
 }
diff --git a/libraries/launcher-helper/src/android/support/test/launcherhelper/LauncherStrategyFactory.java b/libraries/launcher-helper/src/android/support/test/launcherhelper/LauncherStrategyFactory.java
index f812b99..668ec65 100644
--- a/libraries/launcher-helper/src/android/support/test/launcherhelper/LauncherStrategyFactory.java
+++ b/libraries/launcher-helper/src/android/support/test/launcherhelper/LauncherStrategyFactory.java
@@ -15,6 +15,7 @@
  */
 package android.support.test.launcherhelper;
 
+import android.app.Instrumentation;
 import android.support.test.uiautomator.UiDevice;
 import android.util.Log;
 
@@ -53,6 +54,16 @@
 
     /**
      * Retrieves an instance of the {@link LauncherStrategyFactory}
+     *
+     * @param instrumentation
+     * @return
+     */
+    public static LauncherStrategyFactory getInstance(Instrumentation instrumentation) {
+        return getInstance(UiDevice.getInstance(instrumentation));
+    }
+
+    /**
+     * Retrieves an instance of the {@link LauncherStrategyFactory}
      * @param uiDevice
      * @return
      */
@@ -75,6 +86,7 @@
             try {
                 ILauncherStrategy strategy = launcherStrategy.newInstance();
                 mInstanceMap.put(strategy.getSupportedLauncherPackage(), strategy);
+                mKnownLauncherStrategies.add(launcherStrategy);
             } catch (InstantiationException | IllegalAccessException e) {
                 Log.e(LOG_TAG, "exception while creating instance: "
                         + launcherStrategy.getCanonicalName());
diff --git a/libraries/launcher-helper/src/android/support/test/launcherhelper/LeanbackLauncherStrategy.java b/libraries/launcher-helper/src/android/support/test/launcherhelper/LeanbackLauncherStrategy.java
index 2ecc0c0..2087643 100644
--- a/libraries/launcher-helper/src/android/support/test/launcherhelper/LeanbackLauncherStrategy.java
+++ b/libraries/launcher-helper/src/android/support/test/launcherhelper/LeanbackLauncherStrategy.java
@@ -111,6 +111,19 @@
         return appsRow;
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public void openOverview() {
+        throw new UnsupportedOperationException("Overview is not available on Leanback Launcher.");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean clearRecentAppsFromOverview() {
+        throw new UnsupportedOperationException(
+                "Recent apps are not available on Leanback Launcher.");
+    }
+
     /**
      * {@inheritDoc}
      */
@@ -359,6 +372,12 @@
                 "Hot Seat is not available on Leanback Launcher.");
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public BySelector getOverviewSelector() {
+        throw new UnsupportedOperationException("Overview is not available on Leanback Launcher.");
+    }
+
     @SuppressWarnings("unused")
     @Override
     public Direction getWorkspaceScrollDirection() {
diff --git a/libraries/launcher-helper/src/android/support/test/launcherhelper/NexusLauncherStrategy.java b/libraries/launcher-helper/src/android/support/test/launcherhelper/NexusLauncherStrategy.java
index f91fcd9..9ea5a5f 100644
--- a/libraries/launcher-helper/src/android/support/test/launcherhelper/NexusLauncherStrategy.java
+++ b/libraries/launcher-helper/src/android/support/test/launcherhelper/NexusLauncherStrategy.java
@@ -16,7 +16,6 @@
 package android.support.test.launcherhelper;
 
 import android.graphics.Point;
-import android.support.test.InstrumentationRegistry;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.BySelector;
 import android.support.test.uiautomator.Direction;
@@ -24,6 +23,9 @@
 import android.support.test.uiautomator.UiObject2;
 import android.support.test.uiautomator.Until;
 
+import androidx.test.InstrumentationRegistry;
+
+import com.android.launcher3.tapl.BaseOverview;
 import com.android.launcher3.tapl.LauncherInstrumentation;
 
 import junit.framework.Assert;
@@ -138,6 +140,27 @@
         return allAppsContainer;
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public void openOverview() {
+        mLauncher.pressHome().switchToOverview();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean clearRecentAppsFromOverview() {
+        if (!isInOverview()) {
+            openOverview();
+        }
+
+        BaseOverview overview = mLauncher.getOverview();
+        if (overview.hasTasks()) {
+            overview.dismissAllTasks();
+        }
+
+        return !overview.hasTasks();
+    }
+
     /**
      * {@inheritDoc}
      */
diff --git a/libraries/launcher-helper/src/android/support/test/launcherhelper/TvLauncherStrategy.java b/libraries/launcher-helper/src/android/support/test/launcherhelper/TvLauncherStrategy.java
index f98428b..408bfd2 100644
--- a/libraries/launcher-helper/src/android/support/test/launcherhelper/TvLauncherStrategy.java
+++ b/libraries/launcher-helper/src/android/support/test/launcherhelper/TvLauncherStrategy.java
@@ -197,6 +197,18 @@
         return mDevice.wait(Until.findObject(getAllAppsSelector()), UI_WAIT_TIME_MS);
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public void openOverview() {
+        throw new TvLauncherUnsupportedOperationException("No Overview");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean clearRecentAppsFromOverview() {
+        throw new TvLauncherUnsupportedOperationException("No Recent apps");
+    }
+
     public boolean openSettings() {
         Assert.assertNotNull(selectTopRow());
         Assert.assertNotNull(selectBidirect(By.res(getSupportedLauncherPackage(), "settings"),
@@ -998,6 +1010,12 @@
         throw new TvLauncherUnsupportedOperationException("No Hot seat");
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public BySelector getOverviewSelector() {
+        throw new TvLauncherUnsupportedOperationException("No Overview");
+    }
+
     @SuppressWarnings("unused")
     @Override
     public Direction getWorkspaceScrollDirection() {
diff --git a/libraries/launcher-helper/src/android/support/test/launcherhelper/WearLauncherStrategy.java b/libraries/launcher-helper/src/android/support/test/launcherhelper/WearLauncherStrategy.java
index 7f913ad..12714e5 100644
--- a/libraries/launcher-helper/src/android/support/test/launcherhelper/WearLauncherStrategy.java
+++ b/libraries/launcher-helper/src/android/support/test/launcherhelper/WearLauncherStrategy.java
@@ -15,25 +15,21 @@
  */
 package android.support.test.launcherhelper;
 
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.IOException;
-
-import android.app.Instrumentation;
 import android.content.Context;
-import android.content.Intent;
-import android.os.SystemClock;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.BySelector;
 import android.support.test.uiautomator.Direction;
 import android.support.test.uiautomator.UiDevice;
 import android.support.test.uiautomator.UiObject2;
 import android.support.test.uiautomator.Until;
-import android.widget.TextView;
 import android.util.Log;
+import android.widget.TextView;
 
 import junit.framework.Assert;
 
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
 public class WearLauncherStrategy implements ILauncherStrategy {
 
     private static final String LAUNCHER_PKG = "com.google.android.wearable.app";
@@ -98,6 +94,20 @@
         return allAppsContainer;
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public void openOverview() {
+        throw new UnsupportedOperationException(
+                "The 'Overview' is not available on Android Wear Launcher.");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean clearRecentAppsFromOverview() {
+        throw new UnsupportedOperationException(
+                "The 'Recent Apps' are not available on Android Wear Launcher.");
+    }
+
     /**
      * {@inheritDoc}
      */
@@ -106,6 +116,12 @@
          return By.res(getSupportedLauncherPackage(), "watchface_overlay");
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public BySelector getOverviewSelector() {
+        throw new UnsupportedOperationException(
+                "The 'Overview' are not available on Android Wear Launcher.");
+    }
 
     /**
      * Returns a {@link BySelector} describing the all apps drawer
diff --git a/libraries/media-helper/Android.bp b/libraries/media-helper/Android.bp
new file mode 100644
index 0000000..bf4706d
--- /dev/null
+++ b/libraries/media-helper/Android.bp
@@ -0,0 +1,29 @@
+//
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+    name: "media-helper-lib",
+    sdk_version: "test_current",
+    srcs: ["src/**/*.java"],
+    static_libs: [
+        "truth-prebuilt",
+        "guava",
+    ],
+}
diff --git a/libraries/media-helper/src/android/test/mediahelper/MediaStoreHelper.java b/libraries/media-helper/src/android/test/mediahelper/MediaStoreHelper.java
new file mode 100644
index 0000000..b2f42ad
--- /dev/null
+++ b/libraries/media-helper/src/android/test/mediahelper/MediaStoreHelper.java
@@ -0,0 +1,181 @@
+/*
+ * 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.test.mediahelper;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.MediaStore;
+
+import com.google.common.collect.ImmutableList;
+
+import java.io.FileDescriptor;
+import java.io.FileNotFoundException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/** Helper functions for interacting with android {@link MediaStore}. */
+public class MediaStoreHelper {
+    private static MediaStoreHelper sInstance;
+    private ContentResolver mContentResolver;
+
+    private MediaStoreHelper(Context context) {
+        mContentResolver = context.getContentResolver();
+    }
+
+    public static MediaStoreHelper getInstance(Context context) {
+        if (sInstance == null) {
+            sInstance = new MediaStoreHelper(context);
+        }
+        return sInstance;
+    }
+
+    /**
+     * Returns the maximum value of {@link MediaStore.MediaColumns.GENERATION_ADDED} at given URI.
+     *
+     * @param uri The URI, the content:// style uri, for the content to retrieve from {@link
+     *     MediaStore}. For example, to retrieve from image media table, use
+     *     "content://media/external/images/media".
+     * @param selection A filter declaring which rows to return, formatted as an SQL WHERE clause
+     *     (excluding the WHERE itself). Passing null will return all rows for the given URI.
+     * @param selectionArgs You may include ?s in selection, which will be replaced by the values
+     *     from selectionArgs, in the order that they appear in the selection. The values will be
+     *     bound as Strings.
+     * @return A long value of the maximum generation. Returns -1L if no records find.
+     */
+    public long getMaxGeneration(Uri uri, String selection, String[] selectionArgs) {
+        long maxGen = -1L;
+        Cursor cursor =
+                mContentResolver.query(
+                        uri,
+                        new String[] {MediaStore.MediaColumns.GENERATION_ADDED},
+                        selection,
+                        selectionArgs,
+                        MediaStore.MediaColumns.GENERATION_ADDED + " DESC");
+        if (cursor != null) {
+            if (cursor.moveToFirst()) {
+                maxGen =
+                        cursor.getLong(
+                                cursor.getColumnIndex(MediaStore.MediaColumns.GENERATION_ADDED));
+            }
+            cursor.close();
+        }
+        return maxGen;
+    }
+
+    /**
+     * Returns a value list of {@link MediaStore.MediaColumns#_ID}.
+     *
+     * @param uri The URI, the content:// style uri, for the content to retrieve from {@link
+     *     MediaStore}. For example, to retrieve from image media table, use
+     *     "content://media/external/images/media".
+     * @param selection A filter declaring which rows to return, formatted as an SQL WHERE clause
+     *     (excluding the WHERE itself). Passing null will return all rows for the given URI.
+     * @param selectionArgs You may include ?s in selection, which will be replaced by the values
+     *     from selectionArgs, in the order that they appear in the selection. The values will be
+     *     bound as Strings.
+     * @param fromGen Generation added. The id of the records returned all have generation bigger
+     *     than this value. It is also formatted as a WHERE clause together with what specified in
+     *     selection.
+     * @return A list of record ids.
+     */
+    public List<Long> getListOfIdsFromMediaStore(
+            Uri uri, String selection, String[] selectionArgs, long fromGen) {
+        ImmutableList.Builder builder = new ImmutableList.Builder();
+        String selectionStr =
+                MediaStore.MediaColumns.GENERATION_ADDED + ">" + String.valueOf(fromGen);
+        if (selection != null && !selection.isEmpty()) {
+            selectionStr = selectionStr + " AND " + selection;
+        }
+        Cursor cursor =
+                mContentResolver.query(
+                        uri,
+                        new String[] {
+                            MediaStore.MediaColumns._ID, MediaStore.MediaColumns.GENERATION_ADDED
+                        },
+                        selectionStr,
+                        selectionArgs,
+                        null);
+        try {
+            while (cursor != null && cursor.moveToNext()) {
+                builder.add(cursor.getLong(cursor.getColumnIndex(MediaStore.MediaColumns._ID)));
+            }
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+        return builder.build();
+    }
+
+    /**
+     * Returns a list of media {@link FileDescriptor} for specific MIME type.
+     *
+     * @param uri The URI, the content:// style uri, for the content to retrieve from {@link
+     *     MediaStore}. For example, to retrieve from image media table, use
+     *     "content://media/external/images/media".
+     * @param mimeType The MIME type of the media item, eg. "image/jpeg". {@see
+     *     MediaStore.MediaColumns#MIME_TYPE}
+     * @param fromGen Generation added. The returned records all have generation bigger than this.
+     * @param selection A filter declaring which rows to return, formatted as an SQL WHERE clause
+     *     (excluding the WHERE itself). Passing null will return all rows for the given URI.
+     * @param selectionArgs You may include ?s in selection, which will be replaced by the values
+     *     from selectionArgs, in the order that they appear in the selection. The values will be
+     *     bound as Strings.
+     * @return A list of media {@link FileDescriptor} for specified type.
+     */
+    public List<FileDescriptor> getMediaFileDescriptors(
+            Uri uri, String mimeType, String selection, String[] selectionArgs, long fromGen) {
+        ImmutableList.Builder fileListBuilder = new ImmutableList.Builder();
+        StringBuilder selectionBuilder = new StringBuilder();
+        List<String> argList = new ArrayList<>();
+        selectionBuilder.append(MediaStore.MediaColumns.IS_PENDING + "=?");
+        argList.add("0");
+        if (mimeType != null) {
+            selectionBuilder.append(" AND ");
+            selectionBuilder.append(MediaStore.MediaColumns.MIME_TYPE + "=?");
+            argList.add(mimeType);
+        }
+        if (selection != null && !selection.isEmpty()) {
+            selectionBuilder.append(" AND ");
+            selectionBuilder.append(selection);
+        }
+        if (selectionArgs != null) {
+            argList.addAll(Arrays.asList(selectionArgs));
+        }
+        List<Long> ids =
+                getListOfIdsFromMediaStore(
+                        uri,
+                        selectionBuilder.toString(),
+                        argList.toArray(new String[argList.size()]),
+                        fromGen);
+        for (long id : ids) {
+            Uri fileUri = ContentUris.withAppendedId(uri, id);
+            try {
+                FileDescriptor fileDescriptor =
+                        mContentResolver.openFileDescriptor(fileUri, "r").getFileDescriptor();
+                fileListBuilder.add(fileDescriptor);
+            } catch (FileNotFoundException e) {
+                throw new RuntimeException("Unable to open file descriptor at " + uri, e);
+            }
+        }
+        return fileListBuilder.build();
+    }
+}
diff --git a/libraries/media-helper/src/android/test/mediahelper/MediaValidationHelper.java b/libraries/media-helper/src/android/test/mediahelper/MediaValidationHelper.java
new file mode 100644
index 0000000..c7a66c4
--- /dev/null
+++ b/libraries/media-helper/src/android/test/mediahelper/MediaValidationHelper.java
@@ -0,0 +1,117 @@
+/*
+ * 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.test.mediahelper;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.media.MediaExtractor;
+import android.media.MediaFormat;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+/** Helper functions for validating media files. */
+public class MediaValidationHelper {
+    private static final String TAG = MediaValidationHelper.class.getSimpleName();
+    private static final String VIDEO_MIME_TYPE = "video";
+    private static final String AUDIO_MIME_TYPE = "audio";
+
+    /**
+     * Validate video file.
+     *
+     * <p>This function validates that video contains at least 1 video track and 1 audio track. Also
+     * validate that the video track has expected height and width. It also checks the video track
+     * duration is bigger than the specified length.
+     *
+     * @param fileDescriptor The {@link FileDescriptor} of the video file.
+     * @param expHeight Expected video track height.
+     * @param expWidth Expected video track width.
+     * @param minDurationMillis Minimum duration in milli seconds. The video track duration should
+     *     be longer than it.
+     * @throws IOException
+     */
+    public static void validateVideo(
+            FileDescriptor fileDescriptor, int expHeight, int expWidth, long minDurationMillis)
+            throws IOException {
+        MediaExtractor extractor = new MediaExtractor();
+        extractor.setDataSource(fileDescriptor);
+        int numTracks = extractor.getTrackCount();
+        assertThat(numTracks).isGreaterThan(1);
+        Log.d(TAG, String.format("Number of tracks: %d", numTracks));
+        boolean hasVideoTrack = false;
+        boolean hasAudioTrack = false;
+        for (int i = 0; i < numTracks; i++) {
+            MediaFormat format = extractor.getTrackFormat(i);
+            String mime = format.getString(MediaFormat.KEY_MIME);
+            Log.d(TAG, String.format("Mime type: %s; format: %s", mime, format.toString()));
+            if (mime.contains(VIDEO_MIME_TYPE)) {
+                hasVideoTrack = true;
+                validateVideoTrackMediaFormat(format, expHeight, expWidth, minDurationMillis);
+                // TODO(b/164484222): Validate video track frame drop.
+            } else if (mime.contains(AUDIO_MIME_TYPE)) {
+                hasAudioTrack = true;
+            }
+        }
+        assertThat(hasVideoTrack).isTrue();
+        assertThat(hasAudioTrack).isTrue();
+    }
+
+    /**
+     * Validates that video data {@link MediaFormat} is equal to the expected width and height. And
+     * the duration is longer than certain value.
+     *
+     * @param format The {@link MediaFormat} of the video track.
+     * @param expHeight Expected video track height.
+     * @param expWidth Expected video track width.
+     * @param minDurationMillis Minimum duration in milli seconds. The video track duration should
+     *     be longer than it.
+     */
+    private static void validateVideoTrackMediaFormat(
+            MediaFormat format, int expHeight, int expWidth, long minDurationMillis) {
+        long durationMillis =
+                TimeUnit.MICROSECONDS.toMillis(format.getLong(MediaFormat.KEY_DURATION));
+        int width = format.getInteger(MediaFormat.KEY_WIDTH);
+        int height = format.getInteger(MediaFormat.KEY_HEIGHT);
+        Log.d(
+                TAG,
+                String.format(
+                        "Duration: %d; Width: %d; Height: %d", durationMillis, width, height));
+        assertThat(durationMillis).isGreaterThan(minDurationMillis);
+        assertThat(width).isEqualTo(expWidth);
+        assertThat(height).isEqualTo(expHeight);
+    }
+
+    /**
+     * Validates that the image file height and weight is greater than certain value.
+     *
+     * @param fileDescriptor The {@link FileDescriptor} of the image.
+     * @param minHeight Minimum height. The image height should be greater than this value.
+     * @param minWidth Minimum width. The image width should be greater than this value.
+     */
+    public static void validateImage(FileDescriptor fileDescriptor, int minHeight, int minWidth) {
+        Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor);
+        int height = bitmap.getHeight();
+        int width = bitmap.getWidth();
+        Log.d(TAG, String.format("Height: %d; Width: %d", height, width));
+        assertThat(height).isGreaterThan(minHeight);
+        assertThat(width).isGreaterThan(minWidth);
+    }
+}
diff --git a/libraries/metrics-helper/tests/Android.bp b/libraries/metrics-helper/tests/Android.bp
index 57a7a30..b9b7071 100644
--- a/libraries/metrics-helper/tests/Android.bp
+++ b/libraries/metrics-helper/tests/Android.bp
@@ -28,7 +28,7 @@
         "android.test.base",
     ],
     static_libs: [
-        "android-support-test",
+        "androidx.test.rules",
         "mockito-target-minus-junit4",
         "platform-test-annotations",
         "metrics-helper-lib",
diff --git a/libraries/metrics-helper/tests/AndroidManifest.xml b/libraries/metrics-helper/tests/AndroidManifest.xml
index 7b1bf5d..7996729 100644
--- a/libraries/metrics-helper/tests/AndroidManifest.xml
+++ b/libraries/metrics-helper/tests/AndroidManifest.xml
@@ -22,7 +22,7 @@
     </application>
 
     <instrumentation
-        android:name="android.support.test.runner.AndroidJUnitRunner"
+        android:name="androidx.test.runner.AndroidJUnitRunner"
         android:targetPackage="android.support.test.metricshelper"
         android:label="Metrics Helper Tests" />
 </manifest>
diff --git a/libraries/metrics-helper/tests/src/android/support/test/metricshelper/MetricsAssertsTest.java b/libraries/metrics-helper/tests/src/android/support/test/metricshelper/MetricsAssertsTest.java
index 7dc5b66..6b8f04b 100644
--- a/libraries/metrics-helper/tests/src/android/support/test/metricshelper/MetricsAssertsTest.java
+++ b/libraries/metrics-helper/tests/src/android/support/test/metricshelper/MetricsAssertsTest.java
@@ -15,25 +15,24 @@
  */
 package android.support.test.metricshelper;
 
+import static junit.framework.Assert.assertEquals;
+
+import static org.mockito.Mockito.when;
+
 import android.metrics.LogMaker;
 import android.metrics.MetricsReader;
-import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
-
 import android.test.suitebuilder.annotation.SmallTest;
 
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-import android.support.test.runner.AndroidJUnit4;
-
-import static junit.framework.Assert.assertEquals;
-import static org.mockito.Mockito.when;
-import static org.junit.Assert.fail;
-
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class MetricsAssertsTest {
diff --git a/libraries/system-helpers/accessibility-helper/Android.bp b/libraries/system-helpers/accessibility-helper/Android.bp
index bd7b6cc..4f557b4 100644
--- a/libraries/system-helpers/accessibility-helper/Android.bp
+++ b/libraries/system-helpers/accessibility-helper/Android.bp
@@ -21,7 +21,7 @@
 java_library {
     name: "accessibility-helper",
     libs: [
-        "android-support-test",
+        "androidx.test.rules",
         "ub-uiautomator",
         "settings-helper",
         "package-helper",
diff --git a/libraries/system-helpers/account-helper/Android.bp b/libraries/system-helpers/account-helper/Android.bp
index 7806843..7f950de 100644
--- a/libraries/system-helpers/account-helper/Android.bp
+++ b/libraries/system-helpers/account-helper/Android.bp
@@ -22,7 +22,7 @@
     name: "account-helper",
     libs: [
         "ub-uiautomator",
-        "android-support-test",
+        "androidx.test.rules",
         "activity-helper",
         "device-helper",
     ],
diff --git a/libraries/system-helpers/account-helper/src/android/system/helpers/AccountHelper.java b/libraries/system-helpers/account-helper/src/android/system/helpers/AccountHelper.java
index b210bdd..ec15b8b 100644
--- a/libraries/system-helpers/account-helper/src/android/system/helpers/AccountHelper.java
+++ b/libraries/system-helpers/account-helper/src/android/system/helpers/AccountHelper.java
@@ -19,20 +19,18 @@
 import android.accounts.Account;
 import android.accounts.AccountManager;
 import android.content.Context;
-import android.support.test.InstrumentationRegistry;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.UiDevice;
 import android.support.test.uiautomator.UiObject2;
 import android.support.test.uiautomator.Until;
-import android.system.helpers.ActivityHelper;
-import android.system.helpers.DeviceHelper;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
+
+import androidx.test.InstrumentationRegistry;
+
 import junit.framework.Assert;
 
-/**
- * Implement common helper methods for account.
- */
+import java.util.regex.Pattern;
+
+/** Implement common helper methods for account. */
 public class AccountHelper {
     private static final String TAG = AccountHelper.class.getSimpleName();
     public static final int TIMEOUT = 1000;
diff --git a/libraries/system-helpers/activity-helper/Android.bp b/libraries/system-helpers/activity-helper/Android.bp
index 87fcd83..d58baf0 100644
--- a/libraries/system-helpers/activity-helper/Android.bp
+++ b/libraries/system-helpers/activity-helper/Android.bp
@@ -22,7 +22,7 @@
     name: "activity-helper",
     libs: [
         "ub-uiautomator",
-        "android-support-test",
+        "androidx.test.rules",
         "commands-helper",
     ],
     srcs: ["src/**/*.java"],
diff --git a/libraries/system-helpers/activity-helper/src/android/system/helpers/ActivityHelper.java b/libraries/system-helpers/activity-helper/src/android/system/helpers/ActivityHelper.java
index d16aa50..4c0a952 100644
--- a/libraries/system-helpers/activity-helper/src/android/system/helpers/ActivityHelper.java
+++ b/libraries/system-helpers/activity-helper/src/android/system/helpers/ActivityHelper.java
@@ -19,7 +19,6 @@
 import android.content.Context;
 import android.content.Intent;
 import android.os.RemoteException;
-import android.support.test.InstrumentationRegistry;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.UiDevice;
 import android.support.test.uiautomator.UiScrollable;
@@ -28,6 +27,8 @@
 import android.util.Log;
 import android.view.KeyEvent;
 
+import androidx.test.InstrumentationRegistry;
+
 import junit.framework.Assert;
 
 import java.util.regex.Matcher;
diff --git a/libraries/system-helpers/commands-helper/Android.bp b/libraries/system-helpers/commands-helper/Android.bp
index 4ac6294..e2c1c10 100644
--- a/libraries/system-helpers/commands-helper/Android.bp
+++ b/libraries/system-helpers/commands-helper/Android.bp
@@ -22,7 +22,7 @@
     name: "commands-helper",
     libs: [
         "ub-uiautomator",
-        "android-support-test",
+        "androidx.test.rules",
     ],
     srcs: ["src/**/*.java"],
     sdk_version: "current",
diff --git a/libraries/system-helpers/commands-helper/src/android/system/helpers/CommandsHelper.java b/libraries/system-helpers/commands-helper/src/android/system/helpers/CommandsHelper.java
index 3a9c94f..0ca1a01 100644
--- a/libraries/system-helpers/commands-helper/src/android/system/helpers/CommandsHelper.java
+++ b/libraries/system-helpers/commands-helper/src/android/system/helpers/CommandsHelper.java
@@ -16,9 +16,11 @@
 package android.system.helpers;
 
 import android.app.Instrumentation;
-import android.support.test.InstrumentationRegistry;
 import android.support.test.uiautomator.UiDevice;
 import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.List;
diff --git a/libraries/system-helpers/connectivity-helper/Android.bp b/libraries/system-helpers/connectivity-helper/Android.bp
index 2a78fc5..ef432e2 100644
--- a/libraries/system-helpers/connectivity-helper/Android.bp
+++ b/libraries/system-helpers/connectivity-helper/Android.bp
@@ -22,7 +22,7 @@
     name: "connectivity-helper",
     static_libs: [
         "ub-uiautomator",
-        "android-support-test",
+        "androidx.test.rules",
     ],
     srcs: ["src/**/*.java"],
     sdk_version: "current",
diff --git a/libraries/system-helpers/connectivity-helper/src/android/system/helpers/ConnectivityHelper.java b/libraries/system-helpers/connectivity-helper/src/android/system/helpers/ConnectivityHelper.java
index 1f082ff..b4c92fb 100644
--- a/libraries/system-helpers/connectivity-helper/src/android/system/helpers/ConnectivityHelper.java
+++ b/libraries/system-helpers/connectivity-helper/src/android/system/helpers/ConnectivityHelper.java
@@ -24,11 +24,12 @@
 import android.net.NetworkInfo;
 import android.net.wifi.WifiInfo;
 import android.net.wifi.WifiManager;
-import android.support.test.InstrumentationRegistry;
 import android.telecom.TelecomManager;
 import android.telephony.TelephonyManager;
 import android.util.Log;
 
+import androidx.test.InstrumentationRegistry;
+
 import junit.framework.Assert;
 
 import java.io.IOException;
diff --git a/libraries/system-helpers/device-helper/Android.bp b/libraries/system-helpers/device-helper/Android.bp
index 3aa4703..4fd373b 100644
--- a/libraries/system-helpers/device-helper/Android.bp
+++ b/libraries/system-helpers/device-helper/Android.bp
@@ -22,7 +22,7 @@
     name: "device-helper",
     libs: [
         "ub-uiautomator",
-        "android-support-test",
+        "androidx.test.rules",
     ],
     srcs: ["src/**/*.java"],
     sdk_version: "current",
diff --git a/libraries/system-helpers/device-helper/src/android/system/helpers/DeviceHelper.java b/libraries/system-helpers/device-helper/src/android/system/helpers/DeviceHelper.java
index 73ea852..928ae32 100644
--- a/libraries/system-helpers/device-helper/src/android/system/helpers/DeviceHelper.java
+++ b/libraries/system-helpers/device-helper/src/android/system/helpers/DeviceHelper.java
@@ -19,10 +19,11 @@
 import android.content.Context;
 import android.os.Build;
 import android.os.RemoteException;
-import android.support.test.InstrumentationRegistry;
 import android.support.test.uiautomator.UiDevice;
-import android.view.WindowManager;
 import android.util.DisplayMetrics;
+import android.view.WindowManager;
+
+import androidx.test.InstrumentationRegistry;
 
 /**
  * Implement common helper methods for device.
diff --git a/libraries/system-helpers/permission-helper/Android.bp b/libraries/system-helpers/permission-helper/Android.bp
index 4360691..04a8d02 100644
--- a/libraries/system-helpers/permission-helper/Android.bp
+++ b/libraries/system-helpers/permission-helper/Android.bp
@@ -23,7 +23,7 @@
     static_libs: [
         "ub-uiautomator",
         "launcher-helper-lib",
-        "android-support-test",
+        "androidx.test.rules",
     ],
     srcs: ["src/**/*.java"],
     sdk_version: "test_current",
diff --git a/libraries/system-helpers/settings-helper/Android.bp b/libraries/system-helpers/settings-helper/Android.bp
index cee63c0..0d89012 100644
--- a/libraries/system-helpers/settings-helper/Android.bp
+++ b/libraries/system-helpers/settings-helper/Android.bp
@@ -22,7 +22,7 @@
     name: "settings-helper",
     libs: [
         "ub-uiautomator",
-        "android-support-test",
+        "androidx.test.rules",
         "activity-helper",
     ],
     srcs: ["src/**/*.java"],
diff --git a/libraries/system-helpers/settings-helper/src/android/system/helpers/SettingsHelper.java b/libraries/system-helpers/settings-helper/src/android/system/helpers/SettingsHelper.java
index 68fa858..cffb6f5 100644
--- a/libraries/system-helpers/settings-helper/src/android/system/helpers/SettingsHelper.java
+++ b/libraries/system-helpers/settings-helper/src/android/system/helpers/SettingsHelper.java
@@ -25,7 +25,6 @@
 import android.net.wifi.WifiManager;
 import android.provider.Settings;
 import android.provider.Settings.SettingNotFoundException;
-import android.support.test.InstrumentationRegistry;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.BySelector;
 import android.support.test.uiautomator.Direction;
@@ -40,6 +39,8 @@
 import android.widget.Switch;
 import android.widget.TextView;
 
+import androidx.test.InstrumentationRegistry;
+
 import junit.framework.Assert;
 
 import java.util.Random;
diff --git a/libraries/system-helpers/sysui-helper/Android.bp b/libraries/system-helpers/sysui-helper/Android.bp
index 3fab4b2..b61c1bc 100644
--- a/libraries/system-helpers/sysui-helper/Android.bp
+++ b/libraries/system-helpers/sysui-helper/Android.bp
@@ -22,7 +22,7 @@
     name: "sysui-helper",
     static_libs: [
         "ub-uiautomator",
-        "android-support-test",
+        "androidx.test.rules",
         "activity-helper",
         "commands-helper",
         "device-helper",
diff --git a/libraries/system-helpers/sysui-helper/src/android/system/helpers/HotseatHelper.java b/libraries/system-helpers/sysui-helper/src/android/system/helpers/HotseatHelper.java
index 59e44da..519c79a 100644
--- a/libraries/system-helpers/sysui-helper/src/android/system/helpers/HotseatHelper.java
+++ b/libraries/system-helpers/sysui-helper/src/android/system/helpers/HotseatHelper.java
@@ -18,13 +18,13 @@
 
 import android.content.Context;
 import android.content.pm.PackageManager;
-import android.support.test.InstrumentationRegistry;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.UiDevice;
 import android.support.test.uiautomator.UiObject2;
-import android.support.test.uiautomator.UiObjectNotFoundException;
 import android.support.test.uiautomator.Until;
 
+import androidx.test.InstrumentationRegistry;
+
 import junit.framework.Assert;
 
 /**
diff --git a/libraries/system-helpers/sysui-helper/src/android/system/helpers/LockscreenHelper.java b/libraries/system-helpers/sysui-helper/src/android/system/helpers/LockscreenHelper.java
index 4dbb72c..9717c46 100644
--- a/libraries/system-helpers/sysui-helper/src/android/system/helpers/LockscreenHelper.java
+++ b/libraries/system-helpers/sysui-helper/src/android/system/helpers/LockscreenHelper.java
@@ -20,14 +20,13 @@
 import android.content.Context;
 import android.graphics.Point;
 import android.provider.Settings;
-import android.support.test.InstrumentationRegistry;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.BySelector;
 import android.support.test.uiautomator.UiDevice;
 import android.support.test.uiautomator.UiObject2;
 import android.support.test.uiautomator.Until;
-import android.system.helpers.ActivityHelper;
-import android.system.helpers.DeviceHelper;
+
+import androidx.test.InstrumentationRegistry;
 
 import junit.framework.Assert;
 
diff --git a/libraries/system-helpers/sysui-helper/src/android/system/helpers/NotificationHelper.java b/libraries/system-helpers/sysui-helper/src/android/system/helpers/NotificationHelper.java
index b940256..1096668 100644
--- a/libraries/system-helpers/sysui-helper/src/android/system/helpers/NotificationHelper.java
+++ b/libraries/system-helpers/sysui-helper/src/android/system/helpers/NotificationHelper.java
@@ -26,11 +26,12 @@
 import android.content.Intent;
 import android.os.Handler;
 import android.service.notification.StatusBarNotification;
-import android.support.test.InstrumentationRegistry;
 import android.support.test.uiautomator.UiDevice;
 import android.util.Log;
 import android.widget.Toast;
 
+import androidx.test.InstrumentationRegistry;
+
 import java.util.List;
 
 /**
@@ -253,8 +254,13 @@
             toastIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
             toastIntent.setAction("toast:" + text);
             toastIntent.putExtra("text", text);
-            PendingIntent pi = PendingIntent.getService(
-                    context, 58, toastIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+            PendingIntent pi =
+                    PendingIntent.getService(
+                            context,
+                            58,
+                            toastIntent,
+                            PendingIntent.FLAG_UPDATE_CURRENT
+                                    | PendingIntent.FLAG_MUTABLE_UNAUDITED);
             return pi;
         }
     }
diff --git a/libraries/system-helpers/sysui-helper/src/android/system/helpers/OverviewHelper.java b/libraries/system-helpers/sysui-helper/src/android/system/helpers/OverviewHelper.java
index 3776d53..219e0a9 100644
--- a/libraries/system-helpers/sysui-helper/src/android/system/helpers/OverviewHelper.java
+++ b/libraries/system-helpers/sysui-helper/src/android/system/helpers/OverviewHelper.java
@@ -25,7 +25,6 @@
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.SystemClock;
-import android.support.test.InstrumentationRegistry;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.UiDevice;
 import android.support.test.uiautomator.UiObject2;
@@ -33,6 +32,8 @@
 import android.support.test.uiautomator.Until;
 import android.util.Log;
 
+import androidx.test.InstrumentationRegistry;
+
 import org.junit.Assert;
 
 import java.io.IOException;
diff --git a/libraries/system-helpers/user-helper/Android.bp b/libraries/system-helpers/user-helper/Android.bp
index 640cbe8..5943998 100644
--- a/libraries/system-helpers/user-helper/Android.bp
+++ b/libraries/system-helpers/user-helper/Android.bp
@@ -22,7 +22,7 @@
     name: "user-helper",
     libs: [
         "ub-uiautomator",
-        "android-support-test",
+        "androidx.test.rules",
         "commands-helper",
     ],
     srcs: ["src/**/*.java"],
diff --git a/libraries/system-helpers/user-helper/src/android/system/helpers/UserHelper.java b/libraries/system-helpers/user-helper/src/android/system/helpers/UserHelper.java
index 55f2904..6c17e9a 100644
--- a/libraries/system-helpers/user-helper/src/android/system/helpers/UserHelper.java
+++ b/libraries/system-helpers/user-helper/src/android/system/helpers/UserHelper.java
@@ -18,12 +18,14 @@
 
 import android.content.Context;
 import android.os.UserManager;
-import android.support.test.InstrumentationRegistry;
-import android.system.helpers.CommandsHelper;
 import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+
+import junit.framework.Assert;
+
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
-import junit.framework.Assert;
 
 /**
  * Implement common helper methods for user.
diff --git a/libraries/telephony-utility/Android.bp b/libraries/telephony-utility/Android.bp
index 3afdec1..62e93a6 100644
--- a/libraries/telephony-utility/Android.bp
+++ b/libraries/telephony-utility/Android.bp
@@ -30,5 +30,6 @@
         "junit",
     ],
 
-    sdk_version: "28",
+    sdk_version: "30",
+    min_sdk_version: "28",
 }
diff --git a/scripts/androidx-perf-setup/Android.mk b/scripts/androidx-perf-setup/Android.mk
new file mode 100644
index 0000000..16c60c8
--- /dev/null
+++ b/scripts/androidx-perf-setup/Android.mk
@@ -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.
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := AndroidXComposeStartupApp
+LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
+LOCAL_LICENSE_CONDITIONS := notice
+LOCAL_MODULE_CLASS := APPS
+LOCAL_MODULE_TAGS := optional
+LOCAL_PREBUILT_MODULE_FILE := prebuilts/misc/common/androidx-perf/ui_apks_compose-integration-tests-demos_compose-demos-testapp-release.apk
+LOCAL_CERTIFICATE := PRESIGNED
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA)/local/tmp
+include $(BUILD_PREBUILT)
diff --git a/scripts/perfetto-setup/Android.mk b/scripts/perfetto-setup/Android.mk
index 02544e1..1bc2f02 100644
--- a/scripts/perfetto-setup/Android.mk
+++ b/scripts/perfetto-setup/Android.mk
@@ -55,6 +55,26 @@
 include $(BUILD_PREBUILT)
 
 include $(CLEAR_VARS)
+LOCAL_MODULE := trace_config_multi_user_cuj_tests.textproto
+LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
+LOCAL_LICENSE_CONDITIONS := notice
+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_multi_user_cuj_tests.textproto
+include $(BUILD_PREBUILT)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := trace_config_multi_user.textproto
+LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
+LOCAL_LICENSE_CONDITIONS := notice
+LOCAL_MODULE_CLASS := ETC
+LOCAL_MODULE_TAGS := optional
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA)/local/tmp
+LOCAL_PREBUILT_MODULE_FILE := frameworks/base/apct-tests/perftests/multiuser/trace_configs/trace_config_multi_user.textproto
+include $(BUILD_PREBUILT)
+
+include $(CLEAR_VARS)
 LOCAL_MODULE := perfetto_trace_processor_shell
 LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
 LOCAL_LICENSE_CONDITIONS := notice
diff --git a/tests/apphealth/scenarios/src/android/platform/test/scenario/facebook/FacebookAppHelperImpl.java b/tests/apphealth/scenarios/src/android/platform/test/scenario/facebook/FacebookAppHelperImpl.java
index 8473587..961a58f 100644
--- a/tests/apphealth/scenarios/src/android/platform/test/scenario/facebook/FacebookAppHelperImpl.java
+++ b/tests/apphealth/scenarios/src/android/platform/test/scenario/facebook/FacebookAppHelperImpl.java
@@ -30,6 +30,8 @@
 import androidx.test.uiautomator.UiObject2;
 import androidx.test.uiautomator.Until;
 
+import java.util.regex.Pattern;
+
 import junit.framework.Assert;
 
 /** Create a facebook app helper that can be used to open and navigate FB App */
@@ -44,7 +46,11 @@
     private static final String LOGIN_BUTTON_DESC = "Login";
     private static final String OK_BUTTON_DESC = "OK";
     private static final String DENY_BUTTON_DESC = "Deny";
-    private static final String NEWSFEED_LIST_ID = "android:id/list";
+    private static final String NEWSFEED_LIST_ID = "android:id/content";
+    private static final String GENERIC_ALLOW_TEXT = "ALLOW";
+    private static final String WHILE_USING_APP_TEXT = "While using the app";
+    private static final String REMIND_ME_LATER_TEXT = "REMIND ME LATER";
+    private static final String FEED_TAB_PATTERN = "News Feed, Tab .*";
 
     private static final String LOGIN_ACTIVITY_NAME =
             "com.facebook.account.login.activity.SimpleLoginActivity";
@@ -71,11 +77,38 @@
         Assert.assertNotNull("Login options not found", loginOptions);
         loginOptions.click();
 
+        UiObject2 accessLocation =
+                mDevice.wait(Until.findObject(By.text(GENERIC_ALLOW_TEXT)), LONG_TIMEOUT);
+        if (accessLocation != null) {
+            accessLocation.click();
+            mDevice.waitForIdle();
+
+            UiObject2 locationPermission =
+                    mDevice.wait(Until.findObject(By.text(WHILE_USING_APP_TEXT)), LONG_TIMEOUT);
+            if (locationPermission != null) {
+                locationPermission.click();
+                mDevice.waitForIdle();
+            }
+        }
+
+        UiObject2 outOfDate =
+                mDevice.wait(Until.findObject(By.text(REMIND_ME_LATER_TEXT)), LONG_TIMEOUT);
+        if (outOfDate != null) {
+            outOfDate.click();
+            mDevice.waitForIdle();
+        }
+
         UiObject2 denyCamera =
                 mDevice.wait(Until.findObject(By.desc(DENY_BUTTON_DESC)), LONG_TIMEOUT);
+        if (denyCamera != null) {
+            denyCamera.click();
+            mDevice.waitForIdle();
+        }
 
-        Assert.assertNotNull("Deny Camera button not found", denyCamera);
-        denyCamera.click();
+        Assert.assertTrue(
+                "The phone doesn't appear to be on the news feed tab.",
+                mDevice.wait(
+                        Until.hasObject(By.desc(Pattern.compile(FEED_TAB_PATTERN))), LONG_TIMEOUT));
     }
 
     @Override
@@ -108,13 +141,13 @@
     }
 
     public void scrollNewsfeed(Direction direction, int count) {
-        UiObject2 newsfeedList =
-                mDevice.wait(Until.findObject(By.res(NEWSFEED_LIST_ID)), LONG_TIMEOUT);
-        Assert.assertNotNull("Newsfeed List not found", newsfeedList);
-
         for (int i = 0; i < count; i++) {
+            UiObject2 newsfeedList =
+                    mDevice.wait(Until.findObject(By.res(NEWSFEED_LIST_ID)), LONG_TIMEOUT);
+            Assert.assertNotNull("Newsfeed List not found", newsfeedList);
+            newsfeedList.setGestureMargin(mDevice.getDisplayHeight() / 4);
             newsfeedList.fling(direction);
-            SystemClock.sleep(10);
+            SystemClock.sleep(2500);
         }
     }
 
diff --git a/tests/apphealth/scenarios/src/android/platform/test/scenario/facebook/OpenApp.java b/tests/apphealth/scenarios/src/android/platform/test/scenario/facebook/OpenApp.java
index f49969f..572cebb 100644
--- a/tests/apphealth/scenarios/src/android/platform/test/scenario/facebook/OpenApp.java
+++ b/tests/apphealth/scenarios/src/android/platform/test/scenario/facebook/OpenApp.java
@@ -17,6 +17,7 @@
 package android.platform.test.scenario.facebook;
 
 import android.platform.helpers.HelperAccessor;
+import android.platform.test.option.IntegerOption;
 import android.platform.test.option.StringOption;
 import android.platform.test.rule.AppVersionRule;
 import android.platform.test.scenario.annotation.Scenario;
@@ -45,6 +46,9 @@
     @ClassRule
     public static StringOption sPasswordOption = new StringOption("password").setRequired(true);
 
+    @ClassRule
+    public static IntegerOption sScrollCount = new IntegerOption("scroll-count").setDefault(3);
+
     private static HelperAccessor<IFacebookAppHelper> sHelper =
             new HelperAccessor<>(IFacebookAppHelper.class);
 
@@ -53,7 +57,7 @@
         sHelper.get().open();
         sHelper.get().loginWithUi(sUsernameOption.get(), sPasswordOption.get());
         sHelper.get().dismissInitialDialogs();
-        sHelper.get().scrollNewsfeed(Direction.DOWN, 3);
+        sHelper.get().scrollNewsfeed(Direction.DOWN, sScrollCount.get());
         sHelper.get().exit();
     }
 }
diff --git a/tests/apphealth/scenarios/tests/Android.bp b/tests/apphealth/scenarios/tests/Android.bp
index 5558968..5b424e5 100644
--- a/tests/apphealth/scenarios/tests/Android.bp
+++ b/tests/apphealth/scenarios/tests/Android.bp
@@ -20,7 +20,7 @@
     name: "FacebookAppsScenarioTests",
     min_sdk_version: "24",
     static_libs: [
-        "android-support-test",
+        "androidx.test.rules",
         "collector-device-lib-platform",
         "common-profile-text-protos",
         "facebook-scenarios",
@@ -33,6 +33,7 @@
         "platform-test-composers",
         "platform-test-options",
         "platform-test-rules",
+        "ub-uiautomator",
     ],
     srcs: ["src/**/*.java"],
     test_suites: ["device-tests"],
diff --git a/tests/apphealth/scenarios/tests/AndroidManifest.xml b/tests/apphealth/scenarios/tests/AndroidManifest.xml
index aab81e2..e09019e 100644
--- a/tests/apphealth/scenarios/tests/AndroidManifest.xml
+++ b/tests/apphealth/scenarios/tests/AndroidManifest.xml
@@ -21,6 +21,7 @@
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+    <uses-permission android:name="android.permission.READ_LOGS" />
     <application>
         <uses-library android:name="android.test.runner"/>
     </application>
diff --git a/tests/automotive/functional/appgrid/Android.bp b/tests/automotive/functional/appgrid/Android.bp
new file mode 100644
index 0000000..54bb764
--- /dev/null
+++ b/tests/automotive/functional/appgrid/Android.bp
@@ -0,0 +1,34 @@
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "AndroidAutomotiveAppGridTests",
+    min_sdk_version: "24",
+    static_libs: [
+        "androidx.test.runner",
+        "androidx.test.rules",
+        "automotive-app-grid-helper",
+        "automotive-utility-helper",
+        "app-helpers-auto-interfaces",
+        "ub-uiautomator",
+        "hamcrest-library",
+        "platform-test-options",
+    ],
+    srcs: ["src/**/*.java"],
+    test_suites: ["catbox"],
+}
diff --git a/tests/automotive/functional/appgrid/AndroidManifest.xml b/tests/automotive/functional/appgrid/AndroidManifest.xml
new file mode 100644
index 0000000..71d8c42
--- /dev/null
+++ b/tests/automotive/functional/appgrid/AndroidManifest.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2021 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="android.platform.tests">
+  <uses-sdk android:minSdkVersion="23"
+      android:targetSdkVersion="24" />
+  <application>
+    <uses-library android:name="android.test.runner" />
+  </application>
+  <uses-permission android:name="android.permission.BLUETOOTH"/>
+  <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+  <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+  <uses-permission android:name="android.car.permission.CAR_CONTROL_AUDIO_VOLUME"/>
+  <uses-permission android:name="android.car.permission.CAR_CONTROL_AUDIO_SETTINGS"/>
+  <uses-permission android:name="android.car.permission.CAR_DRIVING_STATE"/>
+  <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+  <instrumentation
+      android:name="androidx.test.runner.AndroidJUnitRunner"
+      android:targetPackage="android.platform.tests"
+      android:label="Android Automotive App Grid App Tests" />
+</manifest>
\ No newline at end of file
diff --git a/tests/automotive/functional/appgrid/src/android/platform/tests/AppGridTest.java b/tests/automotive/functional/appgrid/src/android/platform/tests/AppGridTest.java
new file mode 100644
index 0000000..1b89492
--- /dev/null
+++ b/tests/automotive/functional/appgrid/src/android/platform/tests/AppGridTest.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2021 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.tests;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import android.platform.helpers.AutoUtility;
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoAppGridHelper;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class AppGridTest {
+
+    private HelperAccessor<IAutoAppGridHelper> mAppGridHelper;
+
+    public AppGridTest() {
+        mAppGridHelper = new HelperAccessor<>(IAutoAppGridHelper.class);
+    }
+
+    @BeforeClass
+    public static void exitSuw() {
+        AutoUtility.exitSuw();
+    }
+
+    @Test
+    public void testOpen() {
+        // Make sure app grid is not open before testing.
+        mAppGridHelper.get().exit();
+        assertFalse(mAppGridHelper.get().isAppInForeground());
+        // Test open.
+        mAppGridHelper.get().open();
+        assertTrue(mAppGridHelper.get().isAppInForeground());
+    }
+
+    @Test
+    public void testExit() {
+        // Make sure app grid has been opened before testing.
+        mAppGridHelper.get().open();
+        assertTrue(mAppGridHelper.get().isAppInForeground());
+        // Test exit.
+        mAppGridHelper.get().exit();
+        assertFalse(mAppGridHelper.get().isAppInForeground());
+    }
+
+    @Test
+    public void testScroll() {
+        // Re-enter app grid.
+        mAppGridHelper.get().exit();
+        mAppGridHelper.get().open();
+        assertTrue(mAppGridHelper.get().isTop());
+        // Test scroll only when there are more than one page in app grid.
+        if (!mAppGridHelper.get().isBottom()) {
+            mAppGridHelper.get().scrollDownOnePage();
+            assertFalse(mAppGridHelper.get().isTop());
+            mAppGridHelper.get().scrollUpOnePage();
+            assertTrue(mAppGridHelper.get().isTop());
+        }
+    }
+}
diff --git a/tests/automotive/functional/dialer/Android.bp b/tests/automotive/functional/dialer/Android.bp
new file mode 100644
index 0000000..0993fd1
--- /dev/null
+++ b/tests/automotive/functional/dialer/Android.bp
@@ -0,0 +1,35 @@
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "AndroidAutomotiveDialTests",
+    min_sdk_version: "24",
+    static_libs: [
+        "androidx.test.runner",
+        "androidx.test.rules",
+        "automotive-dial-app-helper",
+        "automotive-vehiclehardkeys-app-helper",
+        "automotive-utility-helper",
+        "app-helpers-auto-interfaces",
+        "ub-uiautomator",
+        "hamcrest-library",
+        "platform-test-options",
+    ],
+    srcs: ["src/**/*.java"],
+    test_suites: ["catbox"],
+}
diff --git a/tests/automotive/functional/dialer/AndroidManifest.xml b/tests/automotive/functional/dialer/AndroidManifest.xml
new file mode 100644
index 0000000..71c08c7
--- /dev/null
+++ b/tests/automotive/functional/dialer/AndroidManifest.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2021 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="android.platform.tests">
+    <uses-sdk android:minSdkVersion="23"
+              android:targetSdkVersion="24" />
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+    <uses-permission android:name="android.permission.BLUETOOTH"/>
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+    <uses-permission android:name="android.car.permission.CAR_CONTROL_AUDIO_VOLUME"/>
+    <uses-permission android:name="android.car.permission.CAR_CONTROL_AUDIO_SETTINGS"/>
+    <uses-permission android:name="android.car.permission.CAR_DRIVING_STATE"/>
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="android.platform.tests"
+        android:label="Android Automotive Dial App Tests" />
+</manifest>
\ No newline at end of file
diff --git a/tests/automotive/functional/dialer/src/android/platform/tests/DialTest.java b/tests/automotive/functional/dialer/src/android/platform/tests/DialTest.java
new file mode 100644
index 0000000..5f1ae73
--- /dev/null
+++ b/tests/automotive/functional/dialer/src/android/platform/tests/DialTest.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2021 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.tests;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import android.platform.helpers.AutoUtility;
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoDialContactDetailsHelper;
+import android.platform.helpers.IAutoDialContactDetailsHelper.ContactType;
+import android.platform.helpers.IAutoDialHelper;
+import android.platform.helpers.IAutoDialHelper.AudioSource;
+import android.platform.helpers.IAutoDialHelper.OrderType;
+import android.platform.helpers.IAutoVehicleHardKeysHelper;
+import android.platform.test.option.StringOption;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.runner.RunWith;
+import org.junit.Test;
+
+@RunWith(AndroidJUnit4.class)
+public class DialTest {
+    public static final String DIAL_A_NUMBER = "Dial a number";
+    public static final String DIALED_CONTACT = "Aaron";
+    public static final String FAVORITES_CONTACT = "Bob";
+    public static final String DETAILED_CONTACT = "Aaron";
+    public static final String DIAL_CONTACT_BY_NAME = "Jane Doe";
+    public static final String CONTACT_TYPE = "Work";
+
+    private static final String SMALL_NUMBER_PARAM = "small-phone-number";
+    private static final String LARGE_NUMBER_PARAM = "large-phone-number";
+    private static final String SEARCH_CONTACT_NUMBER_PARAM = "search-contact-number";
+    private static final String SEARCH_CONTACT_NAME_PARAM = "search-contact-name";
+
+    @ClassRule
+    public static StringOption mSmallPhoneNumber =
+            new StringOption(SMALL_NUMBER_PARAM).setRequired(true);
+
+    @ClassRule
+    public static StringOption mLargePhoneNumber =
+            new StringOption(LARGE_NUMBER_PARAM).setRequired(true);
+
+    @ClassRule
+    public static StringOption mSearchContactNumber =
+            new StringOption(SEARCH_CONTACT_NUMBER_PARAM).setRequired(true);
+
+    @ClassRule
+    public static StringOption mSearchContactName =
+            new StringOption(SEARCH_CONTACT_NAME_PARAM).setRequired(true);
+
+    private HelperAccessor<IAutoDialHelper> mDialerHelper;
+    private HelperAccessor<IAutoVehicleHardKeysHelper> mVehicleHardKeysHelper;
+    private HelperAccessor<IAutoDialContactDetailsHelper> mContactDetailsHelper;
+
+    public DialTest() throws Exception {
+        mDialerHelper = new HelperAccessor<>(IAutoDialHelper.class);
+        mContactDetailsHelper = new HelperAccessor<>(IAutoDialContactDetailsHelper.class);
+        mVehicleHardKeysHelper = new HelperAccessor<>(IAutoVehicleHardKeysHelper.class);
+    }
+
+    @BeforeClass
+    public static void setUp() {
+        AutoUtility.exitSuw();
+    }
+
+    @After
+    public void endCall() {
+        mVehicleHardKeysHelper.get().pressEndCallKey();
+    }
+
+    @Test
+    public void testDialSmallNumber() {
+        mDialerHelper.get().dialANumber(mSmallPhoneNumber.get());
+        mDialerHelper.get().makeCall();
+        String actualDialedNumber = mDialerHelper.get().getDialedNumber();
+        assertEquals(mSmallPhoneNumber.get(), actualDialedNumber.replaceAll("[-()\\s]", ""));
+        mDialerHelper.get().endCall();
+    }
+
+    @Test
+    public void testDialLargeNumber() {
+        mDialerHelper.get().dialANumber(mLargePhoneNumber.get());
+        mDialerHelper.get().makeCall();
+        String actualDialedNumber = mDialerHelper.get().getDialedNumber();
+        assertEquals(mLargePhoneNumber.get(), actualDialedNumber.replaceAll("[-()\\s]", ""));
+        mDialerHelper.get().endCall();
+    }
+
+    @Test
+    public void testHistoryUpdatesCalledNumber() {
+        mDialerHelper.get().dialANumber(mSmallPhoneNumber.get());
+        mDialerHelper.get().makeCall();
+        mDialerHelper.get().endCall();
+        mDialerHelper.get().openCallHistory();
+        assertTrue(
+                "Call History did not update",
+                mDialerHelper.get().getRecentCallHistory().equals(mSmallPhoneNumber.get()));
+    }
+
+    @Test
+    public void testHistoryUpdatesCalledContactName() {
+        mDialerHelper.get().open();
+        mDialerHelper.get().callContact(DIAL_CONTACT_BY_NAME);
+        mDialerHelper.get().endCall();
+        mDialerHelper.get().openCallHistory();
+        assertTrue(
+                "Call History did not update",
+                mDialerHelper.get().getRecentCallHistory().equals(DIAL_CONTACT_BY_NAME));
+    }
+
+    @Test
+    public void testDeleteDialedNumber() {
+        mDialerHelper.get().dialANumber(mSmallPhoneNumber.get());
+        mDialerHelper.get().deleteDialedNumber();
+        String numberAfterDelete = mDialerHelper.get().getDialInNumber();
+        assertTrue(DIAL_A_NUMBER.equals(numberAfterDelete));
+    }
+
+    @Test
+    public void testMuteUnmuteCall() {
+        mDialerHelper.get().dialANumber(mSmallPhoneNumber.get());
+        mDialerHelper.get().makeCall();
+        try {
+            mDialerHelper.get().muteCall();
+            mDialerHelper.get().unmuteCall();
+        } catch (RuntimeException e) {
+            throw new RuntimeException(e);
+        } finally {
+            mDialerHelper.get().endCall();
+        }
+    }
+
+    @Test
+    public void testEndCallHardkey() {
+        mDialerHelper.get().dialANumber(mLargePhoneNumber.get());
+        mDialerHelper.get().makeCall();
+        String actualDialedNumber = mDialerHelper.get().getDialedNumber();
+        assertEquals(mLargePhoneNumber.get(), actualDialedNumber.replaceAll("[-()\\s]", ""));
+        mVehicleHardKeysHelper.get().pressEndCallKey();
+    }
+
+    @Test
+    public void testCallAudioSourceTransfer() {
+        mDialerHelper.get().dialANumber(mSmallPhoneNumber.get());
+        mDialerHelper.get().makeCall();
+        mDialerHelper.get().changeAudioSource(AudioSource.PHONE);
+        mDialerHelper.get().changeAudioSource(AudioSource.CAR_SPEAKERS);
+        mDialerHelper.get().endCall();
+    }
+
+    @Test
+    public void testCallFromHistory() {
+        mDialerHelper.get().dialANumber(mSmallPhoneNumber.get());
+        mDialerHelper.get().makeCall();
+        mDialerHelper.get().endCall();
+        mDialerHelper.get().openCallHistory();
+        mDialerHelper.get().callMostRecentHistory();
+        assertTrue(
+                "History is not same as dialed number.",
+                mDialerHelper.get().getDialedContactName().equals(mSmallPhoneNumber.get()));
+        mDialerHelper.get().endCall();
+    }
+
+    @Test
+    public void testDisplayedNameMatchesCalledContactName() {
+        mDialerHelper.get().open();
+        mDialerHelper.get().callContact(DIAL_CONTACT_BY_NAME);
+        assertTrue(
+                "Contact name is not the same",
+                mDialerHelper.get().getContactName().contains(DIAL_CONTACT_BY_NAME));
+        mDialerHelper.get().endCall();
+    }
+
+    @Test
+    public void testDisplayedContactTypeMatchesCalledContactType() {
+        mDialerHelper.get().open();
+        mDialerHelper.get().callContact(DIAL_CONTACT_BY_NAME);
+        assertTrue(
+                "Contact detail is not the same",
+                mDialerHelper.get().getContactType().contains(CONTACT_TYPE));
+        mDialerHelper.get().endCall();
+    }
+
+    @Test
+    public void testSearchContactByName() {
+        mDialerHelper.get().open();
+        mDialerHelper.get().searchContactsByName("Jane");
+        assertEquals(
+                "Cannot find contact",
+                DIAL_CONTACT_BY_NAME,
+                mDialerHelper.get().getFirstSearchResult());
+    }
+
+    @Test
+    public void testSearchContactByNumber() {
+        mDialerHelper.get().open();
+        mDialerHelper.get().searchContactsByNumber(mSearchContactNumber.get());
+        assertEquals(
+                "Cannot find contact",
+                mSearchContactName.get(),
+                mDialerHelper.get().getFirstSearchResult());
+    }
+
+    @Test
+    public void testSortContacts() {
+        mDialerHelper.get().open();
+        mDialerHelper.get().sortContactListBy(OrderType.LAST_NAME);
+        assertEquals(
+                "Order by last name is not correct.",
+                mDialerHelper.get().getFirstContactFromContactList(),
+                DIALED_CONTACT);
+        mDialerHelper.get().sortContactListBy(OrderType.FIRST_NAME);
+        assertEquals(
+                "Order is not correct.",
+                mDialerHelper.get().getFirstContactFromContactList(),
+                DIALED_CONTACT);
+    }
+
+    @Test
+    public void testAddRemoveFavoriteContact() {
+        mDialerHelper.get().open();
+        mDialerHelper.get().openDetailsPage(FAVORITES_CONTACT);
+        mContactDetailsHelper.get().addRemoveFavoriteContact();
+        mContactDetailsHelper.get().closeDetailsPage();
+        assertTrue(
+                "Contact is not added to favorites.",
+                mDialerHelper.get().isContactInFavorites(FAVORITES_CONTACT));
+        mDialerHelper.get().openDetailsPage(FAVORITES_CONTACT);
+        mContactDetailsHelper.get().addRemoveFavoriteContact();
+        mContactDetailsHelper.get().closeDetailsPage();
+        assertFalse(
+                "Contact is not removed from favorites.",
+                mDialerHelper.get().isContactInFavorites(FAVORITES_CONTACT));
+    }
+
+    @Test
+    public void testMakeCallFromContactDetailsPage() {
+        mDialerHelper.get().open();
+        mDialerHelper.get().openDetailsPage(DETAILED_CONTACT);
+        mContactDetailsHelper.get().makeCallFromDetailsPageByType(ContactType.MOBILE);
+        assertTrue(
+                "Contact name is not the same",
+                mDialerHelper.get().getContactName().contains(DETAILED_CONTACT));
+        mDialerHelper.get().endCall();
+        mContactDetailsHelper.get().closeDetailsPage();
+    }
+}
diff --git a/tests/automotive/functional/home/Android.bp b/tests/automotive/functional/home/Android.bp
new file mode 100644
index 0000000..19f5b9b
--- /dev/null
+++ b/tests/automotive/functional/home/Android.bp
@@ -0,0 +1,34 @@
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "AndroidAutomotiveHomeTests",
+    min_sdk_version: "24",
+    static_libs: [
+        "androidx.test.runner",
+        "androidx.test.rules",
+        "automotive-home-helper",
+        "automotive-utility-helper",
+        "app-helpers-auto-interfaces",
+        "ub-uiautomator",
+        "hamcrest-library",
+        "platform-test-options",
+    ],
+    srcs: ["src/**/*.java"],
+    test_suites: ["catbox"],
+}
diff --git a/tests/automotive/functional/home/AndroidManifest.xml b/tests/automotive/functional/home/AndroidManifest.xml
new file mode 100644
index 0000000..d6777c2
--- /dev/null
+++ b/tests/automotive/functional/home/AndroidManifest.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2021 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="android.platform.tests">
+  <uses-sdk android:minSdkVersion="23"
+      android:targetSdkVersion="24" />
+  <application>
+    <uses-library android:name="android.test.runner" />
+  </application>
+  <uses-permission android:name="android.permission.BLUETOOTH"/>
+  <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+  <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+  <uses-permission android:name="android.car.permission.CAR_CONTROL_AUDIO_VOLUME"/>
+  <uses-permission android:name="android.car.permission.CAR_CONTROL_AUDIO_SETTINGS"/>
+  <uses-permission android:name="android.car.permission.CAR_DRIVING_STATE"/>
+  <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+  <instrumentation
+      android:name="androidx.test.runner.AndroidJUnitRunner"
+      android:targetPackage="android.platform.tests"
+      android:label="Android Automotive Home App Tests" />
+</manifest>
\ No newline at end of file
diff --git a/tests/automotive/functional/home/src/android/platform/tests/HomeTest.java b/tests/automotive/functional/home/src/android/platform/tests/HomeTest.java
new file mode 100644
index 0000000..dc47b78
--- /dev/null
+++ b/tests/automotive/functional/home/src/android/platform/tests/HomeTest.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2021 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.tests;
+
+import android.platform.helpers.AutoUtility;
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoHomeHelper;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static junit.framework.Assert.assertTrue;
+
+@RunWith(AndroidJUnit4.class)
+public class HomeTest {
+    private HelperAccessor<IAutoHomeHelper> mHomeHelper;
+
+    public HomeTest() {
+        mHomeHelper = new HelperAccessor<>(IAutoHomeHelper.class);
+    }
+
+    @BeforeClass
+    public static void exitSuw() {
+        AutoUtility.exitSuw();
+    }
+
+    @Before
+    public void setup() {
+        mHomeHelper.get().open();
+    }
+
+    @Test
+    public void testAssistantWidget() {
+        assertTrue(mHomeHelper.get().hasAssistantWidget());
+    }
+
+    @Test
+    public void testMediaWidget() {
+        assertTrue(mHomeHelper.get().hasMediaWidget());
+    }
+}
diff --git a/tests/automotive/functional/lockscreen/Android.bp b/tests/automotive/functional/lockscreen/Android.bp
new file mode 100644
index 0000000..1ebe583
--- /dev/null
+++ b/tests/automotive/functional/lockscreen/Android.bp
@@ -0,0 +1,35 @@
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "AndroidAutomotiveLockScreenTests",
+    min_sdk_version: "24",
+    static_libs: [
+        "androidx.test.runner",
+        "androidx.test.rules",
+        "automotive-lock-screen-helper",
+        "automotive-settings-app-helper",
+        "automotive-utility-helper",
+        "app-helpers-auto-interfaces",
+        "ub-uiautomator",
+        "hamcrest-library",
+        "platform-test-options",
+    ],
+    srcs: ["src/**/*.java"],
+    test_suites: ["catbox"],
+}
diff --git a/tests/automotive/functional/lockscreen/AndroidManifest.xml b/tests/automotive/functional/lockscreen/AndroidManifest.xml
new file mode 100644
index 0000000..5d5f3e8
--- /dev/null
+++ b/tests/automotive/functional/lockscreen/AndroidManifest.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2021 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="android.platform.tests">
+  <uses-sdk android:minSdkVersion="23"
+      android:targetSdkVersion="24" />
+  <application>
+    <uses-library android:name="android.test.runner" />
+  </application>
+  <uses-permission android:name="android.permission.BLUETOOTH"/>
+  <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+  <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+  <uses-permission android:name="android.car.permission.CAR_CONTROL_AUDIO_VOLUME"/>
+  <uses-permission android:name="android.car.permission.CAR_CONTROL_AUDIO_SETTINGS"/>
+  <uses-permission android:name="android.car.permission.CAR_DRIVING_STATE"/>
+  <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+  <instrumentation
+      android:name="androidx.test.runner.AndroidJUnitRunner"
+      android:targetPackage="android.platform.tests"
+      android:label="Android Automotive Lock Screen Tests" />
+</manifest>
\ No newline at end of file
diff --git a/tests/automotive/functional/lockscreen/src/android/platform/tests/LockScreenTest.java b/tests/automotive/functional/lockscreen/src/android/platform/tests/LockScreenTest.java
new file mode 100644
index 0000000..e5d78f7
--- /dev/null
+++ b/tests/automotive/functional/lockscreen/src/android/platform/tests/LockScreenTest.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2021 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.tests;
+
+import static junit.framework.Assert.assertTrue;
+
+import android.platform.helpers.AutoConfigConstants;
+import android.platform.helpers.AutoUtility;
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoLockScreenHelper;
+import android.platform.helpers.IAutoLockScreenHelper.LockType;
+import android.platform.helpers.IAutoSecuritySettingsHelper;
+import android.platform.helpers.IAutoSettingHelper;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class LockScreenTest {
+
+    private static final String PASSWORD = "test4fun";
+    private static final String PIN = "1234";
+
+    private HelperAccessor<IAutoLockScreenHelper> mLockScreenHelper;
+    private HelperAccessor<IAutoSecuritySettingsHelper> mSecuritySettingsHelper;
+    private HelperAccessor<IAutoSettingHelper> mSettingHelper;
+
+    public LockScreenTest() throws Exception {
+        mSecuritySettingsHelper = new HelperAccessor<>(IAutoSecuritySettingsHelper.class);
+        mLockScreenHelper = new HelperAccessor<>(IAutoLockScreenHelper.class);
+        mSettingHelper = new HelperAccessor<>(IAutoSettingHelper.class);
+    }
+
+    @BeforeClass
+    public static void exitSuw() {
+        AutoUtility.exitSuw();
+    }
+
+    @Before
+    public void openSecuritySettingFacet() {
+        mSettingHelper.get().openSetting(AutoConfigConstants.SECURITY_SETTINGS);
+    }
+
+    @After
+    public void goBackToSettingsScreen() {
+        mSettingHelper.get().goBackToSettingsScreen();
+    }
+
+    @Test
+    public void testLockUnlockScreenByPassword() {
+        mLockScreenHelper.get().lockScreenBy(LockType.PASSWORD, PASSWORD);
+        mLockScreenHelper.get().unlockScreenBy(LockType.PASSWORD, PASSWORD);
+        mSecuritySettingsHelper.get().removeLock();
+        assertTrue(
+                "Password has not been removed", !mSecuritySettingsHelper.get().isDeviceLocked());
+    }
+}
diff --git a/tests/automotive/functional/mediacenter/Android.bp b/tests/automotive/functional/mediacenter/Android.bp
new file mode 100644
index 0000000..ada76da
--- /dev/null
+++ b/tests/automotive/functional/mediacenter/Android.bp
@@ -0,0 +1,34 @@
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "AndroidAutomotiveMediaCenterTests",
+    min_sdk_version: "24",
+    static_libs: [
+        "androidx.test.runner",
+        "androidx.test.rules",
+        "automotive-media-center-app-helper",
+        "automotive-utility-helper",
+        "app-helpers-auto-interfaces",
+        "ub-uiautomator",
+        "hamcrest-library",
+        "platform-test-options",
+    ],
+    srcs: ["src/**/*.java"],
+    test_suites: ["catbox"],
+}
diff --git a/tests/automotive/functional/mediacenter/AndroidManifest.xml b/tests/automotive/functional/mediacenter/AndroidManifest.xml
new file mode 100644
index 0000000..1d71bc4
--- /dev/null
+++ b/tests/automotive/functional/mediacenter/AndroidManifest.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2021 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="android.platform.tests">
+    <uses-sdk android:minSdkVersion="23"
+              android:targetSdkVersion="24" />
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+    <uses-permission android:name="android.permission.BLUETOOTH"/>
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+    <uses-permission android:name="android.car.permission.CAR_CONTROL_AUDIO_VOLUME"/>
+    <uses-permission android:name="android.car.permission.CAR_CONTROL_AUDIO_SETTINGS"/>
+    <uses-permission android:name="android.car.permission.CAR_DRIVING_STATE"/>
+    <uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL"/>
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="android.platform.tests"
+        android:label="Android Automotive Media Center Tests" />
+</manifest>
\ No newline at end of file
diff --git a/tests/automotive/functional/mediacenter/src/android/platform/tests/BluetoothAudioTest.java b/tests/automotive/functional/mediacenter/src/android/platform/tests/BluetoothAudioTest.java
new file mode 100644
index 0000000..708c7da
--- /dev/null
+++ b/tests/automotive/functional/mediacenter/src/android/platform/tests/BluetoothAudioTest.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2021 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.tests;
+
+import static junit.framework.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static junit.framework.Assert.assertTrue;
+
+import android.platform.helpers.AutoUtility;
+import android.platform.helpers.IAutoMediaHelper;
+import android.platform.helpers.HelperAccessor;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class BluetoothAudioTest {
+    private HelperAccessor<IAutoMediaHelper> mBluetoothAudioHelper;
+
+    public BluetoothAudioTest() throws Exception {
+        mBluetoothAudioHelper = new HelperAccessor<>(IAutoMediaHelper.class);
+    }
+
+    @BeforeClass
+    public static void exitSuw() {
+        AutoUtility.exitSuw();
+    }
+
+    @Before
+    public void openMediaFacet() {
+        mBluetoothAudioHelper.get().open();
+    }
+
+    @After
+    public void goBackToMediaFacet() {
+        mBluetoothAudioHelper.get().goBackToMediaHomePage();
+    }
+
+    @Test
+    public void testPlayPauseMedia() {
+        mBluetoothAudioHelper.get().pauseMedia();
+        assertFalse("Song not paused.", mBluetoothAudioHelper.get().isPlaying());
+        mBluetoothAudioHelper.get().playMedia();
+        assertTrue("Song not playing.", mBluetoothAudioHelper.get().isPlaying());
+    }
+
+    @Test
+    public void testNextTrack() {
+        String currentSong = mBluetoothAudioHelper.get().getMediaTrackName();
+        mBluetoothAudioHelper.get().clickNextTrack();
+        assertNotEquals(
+                "Song playing has not been changed",
+                currentSong,
+                mBluetoothAudioHelper.get().getMediaTrackName());
+    }
+}
diff --git a/tests/automotive/functional/navigationbar/Android.bp b/tests/automotive/functional/navigationbar/Android.bp
new file mode 100644
index 0000000..f728028
--- /dev/null
+++ b/tests/automotive/functional/navigationbar/Android.bp
@@ -0,0 +1,37 @@
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "AndroidAutomotiveNavigationBarTests",
+    min_sdk_version: "24",
+    static_libs: [
+        "androidx.test.runner",
+        "androidx.test.rules",
+        "automotive-notifications-app-helper",
+        "automotive-settings-app-helper",
+        "automotive-home-helper",
+        "automotive-dial-app-helper",
+        "automotive-app-grid-helper",
+        "automotive-utility-helper",
+        "app-helpers-auto-interfaces",
+        "ub-uiautomator",
+        "hamcrest-library",
+    ],
+    srcs: ["src/**/*.java"],
+    test_suites: ["catbox"],
+}
diff --git a/tests/automotive/functional/navigationbar/AndroidManifest.xml b/tests/automotive/functional/navigationbar/AndroidManifest.xml
new file mode 100644
index 0000000..903c0c4
--- /dev/null
+++ b/tests/automotive/functional/navigationbar/AndroidManifest.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2021 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="android.platform.tests">
+  <uses-sdk android:minSdkVersion="23"
+      android:targetSdkVersion="24" />
+  <application>
+    <uses-library android:name="android.test.runner" />
+  </application>
+  <uses-permission android:name="android.permission.BLUETOOTH"/>
+  <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+  <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+  <uses-permission android:name="android.car.permission.CAR_CONTROL_AUDIO_VOLUME"/>
+  <uses-permission android:name="android.car.permission.CAR_CONTROL_AUDIO_SETTINGS"/>
+  <uses-permission android:name="android.car.permission.CAR_DRIVING_STATE"/>
+  <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+  <instrumentation
+      android:name="androidx.test.runner.AndroidJUnitRunner"
+      android:targetPackage="android.platform.tests"
+      android:label="Android Automotive Navigation Bar Tests" />
+</manifest>
\ No newline at end of file
diff --git a/tests/automotive/functional/navigationbar/src/android/platform/tests/NavigationBarTest.java b/tests/automotive/functional/navigationbar/src/android/platform/tests/NavigationBarTest.java
new file mode 100644
index 0000000..dd63162
--- /dev/null
+++ b/tests/automotive/functional/navigationbar/src/android/platform/tests/NavigationBarTest.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2021 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.tests;
+
+import android.app.Instrumentation;
+import android.platform.helpers.AutoUtility;
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoAppGridHelper;
+import android.platform.helpers.IAutoDialHelper;
+import android.platform.helpers.IAutoHomeHelper;
+import android.platform.helpers.IAutoNotificationHelper;
+import android.platform.helpers.IAutoSettingHelper;
+import android.support.test.uiautomator.UiDevice;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+@RunWith(AndroidJUnit4.class)
+public class NavigationBarTest {
+
+    private Instrumentation mInstrumentation;
+    private UiDevice mDevice;
+    private HelperAccessor<IAutoAppGridHelper> mAppGridHelper;
+    private HelperAccessor<IAutoDialHelper> mDialerHelper;
+    private HelperAccessor<IAutoNotificationHelper> mNotificationHelper;
+    private HelperAccessor<IAutoSettingHelper> mSettingHelper;
+    private HelperAccessor<IAutoHomeHelper> mHomeHelper;
+
+    public NavigationBarTest() {
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
+        mDevice = UiDevice.getInstance(mInstrumentation);
+        mHomeHelper = new HelperAccessor<>(IAutoHomeHelper.class);
+        mAppGridHelper = new HelperAccessor<>(IAutoAppGridHelper.class);
+        mNotificationHelper = new HelperAccessor<>(IAutoNotificationHelper.class);
+        mSettingHelper = new HelperAccessor<>(IAutoSettingHelper.class);
+        mDialerHelper = new HelperAccessor<>(IAutoDialHelper.class);
+    }
+
+    @BeforeClass
+    public static void exitSuw() {
+        AutoUtility.exitSuw();
+    }
+
+    @After
+    public void goBackToHomeScreen() {
+        mDevice.pressHome();
+    }
+
+    @Test
+    public void testHomeButton() {
+        mHomeHelper.get().open();
+        assertTrue(mHomeHelper.get().hasMediaWidget());
+    }
+
+    @Test
+    public void testDialButton() {
+        mDialerHelper.get().open();
+        assertFalse("Phone is paired.", mDialerHelper.get().isPhonePaired());
+    }
+
+    @Test
+    public void testAppGridButton() {
+        mAppGridHelper.get().open();
+        assertTrue("App Grid is not open.", mAppGridHelper.get().isAppInForeground());
+    }
+
+    @Test
+    public void testNotificationButton() {
+        mNotificationHelper.get().open();
+        assertTrue("Notification did not open.", mNotificationHelper.get().isAppInForeground());
+    }
+
+    @Test
+    public void testQuickSetting() {
+        mSettingHelper.get().openQuickSettings();
+    }
+}
diff --git a/tests/automotive/functional/notifications/Android.bp b/tests/automotive/functional/notifications/Android.bp
new file mode 100644
index 0000000..c962633
--- /dev/null
+++ b/tests/automotive/functional/notifications/Android.bp
@@ -0,0 +1,33 @@
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "AndroidAutomotiveNotificationsTests",
+    min_sdk_version: "24",
+    static_libs: [
+        "androidx.test.runner",
+        "androidx.test.rules",
+        "automotive-notifications-app-helper",
+        "automotive-utility-helper",
+        "app-helpers-auto-interfaces",
+        "ub-uiautomator",
+        "hamcrest-library",
+    ],
+    srcs: ["src/**/*.java"],
+    test_suites: ["catbox"],
+}
diff --git a/tests/automotive/functional/notifications/AndroidManifest.xml b/tests/automotive/functional/notifications/AndroidManifest.xml
new file mode 100644
index 0000000..ece1f08
--- /dev/null
+++ b/tests/automotive/functional/notifications/AndroidManifest.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2021 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="android.platform.tests">
+    <uses-sdk android:minSdkVersion="23"
+              android:targetSdkVersion="24" />
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+    <uses-permission android:name="android.permission.BLUETOOTH"/>
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+    <uses-permission android:name="android.car.permission.CAR_CONTROL_AUDIO_VOLUME"/>
+    <uses-permission android:name="android.car.permission.CAR_CONTROL_AUDIO_SETTINGS"/>
+    <uses-permission android:name="android.car.permission.CAR_DRIVING_STATE"/>
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="android.platform.tests"
+        android:label="Android Automotive Notifications App Tests" />
+</manifest>
\ No newline at end of file
diff --git a/tests/automotive/functional/notifications/src/android/platform/tests/NotificationTest.java b/tests/automotive/functional/notifications/src/android/platform/tests/NotificationTest.java
new file mode 100644
index 0000000..d68eab2
--- /dev/null
+++ b/tests/automotive/functional/notifications/src/android/platform/tests/NotificationTest.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2021 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.tests;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import android.platform.helpers.AutoUtility;
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoNotificationMockingHelper;
+import android.platform.helpers.IAutoNotificationHelper;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.runner.RunWith;
+import org.junit.Test;
+
+@RunWith(AndroidJUnit4.class)
+public class NotificationTest {
+    private HelperAccessor<IAutoNotificationHelper> mNotificationHelper;
+    private HelperAccessor<IAutoNotificationMockingHelper> mNotificationMockingHelper;
+
+    private static String NOTIFICATION_TITLE = "AUTO TEST NOTIFICATION";
+
+    public NotificationTest() {
+        mNotificationHelper = new HelperAccessor<>(IAutoNotificationHelper.class);
+        mNotificationMockingHelper = new HelperAccessor<>(IAutoNotificationMockingHelper.class);
+    }
+
+    @BeforeClass
+    public static void exitSuw() {
+        AutoUtility.exitSuw();
+    }
+
+    @Before
+    public void clearAllNotification() {
+        mNotificationMockingHelper.get().clearAllNotification();
+    }
+
+    @After
+    public void exit() {
+        mNotificationHelper.get().exit();
+    }
+
+    @Test
+    public void testOpenCloseNotification() {
+        mNotificationHelper.get().open();
+        assertTrue("Notification did not open.", mNotificationHelper.get().isAppInForeground());
+        mNotificationHelper.get().exit();
+        assertFalse("Notification did not close.", mNotificationHelper.get().isAppInForeground());
+    }
+
+    @Test
+    public void testClearAllNotification() {
+        mNotificationMockingHelper.get().postNotifications(1);
+        mNotificationHelper.get().tapClearAllBtn();
+        mNotificationHelper.get().exit();
+        assertFalse(
+                "Notifications were not cleared.",
+                mNotificationHelper.get().checkNotificationExists(NOTIFICATION_TITLE));
+    }
+
+    @Test
+    public void testPostNotification() {
+        mNotificationMockingHelper.get().postNotifications(1);
+        assertTrue(
+                "Unable to find posted notification.",
+                mNotificationHelper.get().checkNotificationExists(NOTIFICATION_TITLE));
+    }
+
+    @Test
+    public void testSwipeAwayNotification() {
+        mNotificationMockingHelper.get().postNotifications(1);
+        assertTrue(
+                "Unable to find posted notification.",
+                mNotificationHelper.get().checkNotificationExists(NOTIFICATION_TITLE));
+        mNotificationHelper.get().removeNotification(NOTIFICATION_TITLE);
+        assertFalse(
+                "Notifications were not cleared.",
+                mNotificationHelper.get().checkNotificationExists(NOTIFICATION_TITLE));
+    }
+
+    @Test
+    public void testSwipeDownNotification() {
+        mNotificationHelper.get().openNotification();
+        assertTrue("Notification is not open", mNotificationHelper.get().isAppInForeground());
+    }
+}
diff --git a/tests/automotive/functional/settings/Android.bp b/tests/automotive/functional/settings/Android.bp
new file mode 100644
index 0000000..6f575c5
--- /dev/null
+++ b/tests/automotive/functional/settings/Android.bp
@@ -0,0 +1,34 @@
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "AndroidAutomotiveSettingsTests",
+    min_sdk_version: "24",
+    static_libs: [
+        "androidx.test.runner",
+        "androidx.test.rules",
+        "automotive-settings-app-helper",
+        "automotive-utility-helper",
+        "app-helpers-auto-interfaces",
+        "ub-uiautomator",
+        "hamcrest-library",
+        "platform-test-options",
+    ],
+    srcs: ["src/**/*.java"],
+    test_suites: ["catbox"],
+}
diff --git a/tests/automotive/functional/settings/AndroidManifest.xml b/tests/automotive/functional/settings/AndroidManifest.xml
new file mode 100644
index 0000000..8fcb1e0
--- /dev/null
+++ b/tests/automotive/functional/settings/AndroidManifest.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2021 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="android.platform.tests">
+    <uses-sdk android:minSdkVersion="23"
+              android:targetSdkVersion="24" />
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+    <uses-permission android:name="android.permission.BLUETOOTH"/>
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+    <uses-permission android:name="android.car.permission.CAR_CONTROL_AUDIO_VOLUME"/>
+    <uses-permission android:name="android.car.permission.CAR_CONTROL_AUDIO_SETTINGS"/>
+    <uses-permission android:name="android.car.permission.CAR_DRIVING_STATE"/>
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="android.platform.tests"
+        android:label="Android Automotive Settings App Tests" />
+</manifest>
\ No newline at end of file
diff --git a/tests/automotive/functional/settings/src/android/platform/tests/AccountSettingTest.java b/tests/automotive/functional/settings/src/android/platform/tests/AccountSettingTest.java
new file mode 100644
index 0000000..ed0608b
--- /dev/null
+++ b/tests/automotive/functional/settings/src/android/platform/tests/AccountSettingTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2021 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.tests;
+
+import android.platform.helpers.AutoConfigConstants;
+import android.platform.helpers.AutoUtility;
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoAccountsHelper;
+import android.platform.helpers.IAutoSettingHelper;
+import android.platform.test.option.StringOption;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.BeforeClass;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+@RunWith(AndroidJUnit4.class)
+public class AccountSettingTest {
+    private static final String ACCOUNT_EMAIL = "account-email";
+    private static final String ACCOUNT_PASSWORD = "account-password";
+
+    @ClassRule
+    public static StringOption mAccountEmail = new StringOption(ACCOUNT_EMAIL).setRequired(true);
+
+    @ClassRule
+    public static StringOption mAccountPassword =
+            new StringOption(ACCOUNT_PASSWORD).setRequired(true);
+
+    private HelperAccessor<IAutoAccountsHelper> mAccountsHelper;
+    private HelperAccessor<IAutoSettingHelper> mSettingHelper;
+
+    public AccountSettingTest() {
+        mAccountsHelper = new HelperAccessor<>(IAutoAccountsHelper.class);
+        mSettingHelper = new HelperAccessor<>(IAutoSettingHelper.class);
+    }
+
+    @BeforeClass
+    public static void exitSuw() {
+        AutoUtility.exitSuw();
+    }
+
+    @Before
+    public void openAccountsFacet() {
+        mSettingHelper.get().openSetting(AutoConfigConstants.PROFILE_ACCOUNT_SETTINGS);
+    }
+
+    @After
+    public void goBackToHomeScreen() {
+        mSettingHelper.get().goBackToSettingsScreen();
+    }
+
+    @Test
+    public void testAddRemoveAccount() {
+        mAccountsHelper.get().addAccount(mAccountEmail.get(), mAccountPassword.get());
+        mSettingHelper.get().openSetting(AutoConfigConstants.PROFILE_ACCOUNT_SETTINGS);
+        assertTrue(mAccountsHelper.get().doesEmailExist(mAccountEmail.get()));
+        mAccountsHelper.get().removeAccount(mAccountEmail.get());
+        assertFalse(mAccountsHelper.get().doesEmailExist(mAccountEmail.get()));
+    }
+}
diff --git a/tests/automotive/functional/settings/src/android/platform/tests/AppInfoSettingTest.java b/tests/automotive/functional/settings/src/android/platform/tests/AppInfoSettingTest.java
new file mode 100644
index 0000000..e37ac9c
--- /dev/null
+++ b/tests/automotive/functional/settings/src/android/platform/tests/AppInfoSettingTest.java
@@ -0,0 +1,75 @@
+package android.platform.tests;
+
+import static junit.framework.Assert.assertTrue;
+
+import android.platform.helpers.AutoConfigConstants;
+import android.platform.helpers.AutoConfigConstants;
+import android.platform.helpers.AutoUtility;
+import android.platform.helpers.IAutoAppInfoSettingsHelper;
+import android.platform.helpers.IAutoAppInfoSettingsHelper.State;
+import android.platform.helpers.IAutoSettingHelper;
+import android.platform.helpers.HelperAccessor;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class AppInfoSettingTest {
+    private HelperAccessor<IAutoAppInfoSettingsHelper> mAppInfoSettingsHelper;
+    private HelperAccessor<IAutoSettingHelper> mSettingHelper;
+
+    private static final String CONTACTS_APP = "Contacts";
+    private static final String PHONE_PERMISSION = "Phone";
+    private static final String CONTACT_PACKAGE = "com.android.contacts";
+
+    public AppInfoSettingTest() throws Exception {
+        mAppInfoSettingsHelper = new HelperAccessor<>(IAutoAppInfoSettingsHelper.class);
+        mSettingHelper = new HelperAccessor<>(IAutoSettingHelper.class);
+    }
+
+    @BeforeClass
+    public static void exitSuw() {
+        AutoUtility.exitSuw();
+    }
+
+    @Before
+    public void openAppInfoFacet() {
+        mSettingHelper.get().openSetting(AutoConfigConstants.APPS_SETTINGS);
+        mAppInfoSettingsHelper.get().showAllApps();
+    }
+
+    @After
+    public void goBackToSettingsScreen() {
+        mSettingHelper.get().goBackToSettingsScreen();
+    }
+
+    @Test
+    public void testDisableEnableApplication() {
+        mAppInfoSettingsHelper.get().selectApp(CONTACTS_APP);
+        mAppInfoSettingsHelper.get().enableDisableApplication(State.DISABLE);
+        assertTrue(
+                "Application is not disabled",
+                mAppInfoSettingsHelper.get().isApplicationDisabled(CONTACT_PACKAGE));
+        mAppInfoSettingsHelper.get().enableDisableApplication(State.ENABLE);
+        assertTrue(
+                "Application is not enabled",
+                !mAppInfoSettingsHelper.get().isApplicationDisabled(CONTACT_PACKAGE));
+    }
+
+    @Test
+    public void testApplicationPermissions() {
+        mAppInfoSettingsHelper.get().selectApp(CONTACTS_APP);
+        mAppInfoSettingsHelper.get().setAppPermission(PHONE_PERMISSION, State.DISABLE);
+        assertTrue(
+                "Permission is not disabled",
+                !mAppInfoSettingsHelper.get().getCurrentPermissions().contains(PHONE_PERMISSION));
+        mAppInfoSettingsHelper.get().setAppPermission(PHONE_PERMISSION, State.ENABLE);
+        assertTrue(
+                "Permission is disabled",
+                mAppInfoSettingsHelper.get().getCurrentPermissions().contains(PHONE_PERMISSION));
+    }
+}
diff --git a/tests/automotive/functional/settings/src/android/platform/tests/DateTimeSettingTest.java b/tests/automotive/functional/settings/src/android/platform/tests/DateTimeSettingTest.java
new file mode 100644
index 0000000..c4e5ec7
--- /dev/null
+++ b/tests/automotive/functional/settings/src/android/platform/tests/DateTimeSettingTest.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2021 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.tests;
+
+import static junit.framework.Assert.assertTrue;
+
+import android.platform.helpers.AutoConfigConstants;
+import android.platform.helpers.AutoUtility;
+import android.platform.helpers.IAutoDateTimeSettingsHelper;
+import android.platform.helpers.IAutoSettingHelper;
+import android.platform.helpers.HelperAccessor;
+import androidx.test.runner.AndroidJUnit4;
+
+import java.time.LocalDate;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class DateTimeSettingTest {
+    private HelperAccessor<IAutoDateTimeSettingsHelper> mDateTimeSettingsHelper;
+    private HelperAccessor<IAutoSettingHelper> mSettingHelper;
+
+    private static final boolean IS_AM = true;
+    private static final int DEFAULT_WAIT_TIME = 5000;
+    private static final int HOUR = 9;
+    private static final int MINUTE = 23;
+    private static final int YEAR = 2020;
+    private static final int MONTH = 3;
+    private static final int DAY = 9;
+    private static final LocalDate DATE = LocalDate.of(YEAR, MONTH, DAY);
+
+    private static final String FULL_TIME_TWELVE = "9:23 AM";
+    private static final String FULL_TIME_TWENTYFOUR = "09:23";
+    private static final String TIME_ZONE = "Costa Rica";
+    private static final String TIME_ZONE_FULL = "GMT-06:00 Central Standard Time";
+
+    public DateTimeSettingTest() throws Exception {
+        mDateTimeSettingsHelper = new HelperAccessor<>(IAutoDateTimeSettingsHelper.class);
+        mSettingHelper = new HelperAccessor<>(IAutoSettingHelper.class);
+    }
+
+    @BeforeClass
+    public static void exitSuw() {
+        AutoUtility.exitSuw();
+    }
+
+    @Before
+    public void openDateTimeFacet() {
+        mSettingHelper.get().openSetting(AutoConfigConstants.DATE_AND_TIME_SETTINGS);
+    }
+
+    @After
+    public void goBackToSettingsScreen() {
+        mSettingHelper.get().goBackToSettingsScreen();
+    }
+
+    @Test
+    public void testSetDate() {
+        mDateTimeSettingsHelper.get().setDate(DATE);
+        assertTrue(mDateTimeSettingsHelper.get().getDate().equals(DATE));
+    }
+
+    @Test
+    public void testSetTimeTwelveHourFormat() {
+        mDateTimeSettingsHelper.get().setTimeInTwelveHourFormat(HOUR, MINUTE, IS_AM);
+        assertTrue(mDateTimeSettingsHelper.get().getTime().equals(FULL_TIME_TWELVE));
+    }
+
+    @Test
+    public void testSetTimeTwentyFourHourFormat() {
+        mDateTimeSettingsHelper.get().setTimeInTwentyFourHourFormat(HOUR, MINUTE);
+        assertTrue(mDateTimeSettingsHelper.get().getTime().equals(FULL_TIME_TWENTYFOUR));
+    }
+
+    @Test
+    public void testSetTimeZone() {
+        mDateTimeSettingsHelper.get().setTimeZone(TIME_ZONE);
+        assertTrue(mDateTimeSettingsHelper.get().getTimeZone().equals(TIME_ZONE_FULL));
+    }
+
+    @Test
+    public void testSetTwentyFourHourFormat() {
+        if (!mDateTimeSettingsHelper.get().isTwentyFourHourFormatEnabled()) {
+            mDateTimeSettingsHelper.get().toggleTwentyFourHourFormatSwitch();
+        }
+        String currentUiTime = mDateTimeSettingsHelper.get().getTime();
+        assertTrue(currentUiTime.indexOf("AM") == -1 && currentUiTime.indexOf("PM") == -1);
+    }
+
+    @Test
+    public void testSetTwelveHourFormat() {
+        if (mDateTimeSettingsHelper.get().isTwentyFourHourFormatEnabled()) {
+            mDateTimeSettingsHelper.get().toggleTwentyFourHourFormatSwitch();
+        }
+        String currentUiTime = mDateTimeSettingsHelper.get().getTime();
+        assertTrue(currentUiTime.indexOf("AM") != -1 || currentUiTime.indexOf("PM") != -1);
+    }
+}
diff --git a/tests/automotive/functional/settings/src/android/platform/tests/SecuritySettingTest.java b/tests/automotive/functional/settings/src/android/platform/tests/SecuritySettingTest.java
new file mode 100644
index 0000000..3f09e7a
--- /dev/null
+++ b/tests/automotive/functional/settings/src/android/platform/tests/SecuritySettingTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2021 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.tests;
+
+import static junit.framework.Assert.assertTrue;
+
+import android.platform.helpers.AutoConfigConstants;
+import android.platform.helpers.AutoUtility;
+import android.platform.helpers.IAutoSecuritySettingsHelper;
+import android.platform.helpers.IAutoSettingHelper;
+import android.platform.helpers.HelperAccessor;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class SecuritySettingTest {
+    private static final String PASSWORD = "test4fun";
+    private static final String PIN = "1013";
+
+    private HelperAccessor<IAutoSecuritySettingsHelper> mSecuritySettingsHelper;
+    private HelperAccessor<IAutoSettingHelper> mSettingHelper;
+
+    public SecuritySettingTest() throws Exception {
+        mSecuritySettingsHelper = new HelperAccessor<>(IAutoSecuritySettingsHelper.class);
+        mSettingHelper = new HelperAccessor<>(IAutoSettingHelper.class);
+    }
+
+    @BeforeClass
+    public static void exitSuw() {
+        AutoUtility.exitSuw();
+    }
+
+    @Before
+    public void openSecuritySettingFacet() {
+        mSettingHelper.get().openSetting(AutoConfigConstants.SECURITY_SETTINGS);
+    }
+
+    @After
+    public void goBackToSettingsScreen() {
+        mSettingHelper.get().goBackToSettingsScreen();
+    }
+
+    @Test
+    public void testLockUnlockDeviceByPassword() {
+        mSecuritySettingsHelper.get().setLockByPassword(PASSWORD);
+        assertTrue("Password has not been set", mSecuritySettingsHelper.get().isDeviceLocked());
+        mSecuritySettingsHelper.get().unlockByPassword(PASSWORD);
+        mSecuritySettingsHelper.get().removeLock();
+        assertTrue(
+                "Password has not been removed", !mSecuritySettingsHelper.get().isDeviceLocked());
+    }
+
+    @Test
+    public void testLockUnlockDeviceByPIN() {
+        mSecuritySettingsHelper.get().setLockByPin(PIN);
+        assertTrue("PIN has not been set", mSecuritySettingsHelper.get().isDeviceLocked());
+        mSecuritySettingsHelper.get().unlockByPin(PIN);
+        mSecuritySettingsHelper.get().removeLock();
+        assertTrue("PIN has not been removed", !mSecuritySettingsHelper.get().isDeviceLocked());
+    }
+}
diff --git a/tests/automotive/functional/settings/src/android/platform/tests/SettingSearchTest.java b/tests/automotive/functional/settings/src/android/platform/tests/SettingSearchTest.java
new file mode 100644
index 0000000..8b01081
--- /dev/null
+++ b/tests/automotive/functional/settings/src/android/platform/tests/SettingSearchTest.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2021 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.tests;
+
+import static junit.framework.Assert.assertTrue;
+
+import android.platform.helpers.AutoUtility;
+import android.platform.helpers.IAutoSettingHelper;
+import android.platform.helpers.HelperAccessor;
+import android.platform.test.option.StringOption;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class SettingSearchTest {
+    private static final String SEARCH_APP = "search-app";
+    private static final String SEARCH_SETTING = "search-setting";
+
+    @ClassRule
+    public static StringOption mSearchApp = new StringOption(SEARCH_APP).setRequired(false);
+
+    @ClassRule
+    public static StringOption mSearchSetting = new StringOption(SEARCH_SETTING).setRequired(false);
+
+    private static final String SEARCH_DEFAULT_APP = "Contacts";
+    private static final String SEARCH_DEFAULT_SETTING = "Date & time";
+
+    private HelperAccessor<IAutoSettingHelper> mSettingHelper;
+
+    public SettingSearchTest() throws Exception {
+        mSettingHelper = new HelperAccessor<>(IAutoSettingHelper.class);
+    }
+
+    @BeforeClass
+    public static void exitSuw() {
+        AutoUtility.exitSuw();
+    }
+
+    @Before
+    public void openSetting() {
+        mSettingHelper.get().openFullSettings();
+    }
+
+    @After
+    public void exitSettings() {
+        mSettingHelper.get().exit();
+    }
+
+    @Test
+    public void testSearchApplication() {
+        String searchApp = SEARCH_DEFAULT_APP;
+        if (mSearchApp != null && mSearchApp.get() != null && !mSearchApp.get().isEmpty()) {
+            searchApp = mSearchApp.get();
+        }
+        mSettingHelper.get().searchAndSelect(searchApp);
+        assertTrue(
+                "Page title does not contains searched app name",
+                mSettingHelper.get().isValidPageTitle(searchApp));
+    }
+
+    @Test
+    public void testSearchSetting() {
+        String searchSetting = SEARCH_DEFAULT_SETTING;
+        if (mSearchSetting != null
+                && mSearchSetting.get() != null
+                && !mSearchSetting.get().isEmpty()) {
+            searchSetting = mSearchSetting.get();
+        }
+        mSettingHelper.get().searchAndSelect(searchSetting);
+        assertTrue(
+                "Page title does not contains searched setting name",
+                mSettingHelper.get().isValidPageTitle(searchSetting));
+    }
+}
diff --git a/tests/automotive/functional/settings/src/android/platform/tests/SettingTest.java b/tests/automotive/functional/settings/src/android/platform/tests/SettingTest.java
new file mode 100644
index 0000000..8602f8f
--- /dev/null
+++ b/tests/automotive/functional/settings/src/android/platform/tests/SettingTest.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2021 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.tests;
+
+import static junit.framework.Assert.assertFalse;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertThat;
+
+import android.app.UiModeManager;
+import android.platform.helpers.AutoConfigConstants;
+import android.platform.helpers.AutoUtility;
+import android.platform.helpers.IAutoAppInfoSettingsHelper;
+import android.platform.helpers.IAutoSettingHelper;
+import android.platform.helpers.IAutoSettingHelper.DayNightMode;
+import android.platform.helpers.HelperAccessor;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class SettingTest {
+    private static final int DAY_MODE_VALUE = 0;
+    private HelperAccessor<IAutoAppInfoSettingsHelper> mAppInfoSettingsHelper;
+    private HelperAccessor<IAutoSettingHelper> mSettingHelper;
+
+    public SettingTest() throws Exception {
+        mAppInfoSettingsHelper = new HelperAccessor<>(IAutoAppInfoSettingsHelper.class);
+        mSettingHelper = new HelperAccessor<>(IAutoSettingHelper.class);
+    }
+
+    @BeforeClass
+    public static void exitSuw() {
+        AutoUtility.exitSuw();
+    }
+
+    @After
+    public void goBackToSettingsScreen() {
+        mSettingHelper.get().goBackToSettingsScreen();
+    }
+
+    @Test
+    public void testDisplaySettings() {
+        mSettingHelper.get().openSetting(AutoConfigConstants.DISPLAY_SETTINGS);
+    }
+
+    @Test
+    public void testSoundSettings() {
+        mSettingHelper.get().openSetting(AutoConfigConstants.SOUND_SETTINGS);
+    }
+
+    @Test
+    public void testAppinfoSettings() {
+        mSettingHelper.get().openSetting(AutoConfigConstants.APPS_SETTINGS);
+        mAppInfoSettingsHelper.get().showAllApps();
+    }
+
+    @Test
+    public void testAccountsSettings() {
+        mSettingHelper.get().openSetting(AutoConfigConstants.PROFILE_ACCOUNT_SETTINGS);
+    }
+
+    @Test
+    public void testSystemSettings() {
+        mSettingHelper.get().openSetting(AutoConfigConstants.SYSTEM_SETTINGS);
+    }
+
+    @Test
+    public void testBluetoothSettings() {
+        mSettingHelper.get().openSetting(AutoConfigConstants.BLUETOOTH_SETTINGS);
+        mSettingHelper.get().turnOnOffBluetooth(false);
+        assertFalse(mSettingHelper.get().isBluetoothOn());
+        mSettingHelper.get().turnOnOffBluetooth(true);
+    }
+
+    @Test
+    public void testDayMode() {
+        mSettingHelper.get().openQuickSettings();
+        mSettingHelper.get().setDayNightMode(DayNightMode.DAY_MODE);
+        assertThat(mSettingHelper.get().getDayNightModeStatus().getValue(), is(DAY_MODE_VALUE));
+    }
+
+    @Test
+    public void testNightMode() {
+        mSettingHelper.get().openQuickSettings();
+        mSettingHelper.get().setDayNightMode(DayNightMode.NIGHT_MODE);
+        assertThat(
+                mSettingHelper.get().getDayNightModeStatus().getValue(),
+                is(UiModeManager.MODE_NIGHT_YES));
+    }
+}
diff --git a/tests/automotive/functional/settings/src/android/platform/tests/SoundSettingTest.java b/tests/automotive/functional/settings/src/android/platform/tests/SoundSettingTest.java
new file mode 100644
index 0000000..f575ec6
--- /dev/null
+++ b/tests/automotive/functional/settings/src/android/platform/tests/SoundSettingTest.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2021 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.tests;
+
+import static junit.framework.Assert.assertTrue;
+
+import android.platform.helpers.AutoConfigConstants;
+import android.platform.helpers.AutoUtility;
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoSettingHelper;
+import android.platform.helpers.IAutoSoundsSettingHelper;
+import android.platform.helpers.IAutoSoundsSettingHelper.SoundType;
+import android.platform.helpers.IAutoSoundsSettingHelper.VolumeType;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class SoundSettingTest {
+    private static final String RINGTONE = "Luna";
+    private static final String NOTIFICATION_SOUND = "Selenium";
+    private static final String ALARM_SOUND = "Platinum";
+    private static final int INDEX = 20;
+
+    private HelperAccessor<IAutoSoundsSettingHelper> mSoundsSettingHelper;
+    private HelperAccessor<IAutoSettingHelper> mSettingHelper;
+
+    public SoundSettingTest() throws Exception {
+        mSoundsSettingHelper = new HelperAccessor<>(IAutoSoundsSettingHelper.class);
+        mSettingHelper = new HelperAccessor<>(IAutoSettingHelper.class);
+    }
+
+    @BeforeClass
+    public static void exitSuw() {
+        AutoUtility.exitSuw();
+    }
+
+    @Before
+    public void openSoundsSettingFacet() {
+        mSettingHelper.get().openSetting(AutoConfigConstants.SOUND_SETTINGS);
+    }
+
+    @After
+    public void goBackToSettingsScreen() {
+        mSettingHelper.get().goBackToSettingsScreen();
+    }
+
+    @Test
+    public void testChangeMediaVolume() {
+        mSoundsSettingHelper.get().setVolume(VolumeType.MEDIA, INDEX);
+        int volume = mSoundsSettingHelper.get().getVolume(VolumeType.MEDIA);
+        assertTrue("Volume was not set", volume == INDEX);
+    }
+
+    @Test
+    public void testChangeAlarmVolume() {
+        mSoundsSettingHelper.get().setVolume(VolumeType.ALARM, INDEX);
+        int volume = mSoundsSettingHelper.get().getVolume(VolumeType.ALARM);
+        assertTrue("Volume was not set", volume == INDEX);
+    }
+
+    @Test
+    public void testChangeNavigationVolume() {
+        mSoundsSettingHelper.get().setVolume(VolumeType.NAVIGATION, INDEX);
+        int volume = mSoundsSettingHelper.get().getVolume(VolumeType.NAVIGATION);
+        assertTrue("Volume was not set", volume == INDEX);
+    }
+
+    @Test
+    public void testChangeInCallVolume() {
+        mSoundsSettingHelper.get().setVolume(VolumeType.INCALL, INDEX);
+        int volume = mSoundsSettingHelper.get().getVolume(VolumeType.INCALL);
+        assertTrue("Volume was not set", volume == INDEX);
+    }
+
+    @Test
+    public void testChangeRingtone() {
+        mSoundsSettingHelper.get().setSound(SoundType.RINGTONE, RINGTONE);
+        String sound = mSoundsSettingHelper.get().getSound(SoundType.RINGTONE);
+        assertTrue("Sound was not changed", sound.equals(RINGTONE));
+    }
+
+    @Test
+    public void testChangeNotificationSound() {
+        mSoundsSettingHelper.get().setSound(SoundType.NOTIFICATION, NOTIFICATION_SOUND);
+        String sound = mSoundsSettingHelper.get().getSound(SoundType.NOTIFICATION);
+        assertTrue("Sound was not changed", sound.equals(NOTIFICATION_SOUND));
+    }
+
+    @Test
+    public void testChangeAlarmSound() {
+        mSoundsSettingHelper.get().setSound(SoundType.ALARM, ALARM_SOUND);
+        String sound = mSoundsSettingHelper.get().getSound(SoundType.ALARM);
+        assertTrue("Sound was not changed", sound.equals(ALARM_SOUND));
+    }
+}
diff --git a/tests/automotive/functional/settings/src/android/platform/tests/SystemSettingTest.java b/tests/automotive/functional/settings/src/android/platform/tests/SystemSettingTest.java
new file mode 100644
index 0000000..d7ec2d0
--- /dev/null
+++ b/tests/automotive/functional/settings/src/android/platform/tests/SystemSettingTest.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2021 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.tests;
+
+import static junit.framework.Assert.assertTrue;
+
+import android.platform.helpers.AutoConfigConstants;
+import android.platform.helpers.AutoUtility;
+import android.platform.helpers.IAutoSystemSettingsHelper;
+import android.platform.helpers.IAutoSettingHelper;
+import android.platform.helpers.HelperAccessor;
+import androidx.test.runner.AndroidJUnit4;
+
+import java.util.Date;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class SystemSettingTest {
+    private HelperAccessor<IAutoSettingHelper> mSettingHelper;
+    private HelperAccessor<IAutoSystemSettingsHelper> mSystemSettingsHelper;
+
+    public SystemSettingTest() throws Exception {
+        mSettingHelper = new HelperAccessor<>(IAutoSettingHelper.class);
+        mSystemSettingsHelper = new HelperAccessor<>(IAutoSystemSettingsHelper.class);
+    }
+
+    @BeforeClass
+    public static void exitSuw() {
+        AutoUtility.exitSuw();
+    }
+
+    @Before
+    public void openSystemFacet() {
+        mSettingHelper.get().openSetting(AutoConfigConstants.SYSTEM_SETTINGS);
+    }
+
+    @After
+    public void goBackToSettingsScreen() {
+        mSettingHelper.get().goBackToSettingsScreen();
+    }
+
+    @Test
+    public void testDeviceModel() {
+        String model = android.os.Build.MODEL;
+        assertTrue(
+                "Model from API and Model from UI are not the same",
+                mSystemSettingsHelper.get().getDeviceModel().endsWith(model));
+    }
+
+    @Test
+    public void testAndroidVersion() {
+        String androidVersion = android.os.Build.VERSION.RELEASE;
+        assertTrue(
+                "Android Version from API and Android Version from UI are not the same",
+                mSystemSettingsHelper.get().getAndroidVersion().endsWith(androidVersion));
+    }
+
+    @Test
+    public void testAndroidSecurityPatchLevel() {
+        String day = android.os.Build.VERSION.SECURITY_PATCH;
+        String[] arr = day.split("-");
+        Date date =
+                new Date(
+                        Integer.valueOf(arr[0]),
+                        Integer.valueOf(arr[1]) - 1,
+                        Integer.valueOf(arr[2]));
+        assertTrue(
+                "security patch from API and security patch from UI are not the same",
+                date.equals(mSystemSettingsHelper.get().getAndroidSecurityPatchLevel()));
+    }
+
+    @Test
+    public void testKernelVersion() {
+        String kernelVersion = System.getProperty("os.version");
+        assertTrue(
+                "kernel version from API and kernel from UI are not the same",
+                mSystemSettingsHelper.get().getKernelVersion().startsWith(kernelVersion));
+    }
+
+    @Test
+    public void testBuildNumber() {
+        String buildNumber = android.os.Build.DISPLAY;
+        assertTrue(
+                "Build number from API and Build number from UI are not the same",
+                buildNumber.equals(mSystemSettingsHelper.get().getBuildNumber()));
+    }
+}
diff --git a/tests/automotive/functional/settings/src/android/platform/tests/WifiSettingTest.java b/tests/automotive/functional/settings/src/android/platform/tests/WifiSettingTest.java
new file mode 100644
index 0000000..21d7e69
--- /dev/null
+++ b/tests/automotive/functional/settings/src/android/platform/tests/WifiSettingTest.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2021 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.tests;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import android.platform.helpers.AutoConfigConstants;
+import android.platform.helpers.AutoUtility;
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoSettingHelper;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class WifiSettingTest {
+    private HelperAccessor<IAutoSettingHelper> mSettingHelper;
+
+    public WifiSettingTest() throws Exception {
+        mSettingHelper = new HelperAccessor<>(IAutoSettingHelper.class);
+    }
+
+    @BeforeClass
+    public static void exitSuw() {
+        AutoUtility.exitSuw();
+    }
+
+    @Before
+    public void openNetworkSetting() {
+        mSettingHelper.get().openSetting(AutoConfigConstants.NETWORK_AND_INTERNET_SETTINGS);
+    }
+
+    @After
+    public void goBackToSettingsScreen() {
+        mSettingHelper.get().goBackToSettingsScreen();
+    }
+
+    @Test
+    public void testWifiSettings() {
+        mSettingHelper.get().turnOnOffWifi(false);
+        assertFalse(mSettingHelper.get().isWifiOn());
+        mSettingHelper.get().turnOnOffWifi(true);
+        assertTrue(mSettingHelper.get().isWifiOn());
+    }
+
+    @Test
+    public void testTurnOnOffHotspot() {
+        mSettingHelper.get().turnOnOffHotspot(true);
+        assertTrue(mSettingHelper.get().isHotspotOn());
+        mSettingHelper.get().turnOnOffHotspot(false);
+    }
+}
diff --git a/tests/automotive/functional/uxrestriction/Android.bp b/tests/automotive/functional/uxrestriction/Android.bp
new file mode 100644
index 0000000..078c45d
--- /dev/null
+++ b/tests/automotive/functional/uxrestriction/Android.bp
@@ -0,0 +1,36 @@
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "AndroidAutomotiveUxRestrictionTests",
+    min_sdk_version: "24",
+    static_libs: [
+        "androidx.test.runner",
+        "androidx.test.rules",
+        "automotive-settings-app-helper",
+        "automotive-utility-helper",
+        "app-helpers-auto-interfaces",
+        "ub-uiautomator",
+        "hamcrest-library",
+        "platform-test-options",
+        "automotive-vehiclehardkeys-app-helper",
+        "automotive-app-grid-helper",
+    ],
+    srcs: ["src/**/*.java"],
+    test_suites: ["catbox"],
+}
diff --git a/tests/automotive/functional/uxrestriction/AndroidManifest.xml b/tests/automotive/functional/uxrestriction/AndroidManifest.xml
new file mode 100644
index 0000000..cd2aa09
--- /dev/null
+++ b/tests/automotive/functional/uxrestriction/AndroidManifest.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2021 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="android.platform.tests">
+  <uses-sdk android:minSdkVersion="23"
+      android:targetSdkVersion="24" />
+  <application>
+    <uses-library android:name="android.test.runner" />
+  </application>
+  <uses-permission android:name="android.permission.BLUETOOTH"/>
+  <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+  <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+  <uses-permission android:name="android.car.permission.CAR_CONTROL_AUDIO_VOLUME"/>
+  <uses-permission android:name="android.car.permission.CAR_CONTROL_AUDIO_SETTINGS"/>
+  <uses-permission android:name="android.car.permission.CAR_DRIVING_STATE"/>
+  <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+  <instrumentation
+      android:name="androidx.test.runner.AndroidJUnitRunner"
+      android:targetPackage="android.platform.tests"
+      android:label="Android Automotive Driver Restriction Tests" />
+</manifest>
\ No newline at end of file
diff --git a/tests/automotive/functional/uxrestriction/src/android/platform/tests/UxRestrictionTest.java b/tests/automotive/functional/uxrestriction/src/android/platform/tests/UxRestrictionTest.java
new file mode 100644
index 0000000..433f58b
--- /dev/null
+++ b/tests/automotive/functional/uxrestriction/src/android/platform/tests/UxRestrictionTest.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2021 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.tests;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import android.platform.helpers.AutoConfigConstants;
+import android.platform.helpers.AutoUtility;
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoAppGridHelper;
+import android.platform.helpers.IAutoSettingHelper;
+import android.platform.helpers.IAutoVehicleHardKeysHelper;
+import android.platform.helpers.IAutoVehicleHardKeysHelper.DrivingState;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class UxRestrictionTest {
+    private HelperAccessor<IAutoSettingHelper> mSettingHelper;
+    private HelperAccessor<IAutoAppGridHelper> mAppGridHelper;
+    private HelperAccessor<IAutoVehicleHardKeysHelper> mHardKeysHelper;
+
+    private static final int SPEED_TWENTY = 20;
+    private static final int SPEED_ZERO = 0;
+
+    public UxRestrictionTest() throws Exception {
+        mSettingHelper = new HelperAccessor<>(IAutoSettingHelper.class);
+        mAppGridHelper = new HelperAccessor<>(IAutoAppGridHelper.class);
+        mHardKeysHelper = new HelperAccessor<>(IAutoVehicleHardKeysHelper.class);
+    }
+
+    @BeforeClass
+    public static void exitSuw() {
+        AutoUtility.exitSuw();
+    }
+
+    @Before
+    public void enableDrivingMode() {
+        mHardKeysHelper.get().setDrivingState(DrivingState.MOVING);
+        mHardKeysHelper.get().setSpeed(SPEED_TWENTY);
+    }
+
+    @After
+    public void disableDrivingMode() {
+        mSettingHelper.get().goBackToSettingsScreen();
+        mHardKeysHelper.get().setSpeed(SPEED_ZERO);
+        mHardKeysHelper.get().setDrivingState(DrivingState.PARKED);
+    }
+
+    @Test
+    public void testRestrictedSoundSettings() {
+        mSettingHelper.get().openSetting(AutoConfigConstants.SOUND_SETTINGS);
+        String currentTitle = mSettingHelper.get().getPageTitleText();
+        mSettingHelper.get().openMenuWith("Phone ringtone");
+        String newTitle = mSettingHelper.get().getPageTitleText();
+        assertTrue("Phong ringtone setting is not disabled", currentTitle.equals(newTitle));
+    }
+
+    @Test
+    public void testRestrictedNetworkSettings() {
+        mSettingHelper.get().openSetting(AutoConfigConstants.NETWORK_AND_INTERNET_SETTINGS);
+        mSettingHelper.get().turnOnOffHotspot(true);
+        assertFalse("Hotspot is not disabled", mSettingHelper.get().isHotspotOn());
+    }
+
+    @Test
+    public void testRestrictedBluetoothSettings() {
+        mSettingHelper.get().openSetting(AutoConfigConstants.BLUETOOTH_SETTINGS);
+        String currentTitle = mSettingHelper.get().getPageTitleText();
+        mSettingHelper.get().openMenuWith("Pair new device");
+        String newTitle = mSettingHelper.get().getPageTitleText();
+        assertTrue("Bluetooth setting is not disabled", currentTitle.equals(newTitle));
+    }
+
+    @Test
+    public void testRestrictedAppSettings() {
+        mSettingHelper.get().openFullSettings();
+        String currentTitle = mSettingHelper.get().getPageTitleText();
+        mSettingHelper.get().findSettingMenuAndClick(AutoConfigConstants.APPS_SETTINGS);
+        String newTitle = mSettingHelper.get().getPageTitleText();
+        assertTrue("Apps & notification settings is not disabled", currentTitle.equals(newTitle));
+    }
+
+    @Test
+    public void testRestrictedProfilesAndAccountsSettings() {
+        mSettingHelper.get().openFullSettings();
+        String currentTitle = mSettingHelper.get().getPageTitleText();
+        mSettingHelper.get().findSettingMenuAndClick(AutoConfigConstants.PROFILE_ACCOUNT_SETTINGS);
+        String newTitle = mSettingHelper.get().getPageTitleText();
+        assertTrue("Profiles and accounts settings is not disabled", currentTitle.equals(newTitle));
+    }
+
+    @Test
+    public void testRestrictedSecuritySettings() {
+        mSettingHelper.get().openFullSettings();
+        String currentTitle = mSettingHelper.get().getPageTitleText();
+        mSettingHelper.get().findSettingMenuAndClick(AutoConfigConstants.SECURITY_SETTINGS);
+        String newTitle = mSettingHelper.get().getPageTitleText();
+        assertTrue("Security settings is not disabled", currentTitle.equals(newTitle));
+    }
+
+    @Test
+    public void testRestrictedSystemSettings() {
+        mSettingHelper.get().openFullSettings();
+        String currentTitle = mSettingHelper.get().getPageTitleText();
+        mSettingHelper.get().findSettingMenuAndClick(AutoConfigConstants.SYSTEM_SETTINGS);
+        String newTitle = mSettingHelper.get().getPageTitleText();
+        assertTrue("System settings is not disabled", currentTitle.equals(newTitle));
+    }
+
+    @Test
+    public void testRestrictedApp() {
+        mAppGridHelper.get().open();
+        mAppGridHelper.get().openApp("Play Store");
+        assertTrue("app is not restricted", mAppGridHelper.get().isAppInForeground());
+    }
+}
diff --git a/tests/automotive/health/appgrid/Android.bp b/tests/automotive/health/appgrid/Android.bp
new file mode 100644
index 0000000..f1b22d7
--- /dev/null
+++ b/tests/automotive/health/appgrid/Android.bp
@@ -0,0 +1,32 @@
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library_static {
+    name: "automotive-app-grid-scenarios",
+    min_sdk_version: "24",
+    srcs: [ "src/**/*.java" ],
+    libs: [
+        "androidx.test.runner",
+        "automotive-settings-app-helper",
+        "app-helpers-auto-interfaces",
+        "common-platform-scenarios",
+        "platform-test-options",
+        "platform-test-rules",
+        "ub-uiautomator",
+    ],
+}
diff --git a/tests/automotive/health/appgrid/src/android/platform/scenario/appgrid/Scroll.java b/tests/automotive/health/appgrid/src/android/platform/scenario/appgrid/Scroll.java
new file mode 100644
index 0000000..4f25dc0
--- /dev/null
+++ b/tests/automotive/health/appgrid/src/android/platform/scenario/appgrid/Scroll.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2021 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.scenario.appgrid;
+
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoAppGridHelper;
+import android.platform.test.scenario.annotation.Scenario;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Scroll down and up in App Grid. */
+@Scenario
+@RunWith(JUnit4.class)
+public class Scroll {
+    static HelperAccessor<IAutoAppGridHelper> sHelper =
+            new HelperAccessor<>(IAutoAppGridHelper.class);
+
+    @Test
+    public void testScrollDownAndUp() {
+        sHelper.get().scrollDownOnePage(500);
+        sHelper.get().scrollUpOnePage(500);
+    }
+}
diff --git a/tests/automotive/health/appgrid/tests/Android.bp b/tests/automotive/health/appgrid/tests/Android.bp
new file mode 100644
index 0000000..b801a72
--- /dev/null
+++ b/tests/automotive/health/appgrid/tests/Android.bp
@@ -0,0 +1,39 @@
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "AndroidAutomotiveAppGridScenarioTests",
+    min_sdk_version: "24",
+    static_libs: [
+        "androidx.test.runner",
+        "androidx.test.rules",
+        "automotive-settings-scenarios",
+        "automotive-app-grid-scenarios",
+        "automotive-health-test-rules",
+        "automotive-settings-app-helper",
+        "app-helpers-auto-interfaces",
+        "ub-uiautomator",
+        "collector-device-lib-platform",
+        "common-platform-scenarios",
+        "common-platform-scenario-tests",
+        "microbenchmark-device-lib",
+        "platform-test-options",
+    ],
+    srcs: ["src/**/*.java"],
+    test_suites: ["catbox"],
+}
diff --git a/tests/automotive/health/appgrid/tests/AndroidManifest.xml b/tests/automotive/health/appgrid/tests/AndroidManifest.xml
new file mode 100644
index 0000000..f116d77
--- /dev/null
+++ b/tests/automotive/health/appgrid/tests/AndroidManifest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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="android.platform.test.scenario.appgrid" >
+  <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.MANAGE_USERS" />
+  <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
+  <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
+
+  <application>
+    <uses-library android:name="android.test.runner"/>
+  </application>
+  <instrumentation
+      android:name="androidx.test.runner.AndroidJUnitRunner"
+      android:targetPackage="android.platform.test.scenario.appgrid"
+      android:label="Android Automotive App Grid Scenario-Based Tests" />
+</manifest>
\ No newline at end of file
diff --git a/tests/automotive/health/appgrid/tests/src/android/platform/scenario/appgrid/ScrollMicrobenchmark.java b/tests/automotive/health/appgrid/tests/src/android/platform/scenario/appgrid/ScrollMicrobenchmark.java
new file mode 100644
index 0000000..435d1de
--- /dev/null
+++ b/tests/automotive/health/appgrid/tests/src/android/platform/scenario/appgrid/ScrollMicrobenchmark.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2021 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.scenario.appgrid;
+
+import android.platform.test.microbenchmark.Microbenchmark;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.runner.RunWith;
+
+@RunWith(Microbenchmark.class)
+public class ScrollMicrobenchmark extends Scroll {
+    @BeforeClass
+    public static void openApp() {
+        sHelper.get().open();
+    }
+
+    @AfterClass
+    public static void closeApp() {
+        sHelper.get().exit();
+    }
+}
diff --git a/tests/automotive/health/autogeneric/Android.bp b/tests/automotive/health/autogeneric/Android.bp
new file mode 100644
index 0000000..84aa458
--- /dev/null
+++ b/tests/automotive/health/autogeneric/Android.bp
@@ -0,0 +1,32 @@
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library_static {
+    name: "automotive-generic-scenarios",
+    min_sdk_version: "24",
+    srcs: [ "src/**/*.java" ],
+    libs: [
+        "androidx.test.runner",
+        "automotive-settings-app-helper",
+        "app-helpers-auto-interfaces",
+        "common-platform-scenarios",
+        "platform-test-options",
+        "platform-test-rules",
+        "ub-uiautomator",
+    ],
+}
diff --git a/tests/automotive/health/autogeneric/src/android/platform/scenario/autogeneric/OpenApp.java b/tests/automotive/health/autogeneric/src/android/platform/scenario/autogeneric/OpenApp.java
new file mode 100644
index 0000000..d4c6187
--- /dev/null
+++ b/tests/automotive/health/autogeneric/src/android/platform/scenario/autogeneric/OpenApp.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2021 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.scenario.autogeneric;
+
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoGenericAppHelper;
+import android.platform.test.option.StringOption;
+import android.platform.test.scenario.annotation.Scenario;
+
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Open an app by specifying either launch activity or package. */
+@Scenario
+@RunWith(JUnit4.class)
+public class OpenApp {
+    @ClassRule
+    public static StringOption mPackageOption = new StringOption("pkg").setRequired(false);
+
+    @ClassRule
+    public static StringOption mLaunchActivityOption =
+            new StringOption("launch-activity").setRequired(false);
+
+    static HelperAccessor<IAutoGenericAppHelper> sHelper =
+            new HelperAccessor<>(IAutoGenericAppHelper.class);
+
+    public OpenApp() {
+        if (mLaunchActivityOption.get() != null) {
+            sHelper.get().setLaunchActivity(mLaunchActivityOption.get());
+        } else if (mPackageOption.get() != null) {
+            sHelper.get().setPackage(mPackageOption.get());
+        } else {
+            throw new IllegalStateException("Neither launch activity nor package is set.");
+        }
+    }
+
+    @Test
+    public void testOpen() {
+        sHelper.get().open();
+    }
+}
diff --git a/tests/automotive/health/autogeneric/tests/Android.bp b/tests/automotive/health/autogeneric/tests/Android.bp
new file mode 100644
index 0000000..0679eae
--- /dev/null
+++ b/tests/automotive/health/autogeneric/tests/Android.bp
@@ -0,0 +1,39 @@
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "AndroidAutomotiveGenericScenarioTests",
+    min_sdk_version: "24",
+    static_libs: [
+        "androidx.test.runner",
+        "androidx.test.rules",
+        "automotive-settings-scenarios",
+        "automotive-generic-scenarios",
+        "automotive-health-test-rules",
+        "automotive-settings-app-helper",
+        "app-helpers-auto-interfaces",
+        "ub-uiautomator",
+        "collector-device-lib-platform",
+        "common-platform-scenarios",
+        "common-platform-scenario-tests",
+        "microbenchmark-device-lib",
+        "platform-test-options",
+    ],
+    srcs: ["src/**/*.java"],
+    test_suites: ["catbox"],
+}
diff --git a/tests/automotive/health/autogeneric/tests/AndroidManifest.xml b/tests/automotive/health/autogeneric/tests/AndroidManifest.xml
new file mode 100644
index 0000000..a60b7b1
--- /dev/null
+++ b/tests/automotive/health/autogeneric/tests/AndroidManifest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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="android.platform.test.scenario.autogeneric" >
+  <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.MANAGE_USERS" />
+  <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
+  <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
+
+  <application>
+    <uses-library android:name="android.test.runner"/>
+  </application>
+  <instrumentation
+      android:name="androidx.test.runner.AndroidJUnitRunner"
+      android:targetPackage="android.platform.test.scenario.autogeneric"
+      android:label="Android Automotive Generic Scenario-Based Tests" />
+</manifest>
\ No newline at end of file
diff --git a/tests/automotive/health/autogeneric/tests/src/android/platform/scenario/autogeneric/OpenAppMicrobenchmark.java b/tests/automotive/health/autogeneric/tests/src/android/platform/scenario/autogeneric/OpenAppMicrobenchmark.java
new file mode 100644
index 0000000..d77422e
--- /dev/null
+++ b/tests/automotive/health/autogeneric/tests/src/android/platform/scenario/autogeneric/OpenAppMicrobenchmark.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2021 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.scenario.autogeneric;
+
+import android.platform.test.scenario.AppStartupRunRule;
+import android.platform.test.microbenchmark.Microbenchmark;
+
+import org.junit.Rule;
+import org.junit.runner.RunWith;
+
+@RunWith(Microbenchmark.class)
+public class OpenAppMicrobenchmark extends OpenApp {
+
+    @Rule public AppStartupRunRule mAppStartupRunRule = new AppStartupRunRule<>(sHelper.get());
+}
diff --git a/tests/automotive/health/dial/Android.bp b/tests/automotive/health/dial/Android.bp
new file mode 100644
index 0000000..d4a61da
--- /dev/null
+++ b/tests/automotive/health/dial/Android.bp
@@ -0,0 +1,32 @@
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library_static {
+    name: "automotive-dial-scenarios",
+    min_sdk_version: "24",
+    srcs: [ "src/**/*.java" ],
+    libs: [
+        "androidx.test.runner",
+        "automotive-settings-app-helper",
+        "app-helpers-auto-interfaces",
+        "common-platform-scenarios",
+        "platform-test-options",
+        "platform-test-rules",
+        "ub-uiautomator",
+    ],
+}
diff --git a/tests/automotive/health/dial/src/android/platform/scenario/dial/EndCall.java b/tests/automotive/health/dial/src/android/platform/scenario/dial/EndCall.java
new file mode 100644
index 0000000..35f1b79
--- /dev/null
+++ b/tests/automotive/health/dial/src/android/platform/scenario/dial/EndCall.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2021 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.scenario.dial;
+
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoDialHelper;
+import android.platform.test.scenario.annotation.Scenario;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Enables ending of an ongoing call */
+@Scenario
+@RunWith(JUnit4.class)
+public class EndCall {
+    static HelperAccessor<IAutoDialHelper> sHelper = new HelperAccessor<>(IAutoDialHelper.class);
+
+    @Test
+    public void testOpen() {
+        sHelper.get().endCall();
+    }
+}
diff --git a/tests/automotive/health/dial/src/android/platform/scenario/dial/MakeCall.java b/tests/automotive/health/dial/src/android/platform/scenario/dial/MakeCall.java
new file mode 100644
index 0000000..63c1a22
--- /dev/null
+++ b/tests/automotive/health/dial/src/android/platform/scenario/dial/MakeCall.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2021 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.scenario.dial;
+
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoDialHelper;
+import android.platform.test.scenario.annotation.Scenario;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Makes a call by dialing a number */
+@Scenario
+@RunWith(JUnit4.class)
+public class MakeCall {
+    static HelperAccessor<IAutoDialHelper> sHelper = new HelperAccessor<>(IAutoDialHelper.class);
+
+    @Test
+    public void testOpen() {
+        sHelper.get().dialANumber("511");
+        sHelper.get().makeCall();
+    }
+}
diff --git a/tests/automotive/health/dial/src/android/platform/scenario/dial/OpenApp.java b/tests/automotive/health/dial/src/android/platform/scenario/dial/OpenApp.java
new file mode 100644
index 0000000..d0d8133
--- /dev/null
+++ b/tests/automotive/health/dial/src/android/platform/scenario/dial/OpenApp.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2021 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.scenario.dial;
+
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoDialHelper;
+import android.platform.test.scenario.annotation.Scenario;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Opens the Dial application and exits after. */
+@Scenario
+@RunWith(JUnit4.class)
+public class OpenApp {
+    static HelperAccessor<IAutoDialHelper> sHelper = new HelperAccessor<>(IAutoDialHelper.class);
+
+    @Test
+    public void testOpen() {
+        sHelper.get().open();
+    }
+}
diff --git a/tests/automotive/health/dial/src/android/platform/scenario/dial/ScrollContactList.java b/tests/automotive/health/dial/src/android/platform/scenario/dial/ScrollContactList.java
new file mode 100644
index 0000000..522460c
--- /dev/null
+++ b/tests/automotive/health/dial/src/android/platform/scenario/dial/ScrollContactList.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2021 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.scenario.dial;
+
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoGenericAppHelper;
+import android.platform.test.scenario.annotation.Scenario;
+import android.platform.helpers.IAutoDialHelper;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Scroll down and up in contacts screen of dialer app */
+@Scenario
+@RunWith(JUnit4.class)
+public class ScrollContactList {
+    private static final String DIALER_PACKAGE = "com.android.car.dialer";
+
+    static HelperAccessor<IAutoGenericAppHelper> sAutoGenericHelper =
+            new HelperAccessor<>(IAutoGenericAppHelper.class);
+
+    static {
+        sAutoGenericHelper.get().setPackage(DIALER_PACKAGE);
+        sAutoGenericHelper.get().setScrollableMargin(0, 200, 0, 200);
+    }
+
+    static HelperAccessor<IAutoDialHelper> sHelper = new HelperAccessor<>(IAutoDialHelper.class);
+
+    @Test
+    public void testScrollDownAndUp() {
+        sHelper.get().openContacts();
+        // test scroll down by one page in 500ms.
+        sAutoGenericHelper.get().scrollDownOnePage(500);
+        // test scroll up by one page in 500ms.
+        sAutoGenericHelper.get().scrollUpOnePage(500);
+    }
+}
diff --git a/tests/automotive/health/dial/tests/Android.bp b/tests/automotive/health/dial/tests/Android.bp
new file mode 100644
index 0000000..2cf31af
--- /dev/null
+++ b/tests/automotive/health/dial/tests/Android.bp
@@ -0,0 +1,39 @@
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "AndroidAutomotiveDialScenarioTests",
+    min_sdk_version: "24",
+    static_libs: [
+        "androidx.test.runner",
+        "androidx.test.rules",
+        "automotive-settings-scenarios",
+        "automotive-dial-scenarios",
+        "automotive-health-test-rules",
+        "automotive-settings-app-helper",
+        "app-helpers-auto-interfaces",
+        "ub-uiautomator",
+        "collector-device-lib-platform",
+        "common-platform-scenarios",
+        "common-platform-scenario-tests",
+        "microbenchmark-device-lib",
+        "platform-test-options",
+    ],
+    srcs: ["src/**/*.java"],
+    test_suites: ["catbox"],
+}
diff --git a/tests/automotive/health/dial/tests/AndroidManifest.xml b/tests/automotive/health/dial/tests/AndroidManifest.xml
new file mode 100644
index 0000000..e869394
--- /dev/null
+++ b/tests/automotive/health/dial/tests/AndroidManifest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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="android.platform.test.scenario.dial" >
+  <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.MANAGE_USERS" />
+  <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
+  <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
+
+  <application>
+    <uses-library android:name="android.test.runner"/>
+  </application>
+  <instrumentation
+      android:name="androidx.test.runner.AndroidJUnitRunner"
+      android:targetPackage="android.platform.test.scenario.dial"
+      android:label="Android Automotive Dial Scenario-Based Tests" />
+</manifest>
\ No newline at end of file
diff --git a/tests/automotive/health/dial/tests/src/android/platform/scenario/dial/OpenAppMicrobenchmark.java b/tests/automotive/health/dial/tests/src/android/platform/scenario/dial/OpenAppMicrobenchmark.java
new file mode 100644
index 0000000..01f3386
--- /dev/null
+++ b/tests/automotive/health/dial/tests/src/android/platform/scenario/dial/OpenAppMicrobenchmark.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2021 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.scenario.dial;
+
+import android.platform.test.scenario.AppStartupRunRule;
+import android.platform.test.microbenchmark.Microbenchmark;
+
+import org.junit.Rule;
+import org.junit.runner.RunWith;
+
+@RunWith(Microbenchmark.class)
+public class OpenAppMicrobenchmark extends OpenApp {
+
+    @Rule public AppStartupRunRule mAppStartupRunRule = new AppStartupRunRule<>(sHelper.get());
+}
diff --git a/tests/automotive/health/dial/tests/src/android/platform/scenario/dial/ScrollContactListMicrobenchmark.java b/tests/automotive/health/dial/tests/src/android/platform/scenario/dial/ScrollContactListMicrobenchmark.java
new file mode 100644
index 0000000..85383d2
--- /dev/null
+++ b/tests/automotive/health/dial/tests/src/android/platform/scenario/dial/ScrollContactListMicrobenchmark.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2021 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.scenario.dial;
+
+import android.platform.test.microbenchmark.Microbenchmark;
+
+import org.junit.BeforeClass;
+import org.junit.runner.RunWith;
+
+@RunWith(Microbenchmark.class)
+public class ScrollContactListMicrobenchmark extends ScrollContactList {
+
+    @BeforeClass
+    public static void openApp() {
+        sHelper.get().open();
+    }
+}
diff --git a/tests/automotive/health/evs/Android.bp b/tests/automotive/health/evs/Android.bp
new file mode 100644
index 0000000..9c3a9fd
--- /dev/null
+++ b/tests/automotive/health/evs/Android.bp
@@ -0,0 +1,32 @@
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library_static {
+    name: "automotive-evs-scenarios",
+    min_sdk_version: "24",
+    srcs: [ "src/**/*.java" ],
+    libs: [
+        "androidx.test.runner",
+        "automotive-settings-app-helper",
+        "app-helpers-auto-interfaces",
+        "common-platform-scenarios",
+        "platform-test-options",
+        "platform-test-rules",
+        "ub-uiautomator",
+    ],
+}
diff --git a/tests/automotive/health/evs/src/android/platform/scenario/evs/OpenApp.java b/tests/automotive/health/evs/src/android/platform/scenario/evs/OpenApp.java
new file mode 100644
index 0000000..35b2512
--- /dev/null
+++ b/tests/automotive/health/evs/src/android/platform/scenario/evs/OpenApp.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2021 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.scenario.evs;
+
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoCarEvsServiceHelper;
+import android.platform.test.scenario.annotation.Scenario;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@Scenario
+@RunWith(JUnit4.class)
+public class OpenApp {
+
+    static HelperAccessor<IAutoCarEvsServiceHelper> sHelper =
+            new HelperAccessor<>(IAutoCarEvsServiceHelper.class);
+
+    @Test
+    public void testOpen() {
+        sHelper.get().startEvsPreview();
+    }
+}
diff --git a/tests/automotive/health/evs/tests/Android.bp b/tests/automotive/health/evs/tests/Android.bp
new file mode 100644
index 0000000..544ba26
--- /dev/null
+++ b/tests/automotive/health/evs/tests/Android.bp
@@ -0,0 +1,39 @@
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "AndroidAutomotiveEvsScenarioTests",
+    min_sdk_version: "24",
+    static_libs: [
+        "androidx.test.runner",
+        "androidx.test.rules",
+        "automotive-settings-scenarios",
+        "automotive-evs-scenarios",
+        "automotive-health-test-rules",
+        "automotive-settings-app-helper",
+        "app-helpers-auto-interfaces",
+        "ub-uiautomator",
+        "collector-device-lib-platform",
+        "common-platform-scenarios",
+        "common-platform-scenario-tests",
+        "microbenchmark-device-lib",
+        "platform-test-options",
+    ],
+    srcs: ["src/**/*.java"],
+    test_suites: ["catbox"],
+}
diff --git a/tests/automotive/health/evs/tests/AndroidManifest.xml b/tests/automotive/health/evs/tests/AndroidManifest.xml
new file mode 100644
index 0000000..4aa0722
--- /dev/null
+++ b/tests/automotive/health/evs/tests/AndroidManifest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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="android.platform.test.scenario.evs" >
+  <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.MANAGE_USERS" />
+  <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
+  <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
+
+  <application>
+    <uses-library android:name="android.test.runner"/>
+  </application>
+  <instrumentation
+      android:name="androidx.test.runner.AndroidJUnitRunner"
+      android:targetPackage="android.platform.test.scenario.evs"
+      android:label="Android Automotive Evs Scenario-Based Tests" />
+</manifest>
\ No newline at end of file
diff --git a/tests/automotive/health/evs/tests/src/android/platform/scenario/evs/OpenAppMicrobenchmark.java b/tests/automotive/health/evs/tests/src/android/platform/scenario/evs/OpenAppMicrobenchmark.java
new file mode 100644
index 0000000..8e02aa6
--- /dev/null
+++ b/tests/automotive/health/evs/tests/src/android/platform/scenario/evs/OpenAppMicrobenchmark.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2021 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.scenario.evs;
+
+import android.platform.test.scenario.AppStartupRunRule;
+import android.platform.test.microbenchmark.Microbenchmark;
+
+import org.junit.Rule;
+import org.junit.runner.RunWith;
+
+@RunWith(Microbenchmark.class)
+public class OpenAppMicrobenchmark extends OpenApp {
+
+    @Rule public AppStartupRunRule mAppStartupRunRule = new AppStartupRunRule<>(sHelper.get());
+}
diff --git a/tests/automotive/health/googleplay/Android.bp b/tests/automotive/health/googleplay/Android.bp
new file mode 100644
index 0000000..9c1f12e
--- /dev/null
+++ b/tests/automotive/health/googleplay/Android.bp
@@ -0,0 +1,32 @@
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library_static {
+    name: "automotive-google-play-scenarios",
+    min_sdk_version: "24",
+    srcs: [ "src/**/*.java" ],
+    libs: [
+        "androidx.test.runner",
+        "automotive-settings-app-helper",
+        "app-helpers-auto-interfaces",
+        "common-platform-scenarios",
+        "platform-test-options",
+        "platform-test-rules",
+        "ub-uiautomator",
+    ],
+}
diff --git a/tests/automotive/health/googleplay/src/android/platform/scenario/googleplay/InstallApp.java b/tests/automotive/health/googleplay/src/android/platform/scenario/googleplay/InstallApp.java
new file mode 100644
index 0000000..2f99369
--- /dev/null
+++ b/tests/automotive/health/googleplay/src/android/platform/scenario/googleplay/InstallApp.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2021 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.scenario.googleplay;
+
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoGooglePlayHelper;
+import android.platform.test.scenario.annotation.Scenario;
+
+import androidx.test.InstrumentationRegistry;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * This is test scenario to install app from Google Play Store. It assumes the Play Store app is
+ * already open
+ */
+@Scenario
+@RunWith(JUnit4.class)
+public class InstallApp {
+    // The argument keys are accessible to other class in the same package
+    static final String APP_NAME_PARAM = "app_name_on_play_store";
+    static final String DEFAULT_APP_NAME = "NPR One";
+
+    private static HelperAccessor<IAutoGooglePlayHelper> sHelper =
+            new HelperAccessor<>(IAutoGooglePlayHelper.class);
+
+    private String mAppName;
+
+    public InstallApp() {
+        mAppName =
+                InstrumentationRegistry.getArguments().getString(APP_NAME_PARAM, DEFAULT_APP_NAME);
+    }
+
+    @Test
+    public void testSearchAndInstall() {
+        sHelper.get().searchAndClick(mAppName);
+        sHelper.get().installApp();
+    }
+}
diff --git a/tests/automotive/health/googleplay/src/android/platform/scenario/googleplay/OpenApp.java b/tests/automotive/health/googleplay/src/android/platform/scenario/googleplay/OpenApp.java
new file mode 100644
index 0000000..8b2eb53
--- /dev/null
+++ b/tests/automotive/health/googleplay/src/android/platform/scenario/googleplay/OpenApp.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2021 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.scenario.googleplay;
+
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoGooglePlayHelper;
+import android.platform.test.scenario.annotation.Scenario;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Opens the Googleplay application and exits after. */
+@Scenario
+@RunWith(JUnit4.class)
+public class OpenApp {
+    static HelperAccessor<IAutoGooglePlayHelper> sHelper =
+            new HelperAccessor<>(IAutoGooglePlayHelper.class);
+
+    @Test
+    public void testOpen() {
+        sHelper.get().open();
+    }
+}
diff --git a/tests/automotive/health/googleplay/src/android/platform/scenario/googleplay/OpenAppOnPlayStore.java b/tests/automotive/health/googleplay/src/android/platform/scenario/googleplay/OpenAppOnPlayStore.java
new file mode 100644
index 0000000..6321b8b
--- /dev/null
+++ b/tests/automotive/health/googleplay/src/android/platform/scenario/googleplay/OpenAppOnPlayStore.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2021 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.scenario.googleplay;
+
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoGooglePlayHelper;
+import android.platform.helpers.exceptions.UnknownUiException;
+import android.platform.test.scenario.annotation.Scenario;
+
+import androidx.test.InstrumentationRegistry;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * This is a test to search and open an App on Play Store. It assumes the App is already installed,
+ * otherwise the test will throw exception.
+ */
+@Scenario
+@RunWith(JUnit4.class)
+public class OpenAppOnPlayStore {
+
+    private static HelperAccessor<IAutoGooglePlayHelper> sHelper =
+            new HelperAccessor<>(IAutoGooglePlayHelper.class);
+
+    private static String mAppName;
+
+    public OpenAppOnPlayStore() {
+        mAppName =
+                InstrumentationRegistry.getArguments()
+                        .getString(InstallApp.APP_NAME_PARAM, InstallApp.DEFAULT_APP_NAME);
+    }
+
+    @Test
+    public void openApp() {
+        try {
+            sHelper.get().openApp();
+        } catch (UnknownUiException e) {
+            // If app can not be opened directly, try search for the app first then open it.
+            sHelper.get().searchAndClick(mAppName);
+            sHelper.get().openApp();
+        }
+    }
+}
diff --git a/tests/automotive/health/googleplay/src/android/platform/scenario/googleplay/Scroll.java b/tests/automotive/health/googleplay/src/android/platform/scenario/googleplay/Scroll.java
new file mode 100644
index 0000000..b92977e
--- /dev/null
+++ b/tests/automotive/health/googleplay/src/android/platform/scenario/googleplay/Scroll.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2021 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.scenario.googleplay;
+
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoGooglePlayHelper;
+import android.platform.test.scenario.annotation.Scenario;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Scroll down and up in Google play. */
+@Scenario
+@RunWith(JUnit4.class)
+public class Scroll {
+    static HelperAccessor<IAutoGooglePlayHelper> sHelper =
+            new HelperAccessor<>(IAutoGooglePlayHelper.class);
+
+    @Test
+    public void testScrollDownAndUp() {
+        sHelper.get().scrollDownOnePage(500);
+        sHelper.get().scrollUpOnePage(500);
+    }
+}
diff --git a/tests/automotive/health/googleplay/tests/Android.bp b/tests/automotive/health/googleplay/tests/Android.bp
new file mode 100644
index 0000000..116ef39
--- /dev/null
+++ b/tests/automotive/health/googleplay/tests/Android.bp
@@ -0,0 +1,39 @@
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "AndroidAutomotiveGooglePlayScenarioTests",
+    min_sdk_version: "24",
+    static_libs: [
+        "androidx.test.runner",
+        "androidx.test.rules",
+        "automotive-settings-scenarios",
+        "automotive-google-play-scenarios",
+        "automotive-health-test-rules",
+        "automotive-settings-app-helper",
+        "app-helpers-auto-interfaces",
+        "ub-uiautomator",
+        "collector-device-lib-platform",
+        "common-platform-scenarios",
+        "common-platform-scenario-tests",
+        "microbenchmark-device-lib",
+        "platform-test-options",
+    ],
+    srcs: ["src/**/*.java"],
+    test_suites: ["catbox"],
+}
diff --git a/tests/automotive/health/googleplay/tests/AndroidManifest.xml b/tests/automotive/health/googleplay/tests/AndroidManifest.xml
new file mode 100644
index 0000000..2b5419f
--- /dev/null
+++ b/tests/automotive/health/googleplay/tests/AndroidManifest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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="android.platform.test.scenario.googleplay" >
+  <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.MANAGE_USERS" />
+  <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
+  <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
+
+  <application>
+    <uses-library android:name="android.test.runner"/>
+  </application>
+  <instrumentation
+      android:name="androidx.test.runner.AndroidJUnitRunner"
+      android:targetPackage="android.platform.test.scenario.googleplay"
+      android:label="Android Automotive Google Play Scenario-Based Tests" />
+</manifest>
\ No newline at end of file
diff --git a/tests/automotive/health/googleplay/tests/src/android/platform/scenario/googleplay/OpenAppMicrobenchmark.java b/tests/automotive/health/googleplay/tests/src/android/platform/scenario/googleplay/OpenAppMicrobenchmark.java
new file mode 100644
index 0000000..ee7b993
--- /dev/null
+++ b/tests/automotive/health/googleplay/tests/src/android/platform/scenario/googleplay/OpenAppMicrobenchmark.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2021 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.scenario.googleplay;
+
+import android.platform.test.scenario.AppStartupRunRule;
+import android.platform.test.microbenchmark.Microbenchmark;
+
+import org.junit.Rule;
+import org.junit.runner.RunWith;
+
+@RunWith(Microbenchmark.class)
+public class OpenAppMicrobenchmark extends OpenApp {
+
+    @Rule public AppStartupRunRule mAppStartupRunRule = new AppStartupRunRule<>(sHelper.get());
+}
diff --git a/tests/automotive/health/googleplay/tests/src/android/platform/scenario/googleplay/ScrollMicrobenchmark.java b/tests/automotive/health/googleplay/tests/src/android/platform/scenario/googleplay/ScrollMicrobenchmark.java
new file mode 100644
index 0000000..c1a8d65
--- /dev/null
+++ b/tests/automotive/health/googleplay/tests/src/android/platform/scenario/googleplay/ScrollMicrobenchmark.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2021 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.scenario.googleplay;
+
+import android.platform.test.microbenchmark.Microbenchmark;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.runner.RunWith;
+
+@RunWith(Microbenchmark.class)
+public class ScrollMicrobenchmark extends Scroll {
+    @BeforeClass
+    public static void openApp() {
+        sHelper.get().open();
+        sHelper.get().searchApp("youtube");
+    }
+
+    @AfterClass
+    public static void closeApp() {
+        sHelper.get().exit();
+    }
+}
diff --git a/tests/automotive/health/mediacenter/Android.bp b/tests/automotive/health/mediacenter/Android.bp
new file mode 100644
index 0000000..cb19f10
--- /dev/null
+++ b/tests/automotive/health/mediacenter/Android.bp
@@ -0,0 +1,32 @@
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library_static {
+    name: "automotive-media-center-scenarios",
+    min_sdk_version: "24",
+    srcs: [ "src/**/*.java" ],
+    libs: [
+        "androidx.test.runner",
+        "automotive-settings-app-helper",
+        "app-helpers-auto-interfaces",
+        "common-platform-scenarios",
+        "platform-test-options",
+        "platform-test-rules",
+        "ub-uiautomator",
+    ],
+}
diff --git a/tests/automotive/health/mediacenter/src/android/platform/scenario/mediacenter/bluetooth/OpenMedia.java b/tests/automotive/health/mediacenter/src/android/platform/scenario/mediacenter/bluetooth/OpenMedia.java
new file mode 100644
index 0000000..176879a
--- /dev/null
+++ b/tests/automotive/health/mediacenter/src/android/platform/scenario/mediacenter/bluetooth/OpenMedia.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2021 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.scenario.mediacenter.bluetooth;
+
+import android.platform.test.scenario.annotation.Scenario;
+import android.support.test.uiautomator.UiDevice;
+import androidx.test.InstrumentationRegistry;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.IOException;
+
+/** Opens Bluetooth application */
+@Scenario
+@RunWith(JUnit4.class)
+public class OpenMedia {
+    @Test
+    public void testOpen() throws IOException {
+        UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+                .executeShellCommand(
+                        "am start -a android.car.intent.action.MEDIA_TEMPLATE -e "
+                                + "android.car.intent.extra.MEDIA_COMPONENT "
+                                + "com.android.bluetooth"
+                                + "/com.android.bluetooth.avrcpcontroller"
+                                + ".BluetoothMediaBrowseService");
+    }
+}
diff --git a/tests/automotive/health/mediacenter/src/android/platform/scenario/mediacenter/bluetooth/PlayMedia.java b/tests/automotive/health/mediacenter/src/android/platform/scenario/mediacenter/bluetooth/PlayMedia.java
new file mode 100644
index 0000000..5fe911c
--- /dev/null
+++ b/tests/automotive/health/mediacenter/src/android/platform/scenario/mediacenter/bluetooth/PlayMedia.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2021 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.scenario.mediacenter.bluetooth;
+
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoMediaHelper;
+import android.platform.test.scenario.annotation.Scenario;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Scenario to Play Media via Bluetooth. */
+@Scenario
+@RunWith(JUnit4.class)
+public class PlayMedia {
+
+    private static HelperAccessor<IAutoMediaHelper> sHelper =
+            new HelperAccessor<>(IAutoMediaHelper.class);
+
+    @Test
+    public void testPlay() {
+        sHelper.get().playMedia();
+    }
+}
diff --git a/tests/automotive/health/mediacenter/src/android/platform/scenario/mediacenter/googleplaybooks/OpenGoogleBooks.java b/tests/automotive/health/mediacenter/src/android/platform/scenario/mediacenter/googleplaybooks/OpenGoogleBooks.java
new file mode 100644
index 0000000..41ef2a4
--- /dev/null
+++ b/tests/automotive/health/mediacenter/src/android/platform/scenario/mediacenter/googleplaybooks/OpenGoogleBooks.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2021 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.scenario.mediacenter.googleplaybooks;
+
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoMediaCenterHelper;
+import android.platform.test.scenario.annotation.Scenario;
+import android.support.test.uiautomator.UiDevice;
+import androidx.test.InstrumentationRegistry;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.IOException;
+
+/** Opens the Google Books and plays an audio book */
+@Scenario
+@RunWith(JUnit4.class)
+public class OpenGoogleBooks {
+    static HelperAccessor<IAutoMediaCenterHelper> sMediaCenterHelper =
+            new HelperAccessor<>(IAutoMediaCenterHelper.class);
+
+    @Test
+    public void testOpen() throws IOException {
+        UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+                .executeShellCommand(
+                        "am start -a android.car.intent.action.MEDIA_TEMPLATE -e "
+                                + "android.car.intent.extra.MEDIA_COMPONENT "
+                                + "com.google.android.apps.books"
+                                + "/com.google.android.apps.play.books.audio"
+                                + ".BooksMediaBrowseService");
+    }
+}
diff --git a/tests/automotive/health/mediacenter/src/android/platform/scenario/mediacenter/googleplaybooks/PlayBooks.java b/tests/automotive/health/mediacenter/src/android/platform/scenario/mediacenter/googleplaybooks/PlayBooks.java
new file mode 100644
index 0000000..c8e0c64
--- /dev/null
+++ b/tests/automotive/health/mediacenter/src/android/platform/scenario/mediacenter/googleplaybooks/PlayBooks.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2021 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.scenario.mediacenter.googleplaybooks;
+
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoMediaCenterHelper;
+import android.platform.test.scenario.annotation.Scenario;
+import androidx.test.InstrumentationRegistry;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.IOException;
+
+/** Plays an audio book assuming Google Play Books is open */
+@Scenario
+@RunWith(JUnit4.class)
+public class PlayBooks {
+
+    private String mNameOftheSong;
+    static HelperAccessor<IAutoMediaCenterHelper> sMediaCenterHelper =
+            new HelperAccessor<>(IAutoMediaCenterHelper.class);
+
+    @Test
+    public void testOpen() throws IOException {
+        mNameOftheSong =
+                InstrumentationRegistry.getArguments()
+                        .getString("song_name", "Once Gone (a Riley Paige Mystery--Book #1)");
+        sMediaCenterHelper.get().selectMediaTrack("Library", mNameOftheSong);
+    }
+}
diff --git a/tests/automotive/health/mediacenter/src/android/platform/scenario/mediacenter/googleplaybooks/Scroll.java b/tests/automotive/health/mediacenter/src/android/platform/scenario/mediacenter/googleplaybooks/Scroll.java
new file mode 100644
index 0000000..961ddd8
--- /dev/null
+++ b/tests/automotive/health/mediacenter/src/android/platform/scenario/mediacenter/googleplaybooks/Scroll.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2021 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.scenario.mediacenter.googleplaybooks;
+
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoGenericAppHelper;
+import android.platform.test.scenario.annotation.Scenario;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Scroll down and up in Google play Books' content list. */
+@Scenario
+@RunWith(JUnit4.class)
+public class Scroll {
+    private static final String MEDIA_CENTER_PACKAGE = "com.android.car.media";
+
+    static HelperAccessor<IAutoGenericAppHelper> sHelper =
+            new HelperAccessor<>(IAutoGenericAppHelper.class);
+
+    static {
+        sHelper.get().setPackage(MEDIA_CENTER_PACKAGE);
+        // Media center has some UI components overlays the scrollable region at both the top and
+        // bottom section, and hence a large margin is required.
+        sHelper.get().setScrollableMargin(0, 200, 0, 200);
+    }
+
+    @Test
+    public void testScrollDownAndUp() {
+        sHelper.get().scrollDownOnePage(500);
+        sHelper.get().scrollUpOnePage(500);
+    }
+}
diff --git a/tests/automotive/health/mediacenter/src/android/platform/scenario/mediacenter/googleplaybooks/SwitchPlayBack.java b/tests/automotive/health/mediacenter/src/android/platform/scenario/mediacenter/googleplaybooks/SwitchPlayBack.java
new file mode 100644
index 0000000..5236191
--- /dev/null
+++ b/tests/automotive/health/mediacenter/src/android/platform/scenario/mediacenter/googleplaybooks/SwitchPlayBack.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2021 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.scenario.mediacenter.googleplaybooks;
+
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoMediaCenterMinimizeControlBarHelper;
+import android.platform.helpers.IAutoMediaCenterNowPlayingHelper;
+import android.platform.test.scenario.annotation.Scenario;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Maximize and minimize playback screen in Google Play Books. */
+@Scenario
+@RunWith(JUnit4.class)
+public class SwitchPlayBack {
+    private static final String MEDIA_CENTER_PACKAGE = "com.android.car.media";
+
+    private static HelperAccessor<IAutoMediaCenterMinimizeControlBarHelper> sMinPlaybackHelper =
+            new HelperAccessor<>(IAutoMediaCenterMinimizeControlBarHelper.class);
+    private static HelperAccessor<IAutoMediaCenterNowPlayingHelper> sMaxPlaybackHelper =
+            new HelperAccessor<>(IAutoMediaCenterNowPlayingHelper.class);
+
+    @Test
+    public void testScrollDownAndUp() {
+        sMinPlaybackHelper.get().maximizeNowPlaying();
+        sMaxPlaybackHelper.get().minimizeNowPlaying();
+    }
+}
diff --git a/tests/automotive/health/mediacenter/src/android/platform/scenario/mediacenter/testmedia/OpenApp.java b/tests/automotive/health/mediacenter/src/android/platform/scenario/mediacenter/testmedia/OpenApp.java
new file mode 100644
index 0000000..fd64172
--- /dev/null
+++ b/tests/automotive/health/mediacenter/src/android/platform/scenario/mediacenter/testmedia/OpenApp.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2021 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.scenario.mediacenter.testmedia;
+
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoGenericAppHelper;
+import android.platform.helpers.IAutoMediaCenterHelper;
+import android.platform.test.scenario.annotation.Scenario;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/** Opens the Test Media App application by launching media center. */
+@Scenario
+@RunWith(JUnit4.class)
+public class OpenApp {
+
+    private static final String MEDIA_CENTER_PACKAGE = "com.android.car.media";
+    private static final String MEDIA_TEMPLATE = "android.car.intent.action.MEDIA_TEMPLATE";
+    private static final Map<String, String> TEST_MEDIA_APP_EXTRA_ARGS =
+            Stream.of(
+                            new Object[][] {
+                                {
+                                    "android.car.intent.extra.MEDIA_COMPONENT",
+                                    "com.android.car.media.testmediaapp/com.android.car.media.testmediaapp.TmaBrowser"
+                                },
+                            })
+                    .collect(Collectors.toMap(data -> (String) data[0], data -> (String) data[1]));
+
+    static HelperAccessor<IAutoGenericAppHelper> sAutoGenericrHelper =
+            new HelperAccessor<>(IAutoGenericAppHelper.class);
+
+    static HelperAccessor<IAutoMediaCenterHelper> sMediaCenterHelper =
+            new HelperAccessor<>(IAutoMediaCenterHelper.class);
+
+    static {
+        sAutoGenericrHelper.get().setPackage(MEDIA_CENTER_PACKAGE);
+        sAutoGenericrHelper.get().setLaunchAction(MEDIA_TEMPLATE, TEST_MEDIA_APP_EXTRA_ARGS);
+    }
+
+    @Test
+    public void Open() {
+        sAutoGenericrHelper.get().open();
+    }
+}
diff --git a/tests/automotive/health/mediacenter/src/android/platform/scenario/mediacenter/testmedia/Scroll.java b/tests/automotive/health/mediacenter/src/android/platform/scenario/mediacenter/testmedia/Scroll.java
new file mode 100644
index 0000000..31e77b2
--- /dev/null
+++ b/tests/automotive/health/mediacenter/src/android/platform/scenario/mediacenter/testmedia/Scroll.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2021 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.scenario.mediacenter.testmedia;
+
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoGenericAppHelper;
+import android.platform.test.scenario.annotation.Scenario;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Scroll down and up in Google play Books' content list. */
+@Scenario
+@RunWith(JUnit4.class)
+public class Scroll {
+    private static final String MEDIA_CENTER_PACKAGE = "com.android.car.media";
+
+    static HelperAccessor<IAutoGenericAppHelper> sAutoGenericHelper =
+            new HelperAccessor<>(IAutoGenericAppHelper.class);
+
+    static {
+        sAutoGenericHelper.get().setPackage(MEDIA_CENTER_PACKAGE);
+        // Media center has some UI components overlays the scrollable region at both the top and
+        // bottom section, and hence a large margin is required.
+        sAutoGenericHelper.get().setScrollableMargin(0, 200, 0, 200);
+    }
+
+    @Test
+    public void testScrollDownAndUp() {
+        sAutoGenericHelper.get().scrollDownOnePage(2500);
+        sAutoGenericHelper.get().scrollUpOnePage(2500);
+    }
+}
diff --git a/tests/automotive/health/mediacenter/src/android/platform/scenario/mediacenter/testmedia/SwitchPlayback.java b/tests/automotive/health/mediacenter/src/android/platform/scenario/mediacenter/testmedia/SwitchPlayback.java
new file mode 100644
index 0000000..3c9108d
--- /dev/null
+++ b/tests/automotive/health/mediacenter/src/android/platform/scenario/mediacenter/testmedia/SwitchPlayback.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2021 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.scenario.mediacenter.testmedia;
+
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoMediaCenterMinimizeControlBarHelper;
+import android.platform.helpers.IAutoMediaCenterNowPlayingHelper;
+import android.platform.test.scenario.annotation.Scenario;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Maximize and minimize playback screen in Test Media App. */
+@Scenario
+@RunWith(JUnit4.class)
+public class SwitchPlayback {
+
+    static HelperAccessor<IAutoMediaCenterMinimizeControlBarHelper> sMinPlaybackHelper =
+            new HelperAccessor<>(IAutoMediaCenterMinimizeControlBarHelper.class);
+
+    static HelperAccessor<IAutoMediaCenterNowPlayingHelper> sMaxPlaybackHelper =
+            new HelperAccessor<>(IAutoMediaCenterNowPlayingHelper.class);
+
+    @Test
+    public void testSwitchPlayback() {
+        sMaxPlaybackHelper.get().minimizeNowPlaying();
+        sMinPlaybackHelper.get().maximizeNowPlaying();
+    }
+}
diff --git a/tests/automotive/health/mediacenter/tests/Android.bp b/tests/automotive/health/mediacenter/tests/Android.bp
new file mode 100644
index 0000000..824d854
--- /dev/null
+++ b/tests/automotive/health/mediacenter/tests/Android.bp
@@ -0,0 +1,39 @@
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "AndroidAutomotiveMediaCenterScenarioTests",
+    min_sdk_version: "24",
+    static_libs: [
+        "androidx.test.runner",
+        "androidx.test.rules",
+        "automotive-settings-scenarios",
+        "automotive-media-center-scenarios",
+        "automotive-health-test-rules",
+        "automotive-settings-app-helper",
+        "app-helpers-auto-interfaces",
+        "ub-uiautomator",
+        "collector-device-lib-platform",
+        "common-platform-scenarios",
+        "common-platform-scenario-tests",
+        "microbenchmark-device-lib",
+        "platform-test-options",
+    ],
+    srcs: ["src/**/*.java"],
+    test_suites: ["catbox"],
+}
diff --git a/tests/automotive/health/mediacenter/tests/AndroidManifest.xml b/tests/automotive/health/mediacenter/tests/AndroidManifest.xml
new file mode 100644
index 0000000..2272041
--- /dev/null
+++ b/tests/automotive/health/mediacenter/tests/AndroidManifest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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="android.platform.test.scenario.mediacenter" >
+  <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.MANAGE_USERS" />
+  <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
+  <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
+
+  <application>
+    <uses-library android:name="android.test.runner"/>
+  </application>
+  <instrumentation
+      android:name="androidx.test.runner.AndroidJUnitRunner"
+      android:targetPackage="android.platform.test.scenario.mediacenter"
+      android:label="Android Automotive Media Center Scenario-Based Tests" />
+</manifest>
\ No newline at end of file
diff --git a/tests/automotive/health/mediacenter/tests/src/android/platform/scenario/mediacenter/googleplaybooks/ScrollMicrobenchmark.java b/tests/automotive/health/mediacenter/tests/src/android/platform/scenario/mediacenter/googleplaybooks/ScrollMicrobenchmark.java
new file mode 100644
index 0000000..d715825
--- /dev/null
+++ b/tests/automotive/health/mediacenter/tests/src/android/platform/scenario/mediacenter/googleplaybooks/ScrollMicrobenchmark.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2021 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.scenario.mediacenter.googleplaybooks;
+
+import android.platform.test.microbenchmark.Microbenchmark;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.runner.RunWith;
+
+@RunWith(Microbenchmark.class)
+public class ScrollMicrobenchmark extends Scroll {
+    @BeforeClass
+    public static void openApp() {
+        sHelper.get().open();
+    }
+
+    @AfterClass
+    public static void closeApp() {
+        sHelper.get().exit();
+    }
+}
diff --git a/tests/automotive/health/mediacenter/tests/src/android/platform/scenario/mediacenter/googleplaybooks/SwitchPlayBackMicrobenchmark.java b/tests/automotive/health/mediacenter/tests/src/android/platform/scenario/mediacenter/googleplaybooks/SwitchPlayBackMicrobenchmark.java
new file mode 100644
index 0000000..9134d36
--- /dev/null
+++ b/tests/automotive/health/mediacenter/tests/src/android/platform/scenario/mediacenter/googleplaybooks/SwitchPlayBackMicrobenchmark.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2021 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.scenario.mediacenter.googleplaybooks;
+
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoGenericAppHelper;
+import android.platform.test.microbenchmark.Microbenchmark;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.runner.RunWith;
+
+@RunWith(Microbenchmark.class)
+public class SwitchPlayBackMicrobenchmark extends SwitchPlayBack {
+    private static final String MEDIA_CENTER_PACKAGE = "com.android.car.media";
+
+    private static HelperAccessor<IAutoGenericAppHelper> sHelper =
+            new HelperAccessor<>(IAutoGenericAppHelper.class);
+
+    static {
+        sHelper.get().setPackage(MEDIA_CENTER_PACKAGE);
+    }
+
+    @BeforeClass
+    public static void openApp() {
+        sHelper.get().open();
+    }
+
+    @AfterClass
+    public static void closeApp() {
+        sHelper.get().exit();
+    }
+}
diff --git a/tests/automotive/health/mediacenter/tests/src/android/platform/scenario/mediacenter/testmedia/OpenMediaCenterMicrobenchmark.java b/tests/automotive/health/mediacenter/tests/src/android/platform/scenario/mediacenter/testmedia/OpenMediaCenterMicrobenchmark.java
new file mode 100644
index 0000000..5a30ac7
--- /dev/null
+++ b/tests/automotive/health/mediacenter/tests/src/android/platform/scenario/mediacenter/testmedia/OpenMediaCenterMicrobenchmark.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2021 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.scenario.mediacenter.testmedia;
+
+import android.platform.test.microbenchmark.Microbenchmark;
+import android.platform.test.scenario.AppStartupRunRule;
+
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.runner.RunWith;
+
+@RunWith(Microbenchmark.class)
+public class OpenMediaCenterMicrobenchmark extends OpenApp {
+
+    @Rule
+    public AppStartupRunRule mAppStartupRunRule =
+            new AppStartupRunRule<>(sAutoGenericrHelper.get());
+
+    @BeforeClass
+    public static void setup() {
+        sAutoGenericrHelper.get().open();
+        sMediaCenterHelper.get().dismissInitialDialogs();
+    }
+}
diff --git a/tests/automotive/health/mediacenter/tests/src/android/platform/scenario/mediacenter/testmedia/ScrollMicrobenchmark.java b/tests/automotive/health/mediacenter/tests/src/android/platform/scenario/mediacenter/testmedia/ScrollMicrobenchmark.java
new file mode 100644
index 0000000..2e6b338
--- /dev/null
+++ b/tests/automotive/health/mediacenter/tests/src/android/platform/scenario/mediacenter/testmedia/ScrollMicrobenchmark.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2021 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.scenario.mediacenter.testmedia;
+
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoGenericAppHelper;
+import android.platform.helpers.IAutoMediaCenterHelper;
+import android.platform.test.microbenchmark.Microbenchmark;
+
+import org.junit.BeforeClass;
+import org.junit.runner.RunWith;
+
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+@RunWith(Microbenchmark.class)
+public class ScrollMicrobenchmark extends Scroll {
+
+    private static final String MEDIA_CENTER_PACKAGE = "com.android.car.media";
+    private static final String MEDIA_TEMPLATE = "android.car.intent.action.MEDIA_TEMPLATE";
+    private static final Map<String, String> TEST_MEDIA_APP_EXTRA_ARGS =
+            Stream.of(
+                            new Object[][] {
+                                {
+                                    "android.car.intent.extra.MEDIA_COMPONENT",
+                                    "com.android.car.media.testmediaapp/com.android.car.media.testmediaapp.TmaBrowser"
+                                },
+                            })
+                    .collect(Collectors.toMap(data -> (String) data[0], data -> (String) data[1]));
+
+    static HelperAccessor<IAutoGenericAppHelper> sAutoGenericrHelper =
+            new HelperAccessor<>(IAutoGenericAppHelper.class);
+
+    static HelperAccessor<IAutoMediaCenterHelper> sMediaCenterHelper =
+            new HelperAccessor<>(IAutoMediaCenterHelper.class);
+
+    static {
+        sAutoGenericrHelper.get().setPackage(MEDIA_CENTER_PACKAGE);
+        sAutoGenericrHelper.get().setLaunchAction(MEDIA_TEMPLATE, TEST_MEDIA_APP_EXTRA_ARGS);
+    }
+
+    @BeforeClass
+    public static void openApp() {
+        sAutoGenericrHelper.get().open();
+        sMediaCenterHelper.get().dismissInitialDialogs();
+    }
+}
diff --git a/tests/automotive/health/mediacenter/tests/src/android/platform/scenario/mediacenter/testmedia/SwitchPlaybackMicrobenchmark.java b/tests/automotive/health/mediacenter/tests/src/android/platform/scenario/mediacenter/testmedia/SwitchPlaybackMicrobenchmark.java
new file mode 100644
index 0000000..d7ee8ac
--- /dev/null
+++ b/tests/automotive/health/mediacenter/tests/src/android/platform/scenario/mediacenter/testmedia/SwitchPlaybackMicrobenchmark.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2021 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.scenario.mediacenter.testmedia;
+
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoGenericAppHelper;
+import android.platform.helpers.IAutoMediaCenterHelper;
+import android.platform.test.microbenchmark.Microbenchmark;
+
+import org.junit.BeforeClass;
+import org.junit.runner.RunWith;
+
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+@RunWith(Microbenchmark.class)
+public class SwitchPlaybackMicrobenchmark extends SwitchPlayback {
+
+    private static final String MEDIA_CENTER_PACKAGE = "com.android.car.media";
+    private static final String MEDIA_TEMPLATE = "android.car.intent.action.MEDIA_TEMPLATE";
+    private static final String SONG = "A normal 1H song";
+    private static final Map<String, String> TEST_MEDIA_APP_EXTRA_ARGS =
+            Stream.of(
+                            new Object[][] {
+                                {
+                                    "android.car.intent.extra.MEDIA_COMPONENT",
+                                    "com.android.car.media.testmediaapp/com.android.car.media.testmediaapp.TmaBrowser"
+                                },
+                            })
+                    .collect(Collectors.toMap(data -> (String) data[0], data -> (String) data[1]));
+
+    static HelperAccessor<IAutoGenericAppHelper> sAutoGenericrHelper =
+            new HelperAccessor<>(IAutoGenericAppHelper.class);
+
+    static HelperAccessor<IAutoMediaCenterHelper> sMediaCenterHelper =
+            new HelperAccessor<>(IAutoMediaCenterHelper.class);
+
+    static {
+        sAutoGenericrHelper.get().setPackage(MEDIA_CENTER_PACKAGE);
+        sAutoGenericrHelper.get().setLaunchAction(MEDIA_TEMPLATE, TEST_MEDIA_APP_EXTRA_ARGS);
+    }
+
+    @BeforeClass
+    public static void openApp() {
+        sAutoGenericrHelper.get().open();
+        sMediaCenterHelper.get().dismissInitialDialogs();
+        sMediaCenterHelper.get().selectMediaTrack(SONG);
+    }
+}
diff --git a/tests/automotive/health/multiuser/Android.bp b/tests/automotive/health/multiuser/Android.bp
new file mode 100644
index 0000000..cc68c1b
--- /dev/null
+++ b/tests/automotive/health/multiuser/Android.bp
@@ -0,0 +1,37 @@
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library_static {
+    name: "automotive-multiuser-scenarios",
+    min_sdk_version: "24",
+    srcs: [ "src/**/*.java" ],
+    libs: [
+        "androidx.test.runner",
+        "androidx.test.rules",
+        "automotive-settings-app-helper",
+        "automotive-utility-helper",
+        "automotive-settings-scenarios",
+        "app-helpers-auto-interfaces",
+        "app-helpers-common-interfaces",
+        "common-platform-scenarios",
+        "platform-test-options",
+        "platform-test-rules",
+        "ub-uiautomator",
+        "multi-user-helper",
+    ],
+}
diff --git a/tests/automotive/health/multiuser/src/android/platform/scenario/multiuser/nonui/MultiUserConstants.java b/tests/automotive/health/multiuser/src/android/platform/scenario/multiuser/nonui/MultiUserConstants.java
new file mode 100644
index 0000000..a85733a
--- /dev/null
+++ b/tests/automotive/health/multiuser/src/android/platform/scenario/multiuser/nonui/MultiUserConstants.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2021 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.scenario.multiuser;
+
+import android.platform.helpers.MultiUserHelper;
+import androidx.test.InstrumentationRegistry;
+
+class MultiUserConstants {
+
+    // Shouldn't be too optimistic for the MU latency. The time to wait for system idle will be
+    // 2 x (worst-case number) to make sure regression not affecting test itself.
+    static final long WAIT_FOR_IDLE_TIME_MS = 40000;
+    // Default initial user does not change throughout the test process
+    static final int DEFAULT_INITIAL_USER = MultiUserHelper.getInstance().getInitialUser();
+
+    static final String INCLUDE_CREATION_TIME_KEY = "include_creation_time";
+    static final boolean INCLUDE_CREATION_TIME =
+        Boolean.valueOf(
+            InstrumentationRegistry.getArguments().getString(INCLUDE_CREATION_TIME_KEY, "false"));
+
+    static final String GUEST_NAME = "Guest";
+    static final String SECONDARY_USER_NAME = "SecondaryUser";
+}
diff --git a/tests/automotive/health/multiuser/src/android/platform/scenario/multiuser/nonui/SwitchToExistingSecondaryUser.java b/tests/automotive/health/multiuser/src/android/platform/scenario/multiuser/nonui/SwitchToExistingSecondaryUser.java
new file mode 100644
index 0000000..4de4442
--- /dev/null
+++ b/tests/automotive/health/multiuser/src/android/platform/scenario/multiuser/nonui/SwitchToExistingSecondaryUser.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2021 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.scenario.multiuser;
+
+import android.content.pm.UserInfo;
+import android.os.SystemClock;
+import android.platform.helpers.MultiUserHelper;
+import android.platform.test.scenario.annotation.Scenario;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * This test will switch to an existing secondary non-guest user from default initial user.
+ *
+ * <p>It should be running under user 0, otherwise instrumentation may be killed after user
+ * switched.
+ */
+@Scenario
+@RunWith(JUnit4.class)
+public class SwitchToExistingSecondaryUser {
+
+    private final MultiUserHelper mMultiUserHelper = MultiUserHelper.getInstance();
+    private int mTargetUserId;
+
+    @Before
+    public void setup() throws Exception {
+    /*
+    TODO: Create setup util API
+     */
+        UserInfo targetUser = mMultiUserHelper
+            .getUserByName(MultiUserConstants.SECONDARY_USER_NAME);
+        if (targetUser == null) {
+            // Create new user and switch to it for the first time
+            mTargetUserId = mMultiUserHelper
+                .createUser(MultiUserConstants.SECONDARY_USER_NAME, false);
+            // In order to skip reporting the duration for the first time a user is created,
+            // always switch to newly created user for the first time it is created during setup.
+            mMultiUserHelper.switchAndWaitForStable(
+                mTargetUserId, MultiUserConstants.WAIT_FOR_IDLE_TIME_MS);
+        }
+        UserInfo currentUser = mMultiUserHelper.getCurrentForegroundUserInfo();
+        if (currentUser.id != MultiUserConstants.DEFAULT_INITIAL_USER) {
+            SystemClock.sleep(MultiUserConstants.WAIT_FOR_IDLE_TIME_MS);
+            mMultiUserHelper.switchAndWaitForStable(
+                MultiUserConstants.DEFAULT_INITIAL_USER, MultiUserConstants.WAIT_FOR_IDLE_TIME_MS);
+        }
+    }
+
+    @Test
+    public void testSwitch() throws Exception {
+        mMultiUserHelper.switchToUserId(mTargetUserId);
+    }
+}
diff --git a/tests/automotive/health/multiuser/src/android/platform/scenario/multiuser/nonui/SwitchToNewGuest.java b/tests/automotive/health/multiuser/src/android/platform/scenario/multiuser/nonui/SwitchToNewGuest.java
new file mode 100644
index 0000000..4a9c247
--- /dev/null
+++ b/tests/automotive/health/multiuser/src/android/platform/scenario/multiuser/nonui/SwitchToNewGuest.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2021 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.scenario.multiuser;
+
+import android.content.pm.UserInfo;
+import android.os.SystemClock;
+import android.platform.helpers.MultiUserHelper;
+import android.platform.test.scenario.annotation.Scenario;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * This test will always switch to a newly created guest from default initial user.
+ *
+ * <p>It should be running under user 0, otherwise instrumentation may be killed after user
+ * switched.
+ */
+@Scenario
+@RunWith(JUnit4.class)
+public class SwitchToNewGuest {
+
+    private final MultiUserHelper mMultiUserHelper = MultiUserHelper.getInstance();
+    private int mGuestId;
+
+    @Before
+    public void setup() throws Exception {
+    /*
+    TODO: Create setup util API
+     */
+        UserInfo currentUser = mMultiUserHelper.getCurrentForegroundUserInfo();
+        if (currentUser.id != MultiUserConstants.DEFAULT_INITIAL_USER) {
+            SystemClock.sleep(MultiUserConstants.WAIT_FOR_IDLE_TIME_MS);
+            mMultiUserHelper.switchAndWaitForStable(
+                MultiUserConstants.DEFAULT_INITIAL_USER, MultiUserConstants.WAIT_FOR_IDLE_TIME_MS);
+        }
+        if (!MultiUserConstants.INCLUDE_CREATION_TIME) {
+            mGuestId = mMultiUserHelper.createUser(MultiUserConstants.GUEST_NAME, true);
+        }
+    }
+
+    @Test
+    public void testSwitch() throws Exception {
+        if (MultiUserConstants.INCLUDE_CREATION_TIME) {
+            mGuestId = mMultiUserHelper.createUser(MultiUserConstants.GUEST_NAME, true);
+        }
+        mMultiUserHelper.switchToUserId(mGuestId);
+    }
+}
diff --git a/tests/automotive/health/multiuser/src/android/platform/scenario/multiuser/nonui/SwitchToNewSecondaryUser.java b/tests/automotive/health/multiuser/src/android/platform/scenario/multiuser/nonui/SwitchToNewSecondaryUser.java
new file mode 100644
index 0000000..8bfb20d
--- /dev/null
+++ b/tests/automotive/health/multiuser/src/android/platform/scenario/multiuser/nonui/SwitchToNewSecondaryUser.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2021 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.scenario.multiuser;
+
+import android.content.pm.UserInfo;
+import android.os.SystemClock;
+import android.platform.helpers.MultiUserHelper;
+import android.platform.test.scenario.annotation.Scenario;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * This test will switch to a new secondary non-guest user from default initial user.
+ *
+ * <p>It should be running under user 0, otherwise instrumentation may be killed after user
+ * switched.
+ */
+@Scenario
+@RunWith(JUnit4.class)
+public class SwitchToNewSecondaryUser {
+
+    private final MultiUserHelper mMultiUserHelper = MultiUserHelper.getInstance();
+    private int mTargetUserId;
+
+    @Before
+    public void setup() throws Exception {
+    /*
+    TODO(b/194536236): Refactor setup code in multiuser nonui tests and create setup util API instead
+     */
+        UserInfo currentUser = mMultiUserHelper.getCurrentForegroundUserInfo();
+        if (currentUser.id != MultiUserConstants.DEFAULT_INITIAL_USER) {
+            SystemClock.sleep(MultiUserConstants.WAIT_FOR_IDLE_TIME_MS);
+            mMultiUserHelper.switchAndWaitForStable(
+                MultiUserConstants.DEFAULT_INITIAL_USER, MultiUserConstants.WAIT_FOR_IDLE_TIME_MS);
+        }
+        UserInfo targetUser = mMultiUserHelper
+            .getUserByName(MultiUserConstants.SECONDARY_USER_NAME);
+        if (targetUser != null) {
+            if (!mMultiUserHelper.removeUser(targetUser)) {
+                throw new Exception("Failed to remove user: " + targetUser.id);
+            }
+        }
+        if (!MultiUserConstants.INCLUDE_CREATION_TIME) {
+            mTargetUserId = mMultiUserHelper
+                .createUser(MultiUserConstants.SECONDARY_USER_NAME, false);
+        }
+    }
+
+    @Test
+    public void testSwitch() throws Exception {
+        if (MultiUserConstants.INCLUDE_CREATION_TIME) {
+            mTargetUserId = mMultiUserHelper
+                .createUser(MultiUserConstants.SECONDARY_USER_NAME, false);
+        }
+        mMultiUserHelper.switchToUserId(mTargetUserId);
+    }
+}
diff --git a/tests/automotive/health/multiuser/tests/Android.bp b/tests/automotive/health/multiuser/tests/Android.bp
new file mode 100644
index 0000000..c5ea95c
--- /dev/null
+++ b/tests/automotive/health/multiuser/tests/Android.bp
@@ -0,0 +1,47 @@
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "AndroidAutomotiveMultiuserScenarioTests",
+    min_sdk_version: "24",
+    static_libs: [
+        "androidx.test.runner",
+        "androidx.test.rules",
+        "automotive-multiuser-scenarios",
+        "automotive-settings-scenarios",
+        "automotive-utility-helper",
+        "automotive-health-test-rules",
+        "automotive-settings-app-helper",
+        "app-helpers-common-interfaces",
+        "app-helpers-auto-interfaces",
+        "ub-uiautomator",
+        "collector-device-lib-platform",
+        "common-platform-scenarios",
+        "common-platform-scenario-tests",
+        "microbenchmark-device-lib",
+        "platform-test-rules",
+        "platform-test-options",
+        "multi-user-helper",
+        "microbenchmark-device-lib",
+        "longevity-device-lib",
+    ],
+    srcs: ["src/**/*.java"],
+    certificate: "platform",
+    test_suites: ["catbox"],
+    privileged: true,
+}
diff --git a/tests/automotive/health/multiuser/tests/AndroidManifest.xml b/tests/automotive/health/multiuser/tests/AndroidManifest.xml
new file mode 100644
index 0000000..99c46ba
--- /dev/null
+++ b/tests/automotive/health/multiuser/tests/AndroidManifest.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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="android.platform.scenario.multiuser" >
+  <uses-sdk android:minSdkVersion="24" android:targetSdkVersion="24" />
+  <uses-permission android:name="android.permission.DUMP" />
+  <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS"/>
+  <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.Manifest.permission.MANAGE_USERS" />
+  <uses-permission android:name="android.permission.MANAGE_USERS" />
+  <uses-permission android:name="android.permission.CREATE_USERS" />
+  <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
+  <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
+
+  <application>
+    <uses-library android:name="android.test.runner"/>
+  </application>
+  <instrumentation
+      android:name="androidx.test.runner.AndroidJUnitRunner"
+      android:targetPackage="android.platform.scenario.multiuser"
+      android:label="Android Automotive Multiuser Scenario-Based Tests" />
+</manifest>
\ No newline at end of file
diff --git a/tests/automotive/health/multiuser/tests/src/android/platform/scenario/multiuser/nonui/SwitchToExistingSecondaryUserBenchmark.java b/tests/automotive/health/multiuser/tests/src/android/platform/scenario/multiuser/nonui/SwitchToExistingSecondaryUserBenchmark.java
new file mode 100644
index 0000000..d782084
--- /dev/null
+++ b/tests/automotive/health/multiuser/tests/src/android/platform/scenario/multiuser/nonui/SwitchToExistingSecondaryUserBenchmark.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2021 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.scenario.multiuser;
+
+import android.platform.test.scenario.SleepAtTestFinishRule;
+import android.platform.test.microbenchmark.Microbenchmark;
+import android.platform.test.rule.StopwatchRule;
+import org.junit.Rule;
+import org.junit.runner.RunWith;
+
+@RunWith(Microbenchmark.class)
+public class SwitchToExistingSecondaryUserBenchmark extends SwitchToExistingSecondaryUser {
+
+  @Rule
+  public SleepAtTestFinishRule sleepRule =
+      new SleepAtTestFinishRule(MultiUserConstants.WAIT_FOR_IDLE_TIME_MS);
+
+  @Microbenchmark.TightMethodRule public StopwatchRule stopwatchRule = new StopwatchRule();
+}
diff --git a/tests/automotive/health/multiuser/tests/src/android/platform/scenario/multiuser/nonui/SwitchToNewGuestBenchmark.java b/tests/automotive/health/multiuser/tests/src/android/platform/scenario/multiuser/nonui/SwitchToNewGuestBenchmark.java
new file mode 100644
index 0000000..6f6fb2e
--- /dev/null
+++ b/tests/automotive/health/multiuser/tests/src/android/platform/scenario/multiuser/nonui/SwitchToNewGuestBenchmark.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2021 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.scenario.multiuser;
+
+import android.platform.test.scenario.SleepAtTestFinishRule;
+import android.platform.test.microbenchmark.Microbenchmark;
+import android.platform.test.microbenchmark.Microbenchmark.TightMethodRule;
+import android.platform.test.rule.StopwatchRule;
+import org.junit.Rule;
+import org.junit.runner.RunWith;
+
+@RunWith(Microbenchmark.class)
+public class SwitchToNewGuestBenchmark extends SwitchToNewGuest {
+
+  @Rule
+  public SleepAtTestFinishRule sleepRule =
+      new SleepAtTestFinishRule(MultiUserConstants.WAIT_FOR_IDLE_TIME_MS);
+
+  @TightMethodRule public StopwatchRule stopwatchRule = new StopwatchRule();
+}
diff --git a/tests/automotive/health/multiuser/tests/src/android/platform/scenario/multiuser/nonui/SwitchToNewSecondaryUserBenchmark.java b/tests/automotive/health/multiuser/tests/src/android/platform/scenario/multiuser/nonui/SwitchToNewSecondaryUserBenchmark.java
new file mode 100644
index 0000000..7ce42b6
--- /dev/null
+++ b/tests/automotive/health/multiuser/tests/src/android/platform/scenario/multiuser/nonui/SwitchToNewSecondaryUserBenchmark.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2021 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.scenario.multiuser;
+
+import android.platform.test.microbenchmark.Microbenchmark;
+import org.junit.runner.RunWith;
+
+@RunWith(Microbenchmark.class)
+public class SwitchToNewSecondaryUserBenchmark extends SwitchToNewSecondaryUser {}
diff --git a/tests/automotive/health/multiuser/tests/src/android/platform/scenario/multiuser/ui/AddUserQuickSettings.java b/tests/automotive/health/multiuser/tests/src/android/platform/scenario/multiuser/ui/AddUserQuickSettings.java
new file mode 100644
index 0000000..a6505a4
--- /dev/null
+++ b/tests/automotive/health/multiuser/tests/src/android/platform/scenario/multiuser/ui/AddUserQuickSettings.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2021 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.scenario.multiuser;
+
+import static junit.framework.Assert.assertTrue;
+
+import android.content.pm.UserInfo;
+import android.platform.helpers.AutoUtility;
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoProfileHelper;
+import android.platform.helpers.IAutoSettingHelper;
+import android.platform.helpers.MultiUserHelper;
+import androidx.test.runner.AndroidJUnit4;
+import org.junit.After;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * This test will create user through API and delete the same user from UI
+ *
+ * <p>It should be running under user 0, otherwise instrumentation may be killed after user
+ * switched.
+ */
+@RunWith(AndroidJUnit4.class)
+public class AddUserQuickSettings {
+
+    private final MultiUserHelper mMultiUserHelper = MultiUserHelper.getInstance();
+    private HelperAccessor<IAutoProfileHelper> mProfilesHelper;
+    private HelperAccessor<IAutoSettingHelper> mSettingHelper;
+
+    public AddUserQuickSettings() {
+        mProfilesHelper = new HelperAccessor<>(IAutoProfileHelper.class);
+        mSettingHelper = new HelperAccessor<>(IAutoSettingHelper.class);
+    }
+
+    @BeforeClass
+    public static void exitSuw() {
+        AutoUtility.exitSuw();
+    }
+
+    @After
+    public void goBackToHomeScreen() {
+        mSettingHelper.get().goBackToSettingsScreen();
+    }
+
+    @Test
+    public void testAddUser() throws Exception {
+        // create new user quick settings
+        UserInfo initialUser = mMultiUserHelper.getCurrentForegroundUserInfo();
+        mProfilesHelper.get().addProfileQuickSettings(initialUser.name);
+        // switched to new user
+        UserInfo newUser = mMultiUserHelper.getCurrentForegroundUserInfo();
+        // switch from new user to initial user
+        mProfilesHelper.get().switchProfile(newUser.name, initialUser.name);
+        // verify new user is seen in list of users
+        assertTrue(mMultiUserHelper.getUserByName(newUser.name) != null);
+        // remove new user
+        mMultiUserHelper.removeUser(newUser);
+    }
+}
diff --git a/tests/automotive/health/multiuser/tests/src/android/platform/scenario/multiuser/ui/AddUserSettings.java b/tests/automotive/health/multiuser/tests/src/android/platform/scenario/multiuser/ui/AddUserSettings.java
new file mode 100644
index 0000000..bbb75b2
--- /dev/null
+++ b/tests/automotive/health/multiuser/tests/src/android/platform/scenario/multiuser/ui/AddUserSettings.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2021 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.scenario.multiuser;
+
+import static junit.framework.Assert.assertTrue;
+
+import android.content.pm.UserInfo;
+import android.platform.helpers.AutoConfigConstants;
+import android.platform.helpers.AutoUtility;
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoProfileHelper;
+import android.platform.helpers.IAutoSettingHelper;
+import android.platform.helpers.MultiUserHelper;
+import androidx.test.runner.AndroidJUnit4;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * This test will create user through API and delete the same user from UI
+ *
+ * <p>It should be running under user 0, otherwise instrumentation may be killed after user
+ * switched.
+ */
+@RunWith(AndroidJUnit4.class)
+public class AddUserSettings {
+
+    private final MultiUserHelper mMultiUserHelper = MultiUserHelper.getInstance();
+    private HelperAccessor<IAutoProfileHelper> mProfilesHelper;
+    private HelperAccessor<IAutoSettingHelper> mSettingHelper;
+
+    public AddUserSettings() {
+        mProfilesHelper = new HelperAccessor<>(IAutoProfileHelper.class);
+        mSettingHelper = new HelperAccessor<>(IAutoSettingHelper.class);
+    }
+
+    @BeforeClass
+    public static void exitSuw() {
+        AutoUtility.exitSuw();
+    }
+
+    @Before
+    public void openAccountsFacet() {
+        mSettingHelper.get().openSetting(AutoConfigConstants.PROFILE_ACCOUNT_SETTINGS);
+    }
+
+    @After
+    public void goBackToHomeScreen() {
+        mSettingHelper.get().goBackToSettingsScreen();
+    }
+
+    @Test
+    public void testAddUser() throws Exception {
+        // create new user
+        UserInfo initialUser = mMultiUserHelper.getCurrentForegroundUserInfo();
+        mProfilesHelper.get().addProfile();
+        // switched to new user
+        UserInfo newUser = mMultiUserHelper.getCurrentForegroundUserInfo();
+        // switch from new user to initial user
+        mProfilesHelper.get().switchProfile(newUser.name, initialUser.name);
+        // verify new user is seen in list of users
+        assertTrue(mMultiUserHelper.getUserByName(newUser.name) != null);
+        // remove new user
+        mMultiUserHelper.removeUser(newUser);
+    }
+}
diff --git a/tests/automotive/health/multiuser/tests/src/android/platform/scenario/multiuser/ui/DeleteAdminUser.java b/tests/automotive/health/multiuser/tests/src/android/platform/scenario/multiuser/ui/DeleteAdminUser.java
new file mode 100644
index 0000000..0b48dd3
--- /dev/null
+++ b/tests/automotive/health/multiuser/tests/src/android/platform/scenario/multiuser/ui/DeleteAdminUser.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2021 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.scenario.multiuser;
+
+import static junit.framework.Assert.assertFalse;
+
+import android.os.SystemClock;
+import android.platform.helpers.AutoConfigConstants;
+import android.platform.helpers.AutoUtility;
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoProfileHelper;
+import android.platform.helpers.IAutoSettingHelper;
+import android.platform.helpers.MultiUserHelper;
+import androidx.test.runner.AndroidJUnit4;
+import org.junit.After;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * This test will create user through API and delete the same user from UI
+ *
+ * <p>It should be running under user 0, otherwise instrumentation may be killed after user
+ * switched.
+ */
+@RunWith(AndroidJUnit4.class)
+public class DeleteAdminUser {
+
+    private static final String userName = MultiUserConstants.SECONDARY_USER_NAME;
+    private static final int WAIT_TIME = 10000;
+    private final MultiUserHelper mMultiUserHelper = MultiUserHelper.getInstance();
+    private HelperAccessor<IAutoProfileHelper> mProfilesHelper;
+    private HelperAccessor<IAutoSettingHelper> mSettingHelper;
+    private int mTargetUserId;
+
+    public DeleteAdminUser() {
+        mProfilesHelper = new HelperAccessor<>(IAutoProfileHelper.class);
+        mSettingHelper = new HelperAccessor<>(IAutoSettingHelper.class);
+    }
+
+    @BeforeClass
+    public static void exitSuw() {
+        AutoUtility.exitSuw();
+    }
+
+    @After
+    public void goBackToHomeScreen() {
+        mSettingHelper.get().goBackToSettingsScreen();
+    }
+
+    @Test
+    public void testRemoveUser() throws Exception {
+        // create new user
+        mTargetUserId = mMultiUserHelper.createUser(userName, false);
+        SystemClock.sleep(WAIT_TIME);
+        // make the new user admin and delete new user
+        mSettingHelper.get().openSetting(AutoConfigConstants.PROFILE_ACCOUNT_SETTINGS);
+        mProfilesHelper.get().makeUserAdmin(userName);
+        mSettingHelper.get().openSetting(AutoConfigConstants.PROFILE_ACCOUNT_SETTINGS);
+        mProfilesHelper.get().deleteProfile(userName);
+        // verify new user was deleted
+        assertFalse(mProfilesHelper.get().isProfilePresent(userName));
+    }
+}
diff --git a/tests/automotive/health/multiuser/tests/src/android/platform/scenario/multiuser/ui/DeleteCurrentLastAdminUser.java b/tests/automotive/health/multiuser/tests/src/android/platform/scenario/multiuser/ui/DeleteCurrentLastAdminUser.java
new file mode 100644
index 0000000..41affbe
--- /dev/null
+++ b/tests/automotive/health/multiuser/tests/src/android/platform/scenario/multiuser/ui/DeleteCurrentLastAdminUser.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2021 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.scenario.multiuser;
+
+import static junit.framework.Assert.assertTrue;
+
+import android.content.pm.UserInfo;
+import android.platform.helpers.AutoConfigConstants;
+import android.platform.helpers.AutoUtility;
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoProfileHelper;
+import android.platform.helpers.IAutoSettingHelper;
+import android.platform.helpers.MultiUserHelper;
+import androidx.test.runner.AndroidJUnit4;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * This test will create user through API and delete the same user from UI
+ *
+ * <p>It should be running under user 0, otherwise instrumentation may be killed after user
+ * switched.
+ */
+@RunWith(AndroidJUnit4.class)
+public class DeleteCurrentLastAdminUser {
+
+    private final MultiUserHelper mMultiUserHelper = MultiUserHelper.getInstance();
+    private HelperAccessor<IAutoProfileHelper> mProfilesHelper;
+    private HelperAccessor<IAutoSettingHelper> mSettingHelper;
+
+    public DeleteCurrentLastAdminUser() {
+        mProfilesHelper = new HelperAccessor<>(IAutoProfileHelper.class);
+        mSettingHelper = new HelperAccessor<>(IAutoSettingHelper.class);
+    }
+
+    @BeforeClass
+    public static void exitSuw() {
+        AutoUtility.exitSuw();
+    }
+
+    @Before
+    public void openAccountsFacet() {
+        mSettingHelper.get().openSetting(AutoConfigConstants.PROFILE_ACCOUNT_SETTINGS);
+    }
+
+    @After
+    public void goBackToHomeScreen() {
+        mSettingHelper.get().goBackToSettingsScreen();
+    }
+
+    @Test
+    public void testRemoveUserSelf() throws Exception {
+        // add new user
+        UserInfo initialUser = mMultiUserHelper.getCurrentForegroundUserInfo();
+        // user deleted self
+        mProfilesHelper.get().deleteCurrentProfile();
+        UserInfo newUser = mMultiUserHelper.getCurrentForegroundUserInfo();
+        // verify that user is deleted
+        assertTrue((initialUser.id != newUser.id) && (initialUser.name.equals(newUser.name)));
+    }
+}
diff --git a/tests/automotive/health/multiuser/tests/src/android/platform/scenario/multiuser/ui/DeleteCurrentNonAdminUser.java b/tests/automotive/health/multiuser/tests/src/android/platform/scenario/multiuser/ui/DeleteCurrentNonAdminUser.java
new file mode 100644
index 0000000..a359cc2
--- /dev/null
+++ b/tests/automotive/health/multiuser/tests/src/android/platform/scenario/multiuser/ui/DeleteCurrentNonAdminUser.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2021 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.scenario.multiuser;
+
+import static junit.framework.Assert.assertTrue;
+
+import android.content.pm.UserInfo;
+import android.platform.helpers.AutoConfigConstants;
+import android.platform.helpers.AutoUtility;
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoProfileHelper;
+import android.platform.helpers.IAutoSettingHelper;
+import android.platform.helpers.MultiUserHelper;
+import androidx.test.runner.AndroidJUnit4;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * This test will create user through API and delete the same user from UI
+ *
+ * <p>It should be running under user 0, otherwise instrumentation may be killed after user
+ * switched.
+ */
+@RunWith(AndroidJUnit4.class)
+public class DeleteCurrentNonAdminUser {
+
+    private final MultiUserHelper mMultiUserHelper = MultiUserHelper.getInstance();
+    private HelperAccessor<IAutoProfileHelper> mProfilesHelper;
+    private HelperAccessor<IAutoSettingHelper> mSettingHelper;
+
+    public DeleteCurrentNonAdminUser() {
+        mProfilesHelper = new HelperAccessor<>(IAutoProfileHelper.class);
+        mSettingHelper = new HelperAccessor<>(IAutoSettingHelper.class);
+    }
+
+    @BeforeClass
+    public static void exitSuw() {
+        AutoUtility.exitSuw();
+    }
+
+    @Before
+    public void openAccountsFacet() {
+        mSettingHelper.get().openSetting(AutoConfigConstants.PROFILE_ACCOUNT_SETTINGS);
+    }
+
+    @After
+    public void goBackToHomeScreen() {
+        mSettingHelper.get().goBackToSettingsScreen();
+    }
+
+    @Test
+    public void testRemoveUserSelf() throws Exception {
+        // add new user
+        UserInfo initialUser = mMultiUserHelper.getCurrentForegroundUserInfo();
+        mProfilesHelper.get().addProfile();
+        // switched to new user and user deleted self
+        UserInfo newUser = mMultiUserHelper.getCurrentForegroundUserInfo();
+        mSettingHelper.get().openSetting(AutoConfigConstants.PROFILE_ACCOUNT_SETTINGS);
+        mProfilesHelper.get().deleteCurrentProfile();
+        // goes to guest user, switch back to initial user
+        UserInfo guestUser = mMultiUserHelper.getCurrentForegroundUserInfo();
+        mProfilesHelper.get().switchProfile(guestUser.name, initialUser.name);
+        // verify that user is deleted
+        assertTrue(mMultiUserHelper.getUserByName(newUser.name) == null);
+    }
+}
diff --git a/tests/automotive/health/multiuser/tests/src/android/platform/scenario/multiuser/ui/DeleteGuestNotAllowed.java b/tests/automotive/health/multiuser/tests/src/android/platform/scenario/multiuser/ui/DeleteGuestNotAllowed.java
new file mode 100644
index 0000000..03ecfbc
--- /dev/null
+++ b/tests/automotive/health/multiuser/tests/src/android/platform/scenario/multiuser/ui/DeleteGuestNotAllowed.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2021 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.scenario.multiuser;
+
+import static junit.framework.Assert.assertFalse;
+
+import android.content.pm.UserInfo;
+import android.platform.helpers.AutoConfigConstants;
+import android.platform.helpers.AutoUtility;
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoProfileHelper;
+import android.platform.helpers.IAutoSettingHelper;
+import android.platform.helpers.MultiUserHelper;
+import androidx.test.runner.AndroidJUnit4;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * This test will create user through API and delete the same user from UI
+ *
+ * <p>It should be running under user 0, otherwise instrumentation may be killed after user
+ * switched.
+ */
+@RunWith(AndroidJUnit4.class)
+public class DeleteGuestNotAllowed {
+
+    private static final String guestUser = MultiUserConstants.GUEST_NAME;
+    private HelperAccessor<IAutoProfileHelper> mProfilesHelper;
+    private HelperAccessor<IAutoSettingHelper> mSettingHelper;
+
+    public DeleteGuestNotAllowed() {
+        mProfilesHelper = new HelperAccessor<>(IAutoProfileHelper.class);
+        mSettingHelper = new HelperAccessor<>(IAutoSettingHelper.class);
+    }
+
+    @BeforeClass
+    public static void exitSuw() {
+        AutoUtility.exitSuw();
+    }
+
+    @Before
+    public void openAccountsFacet() {
+        mSettingHelper.get().openSetting(AutoConfigConstants.PROFILE_ACCOUNT_SETTINGS);
+    }
+
+    @After
+    public void goBackToHomeScreen() {
+        mSettingHelper.get().goBackToSettingsScreen();
+    }
+
+    @Test
+    public void testDeleteGuestNotAllowed() throws Exception {
+        // verify that guest user cannot be seen and deleted from list of profiles
+        assertFalse(mProfilesHelper.get().isProfilePresent(guestUser));
+    }
+}
diff --git a/tests/automotive/health/multiuser/tests/src/android/platform/scenario/multiuser/ui/DeleteGuestSelfNotAllowed.java b/tests/automotive/health/multiuser/tests/src/android/platform/scenario/multiuser/ui/DeleteGuestSelfNotAllowed.java
new file mode 100644
index 0000000..5413fda
--- /dev/null
+++ b/tests/automotive/health/multiuser/tests/src/android/platform/scenario/multiuser/ui/DeleteGuestSelfNotAllowed.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2021 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.scenario.multiuser;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import android.content.pm.UserInfo;
+import android.platform.helpers.AutoConfigConstants;
+import android.platform.helpers.AutoUtility;
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoProfileHelper;
+import android.platform.helpers.IAutoSettingHelper;
+import android.platform.helpers.MultiUserHelper;
+import android.util.Log;
+import androidx.test.runner.AndroidJUnit4;
+import org.junit.After;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * This test will create user through API and delete the same user from UI
+ *
+ * <p>It should be running under user 0, otherwise instrumentation may be killed after user
+ * switched.
+ */
+@RunWith(AndroidJUnit4.class)
+public class DeleteGuestSelfNotAllowed {
+
+    private static final String guestUser = MultiUserConstants.GUEST_NAME;
+    private final MultiUserHelper mMultiUserHelper = MultiUserHelper.getInstance();
+    private HelperAccessor<IAutoProfileHelper> mProfilesHelper;
+    private HelperAccessor<IAutoSettingHelper> mSettingHelper;
+
+    public DeleteGuestSelfNotAllowed() {
+        mProfilesHelper = new HelperAccessor<>(IAutoProfileHelper.class);
+        mSettingHelper = new HelperAccessor<>(IAutoSettingHelper.class);
+    }
+
+    @BeforeClass
+    public static void exitSuw() {
+        AutoUtility.exitSuw();
+    }
+
+    @After
+    public void goBackToHomeScreen() {
+        mSettingHelper.get().goBackToSettingsScreen();
+    }
+
+    @Test
+    public void testDeleteGuestNotAllowed() throws Exception {
+        UserInfo previousUser = mMultiUserHelper.getCurrentForegroundUserInfo();
+        // switch to Guest and verify the user switch
+        mProfilesHelper.get().switchProfile(previousUser.name, guestUser);
+        UserInfo currentUser = mMultiUserHelper.getCurrentForegroundUserInfo();
+        assertTrue(currentUser.name.equals(guestUser));
+        boolean IsDeleteAllowed = true;
+        // try to delete self - runtime exception encountered
+        try {
+            mSettingHelper.get().openSetting(AutoConfigConstants.PROFILE_ACCOUNT_SETTINGS);
+            mProfilesHelper.get().deleteCurrentProfile();
+        } catch (RuntimeException err) {
+            Log.v(
+                "DeleteGuestSelfNotAllowed",
+                String.format("Error caught while trying to delete Guest(Self) : %s ", err));
+            IsDeleteAllowed = false;
+        }
+        assertFalse(IsDeleteAllowed);
+        // switch to initial user before terminating the test
+        mProfilesHelper.get().switchProfile(currentUser.name, previousUser.name);
+    }
+}
diff --git a/tests/automotive/health/multiuser/tests/src/android/platform/scenario/multiuser/ui/DeleteNonAdminUser.java b/tests/automotive/health/multiuser/tests/src/android/platform/scenario/multiuser/ui/DeleteNonAdminUser.java
new file mode 100644
index 0000000..ddfc2cc
--- /dev/null
+++ b/tests/automotive/health/multiuser/tests/src/android/platform/scenario/multiuser/ui/DeleteNonAdminUser.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2021 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.scenario.multiuser;
+
+import static junit.framework.Assert.assertFalse;
+
+import android.os.SystemClock;
+import android.platform.helpers.AutoConfigConstants;
+import android.platform.helpers.AutoUtility;
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoProfileHelper;
+import android.platform.helpers.IAutoSettingHelper;
+import android.platform.helpers.MultiUserHelper;
+import androidx.test.runner.AndroidJUnit4;
+import org.junit.After;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * This test will create user through API and delete the same user from UI
+ *
+ * <p>It should be running under user 0, otherwise instrumentation may be killed after user
+ * switched.
+ */
+@RunWith(AndroidJUnit4.class)
+public class DeleteNonAdminUser {
+
+    private static final String userName = MultiUserConstants.SECONDARY_USER_NAME;
+    private static final int WAIT_TIME = 10000;
+    private final MultiUserHelper mMultiUserHelper = MultiUserHelper.getInstance();
+    private HelperAccessor<IAutoProfileHelper> mProfilesHelper;
+    private HelperAccessor<IAutoSettingHelper> mSettingHelper;
+    private int mTargetUserId;
+
+    public DeleteNonAdminUser() {
+        mProfilesHelper = new HelperAccessor<>(IAutoProfileHelper.class);
+        mSettingHelper = new HelperAccessor<>(IAutoSettingHelper.class);
+    }
+
+    @BeforeClass
+    public static void exitSuw() {
+        AutoUtility.exitSuw();
+    }
+
+    @After
+    public void goBackToHomeScreen() {
+        mSettingHelper.get().goBackToSettingsScreen();
+    }
+
+    @Test
+    public void testRemoveUser() throws Exception {
+        // create new user
+        mTargetUserId = mMultiUserHelper.createUser(userName, false);
+        SystemClock.sleep(WAIT_TIME);
+        // make the new user admin and delete new user
+        mSettingHelper.get().openSetting(AutoConfigConstants.PROFILE_ACCOUNT_SETTINGS);
+        mProfilesHelper.get().deleteProfile(userName);
+        // verify new user was deleted
+        assertFalse(mProfilesHelper.get().isProfilePresent(userName));
+    }
+}
diff --git a/tests/automotive/health/multiuser/tests/src/android/platform/scenario/multiuser/ui/SwitchToGuestFromNonAdmin.java b/tests/automotive/health/multiuser/tests/src/android/platform/scenario/multiuser/ui/SwitchToGuestFromNonAdmin.java
new file mode 100644
index 0000000..539be6d
--- /dev/null
+++ b/tests/automotive/health/multiuser/tests/src/android/platform/scenario/multiuser/ui/SwitchToGuestFromNonAdmin.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2021 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.scenario.multiuser;
+
+import static junit.framework.Assert.assertTrue;
+
+import android.content.pm.UserInfo;
+import android.platform.helpers.AutoConfigConstants;
+import android.platform.helpers.AutoUtility;
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoProfileHelper;
+import android.platform.helpers.IAutoSettingHelper;
+import android.platform.helpers.MultiUserHelper;
+import androidx.test.runner.AndroidJUnit4;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * This test will create user through API and delete the same user from UI
+ *
+ * <p>It should be running under user 0, otherwise instrumentation may be killed after user
+ * switched.
+ */
+@RunWith(AndroidJUnit4.class)
+public class SwitchToGuestFromNonAdmin {
+
+    private static final String guestUser = MultiUserConstants.GUEST_NAME;
+    private final MultiUserHelper mMultiUserHelper = MultiUserHelper.getInstance();
+    private HelperAccessor<IAutoProfileHelper> mProfilesHelper;
+    private HelperAccessor<IAutoSettingHelper> mSettingHelper;
+
+    public SwitchToGuestFromNonAdmin() {
+        mProfilesHelper = new HelperAccessor<>(IAutoProfileHelper.class);
+        mSettingHelper = new HelperAccessor<>(IAutoSettingHelper.class);
+    }
+
+    @BeforeClass
+    public static void exitSuw() {
+        AutoUtility.exitSuw();
+    }
+
+    @Before
+    public void openAccountsFacet() {
+        mSettingHelper.get().openSetting(AutoConfigConstants.PROFILE_ACCOUNT_SETTINGS);
+    }
+
+    @After
+    public void goBackToHomeScreen() {
+        mSettingHelper.get().goBackToSettingsScreen();
+    }
+
+    @Test
+    public void testSwitchToGuest() throws Exception {
+        // add new user
+        UserInfo initialUser = mMultiUserHelper.getCurrentForegroundUserInfo();
+        mProfilesHelper.get().addProfile();
+        // switched to new user account
+        UserInfo newUser = mMultiUserHelper.getCurrentForegroundUserInfo();
+        // switch to guest from new user
+        mProfilesHelper.get().switchProfile(newUser.name, guestUser);
+        // verify the user switch
+        UserInfo currentUser = mMultiUserHelper.getCurrentForegroundUserInfo();
+        assertTrue(currentUser.name.equals(guestUser));
+        // switch to initial user and delete new user before terminating the test
+        mProfilesHelper.get().switchProfile(currentUser.name, initialUser.name);
+        mMultiUserHelper.removeUser(newUser);
+    }
+}
diff --git a/tests/automotive/health/multiuser/tests/src/android/platform/scenario/multiuser/ui/SwitchUserQuickSettings.java b/tests/automotive/health/multiuser/tests/src/android/platform/scenario/multiuser/ui/SwitchUserQuickSettings.java
new file mode 100644
index 0000000..0dfdc3d
--- /dev/null
+++ b/tests/automotive/health/multiuser/tests/src/android/platform/scenario/multiuser/ui/SwitchUserQuickSettings.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2021 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.scenario.multiuser;
+
+import static junit.framework.Assert.assertTrue;
+
+import android.content.pm.UserInfo;
+import android.platform.helpers.AutoUtility;
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoProfileHelper;
+import android.platform.helpers.IAutoSettingHelper;
+import android.platform.helpers.MultiUserHelper;
+import androidx.test.runner.AndroidJUnit4;
+import org.junit.After;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * This test will create user through API and delete the same user from UI
+ *
+ * <p>It should be running under user 0, otherwise instrumentation may be killed after user
+ * switched.
+ */
+@RunWith(AndroidJUnit4.class)
+public class SwitchUserQuickSettings {
+
+    private static final String guestUser = MultiUserConstants.GUEST_NAME;
+    private final MultiUserHelper mMultiUserHelper = MultiUserHelper.getInstance();
+    private HelperAccessor<IAutoProfileHelper> mProfilesHelper;
+    private HelperAccessor<IAutoSettingHelper> mSettingHelper;
+
+    public SwitchUserQuickSettings() {
+        mProfilesHelper = new HelperAccessor<>(IAutoProfileHelper.class);
+        mSettingHelper = new HelperAccessor<>(IAutoSettingHelper.class);
+    }
+
+    @BeforeClass
+    public static void exitSuw() {
+        AutoUtility.exitSuw();
+    }
+
+    @After
+    public void goBackToHomeScreen() {
+        mSettingHelper.get().goBackToSettingsScreen();
+    }
+
+    @Test
+    public void testSwitchUser() throws Exception {
+        UserInfo previousUser = mMultiUserHelper.getCurrentForegroundUserInfo();
+        // switch to Guest
+        mProfilesHelper.get().switchProfile(previousUser.name, guestUser);
+        UserInfo currentUser = mMultiUserHelper.getCurrentForegroundUserInfo();
+        // verify the user switch
+        assertTrue(currentUser.name.equals(guestUser));
+        // switch to initial user before terminating the test
+        mProfilesHelper.get().switchProfile(currentUser.name, previousUser.name);
+    }
+}
diff --git a/tests/automotive/health/notification/Android.bp b/tests/automotive/health/notification/Android.bp
new file mode 100644
index 0000000..3acfb21
--- /dev/null
+++ b/tests/automotive/health/notification/Android.bp
@@ -0,0 +1,32 @@
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library_static {
+    name: "automotive-notification-scenarios",
+    min_sdk_version: "24",
+    srcs: [ "src/**/*.java" ],
+    libs: [
+        "androidx.test.runner",
+        "automotive-settings-app-helper",
+        "app-helpers-auto-interfaces",
+        "common-platform-scenarios",
+        "platform-test-options",
+        "platform-test-rules",
+        "ub-uiautomator",
+    ],
+}
diff --git a/tests/automotive/health/notification/src/android/platform/scenario/notification/Scroll.java b/tests/automotive/health/notification/src/android/platform/scenario/notification/Scroll.java
new file mode 100644
index 0000000..9c98602
--- /dev/null
+++ b/tests/automotive/health/notification/src/android/platform/scenario/notification/Scroll.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2021 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.scenario.notification;
+
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoNotificationHelper;
+import android.platform.test.scenario.annotation.Scenario;
+import android.platform.test.rule.NotificationPressureRule;
+
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Scroll down and up in Notification app. */
+@Scenario
+@RunWith(JUnit4.class)
+public class Scroll {
+    static HelperAccessor<IAutoNotificationHelper> sHelper =
+            new HelperAccessor<>(IAutoNotificationHelper.class);
+
+    // Populate notification before scrolling.
+    @ClassRule
+    public static NotificationPressureRule NotificationPressureRule =
+            new NotificationPressureRule(5);
+
+    @Test
+    public void testScrollUpAndDown() {
+        sHelper.get().scrollDownOnePage(500);
+        sHelper.get().scrollUpOnePage(500);
+    }
+}
diff --git a/tests/automotive/health/notification/tests/Android.bp b/tests/automotive/health/notification/tests/Android.bp
new file mode 100644
index 0000000..8c7f266
--- /dev/null
+++ b/tests/automotive/health/notification/tests/Android.bp
@@ -0,0 +1,38 @@
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "AndroidAutomotiveNotificationScenarioTests",
+    min_sdk_version: "24",
+    static_libs: [
+        "androidx.test.runner",
+        "androidx.test.rules",
+        "automotive-notification-scenarios",
+        "automotive-health-test-rules",
+        "automotive-settings-app-helper",
+        "app-helpers-auto-interfaces",
+        "ub-uiautomator",
+        "collector-device-lib-platform",
+        "common-platform-scenarios",
+        "common-platform-scenario-tests",
+        "microbenchmark-device-lib",
+        "platform-test-options",
+    ],
+    srcs: ["src/**/*.java"],
+    test_suites: ["catbox"],
+}
diff --git a/tests/automotive/health/notification/tests/AndroidManifest.xml b/tests/automotive/health/notification/tests/AndroidManifest.xml
new file mode 100644
index 0000000..32ef653
--- /dev/null
+++ b/tests/automotive/health/notification/tests/AndroidManifest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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="android.platform.test.scenario.notification" >
+  <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.MANAGE_USERS" />
+  <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
+  <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
+
+  <application>
+    <uses-library android:name="android.test.runner"/>
+  </application>
+  <instrumentation
+      android:name="androidx.test.runner.AndroidJUnitRunner"
+      android:targetPackage="android.platform.test.scenario.notification"
+      android:label="Android Automotive Notification Scenario-Based Tests" />
+</manifest>
\ No newline at end of file
diff --git a/tests/automotive/health/notification/tests/src/android/platform/scenario/notification/ScrollMicrobenchmark.java b/tests/automotive/health/notification/tests/src/android/platform/scenario/notification/ScrollMicrobenchmark.java
new file mode 100644
index 0000000..94124c8
--- /dev/null
+++ b/tests/automotive/health/notification/tests/src/android/platform/scenario/notification/ScrollMicrobenchmark.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2021 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.scenario.notification;
+
+import android.platform.test.microbenchmark.Microbenchmark;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.runner.RunWith;
+
+@RunWith(Microbenchmark.class)
+public class ScrollMicrobenchmark extends Scroll {
+    @BeforeClass
+    public static void openApp() {
+        sHelper.get().open();
+    }
+
+    @AfterClass
+    public static void closeApp() {
+        sHelper.get().exit();
+    }
+}
diff --git a/tests/automotive/health/radio/Android.bp b/tests/automotive/health/radio/Android.bp
new file mode 100644
index 0000000..89e7987
--- /dev/null
+++ b/tests/automotive/health/radio/Android.bp
@@ -0,0 +1,32 @@
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library_static {
+    name: "automotive-radio-scenarios",
+    min_sdk_version: "24",
+    srcs: [ "src/**/*.java" ],
+    libs: [
+        "androidx.test.runner",
+        "automotive-settings-app-helper",
+        "app-helpers-auto-interfaces",
+        "common-platform-scenarios",
+        "platform-test-options",
+        "platform-test-rules",
+        "ub-uiautomator",
+    ],
+}
diff --git a/tests/automotive/health/radio/src/android/platform/scenario/radio/OpenApp.java b/tests/automotive/health/radio/src/android/platform/scenario/radio/OpenApp.java
new file mode 100644
index 0000000..84f184a
--- /dev/null
+++ b/tests/automotive/health/radio/src/android/platform/scenario/radio/OpenApp.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2021 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.scenario.radio;
+
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoRadioHelper;
+import android.platform.test.scenario.annotation.Scenario;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Opens the Radio application and exits after. */
+@Scenario
+@RunWith(JUnit4.class)
+public class OpenApp {
+    static HelperAccessor<IAutoRadioHelper> sHelper = new HelperAccessor<>(IAutoRadioHelper.class);
+
+    @Test
+    public void testOpen() {
+        sHelper.get().open();
+    }
+}
diff --git a/tests/automotive/health/radio/src/android/platform/scenario/radio/TuneAndPlay.java b/tests/automotive/health/radio/src/android/platform/scenario/radio/TuneAndPlay.java
new file mode 100644
index 0000000..7bf8138
--- /dev/null
+++ b/tests/automotive/health/radio/src/android/platform/scenario/radio/TuneAndPlay.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2021 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.scenario.radio;
+
+import static org.junit.Assume.assumeTrue;
+
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoRadioHelper;
+import android.platform.test.scenario.annotation.Scenario;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.Until;
+import androidx.test.InstrumentationRegistry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** This scenario will tune and play an FM station, assuming radio app is open. */
+@Scenario
+@RunWith(JUnit4.class)
+public class TuneAndPlay {
+
+    private static final long OPEN_RADIO_TIMEOUT_MS = 10000;
+    private static HelperAccessor<IAutoRadioHelper> sHelper =
+            new HelperAccessor<>(IAutoRadioHelper.class);
+
+    @Before
+    public void checkApp() {
+        // TODO: Remove the checking logic and use default helper open() method after migrating to Q
+        UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        String pkgName = sHelper.get().getPackage();
+        assumeTrue(device.wait(Until.hasObject(By.pkg(pkgName).depth(0)), OPEN_RADIO_TIMEOUT_MS));
+    }
+
+    @Test
+    public void testTuneAndPlay() {
+        sHelper.get().setStation("FM", 92.3);
+        sHelper.get().playRadio();
+    }
+}
diff --git a/tests/automotive/health/radio/tests/Android.bp b/tests/automotive/health/radio/tests/Android.bp
new file mode 100644
index 0000000..668017d
--- /dev/null
+++ b/tests/automotive/health/radio/tests/Android.bp
@@ -0,0 +1,38 @@
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "AndroidAutomotiveRadioScenarioTests",
+    min_sdk_version: "24",
+    static_libs: [
+        "androidx.test.runner",
+        "androidx.test.rules",
+        "automotive-radio-scenarios",
+        "automotive-health-test-rules",
+        "automotive-settings-app-helper",
+        "app-helpers-auto-interfaces",
+        "ub-uiautomator",
+        "collector-device-lib-platform",
+        "common-platform-scenarios",
+        "common-platform-scenario-tests",
+        "microbenchmark-device-lib",
+        "platform-test-options",
+    ],
+    srcs: ["src/**/*.java"],
+    test_suites: ["catbox"],
+}
diff --git a/tests/automotive/health/radio/tests/AndroidManifest.xml b/tests/automotive/health/radio/tests/AndroidManifest.xml
new file mode 100644
index 0000000..3dcbfdb
--- /dev/null
+++ b/tests/automotive/health/radio/tests/AndroidManifest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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="android.platform.test.scenario.radio" >
+  <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.MANAGE_USERS" />
+  <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
+  <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
+
+  <application>
+    <uses-library android:name="android.test.runner"/>
+  </application>
+  <instrumentation
+      android:name="androidx.test.runner.AndroidJUnitRunner"
+      android:targetPackage="android.platform.test.scenario.radio"
+      android:label="Android Automotive Radio Scenario-Based Tests" />
+</manifest>
\ No newline at end of file
diff --git a/tests/automotive/health/radio/tests/src/android/platform/scenario/radio/OpenAppMicrobenchmark.java b/tests/automotive/health/radio/tests/src/android/platform/scenario/radio/OpenAppMicrobenchmark.java
new file mode 100644
index 0000000..b00e071
--- /dev/null
+++ b/tests/automotive/health/radio/tests/src/android/platform/scenario/radio/OpenAppMicrobenchmark.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2021 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.scenario.radio;
+
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoGenericAppHelper;
+import android.platform.test.scenario.AppStartupRunRule;
+import android.platform.test.microbenchmark.Microbenchmark;
+
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.runner.RunWith;
+
+@RunWith(Microbenchmark.class)
+public class OpenAppMicrobenchmark extends OpenApp {
+    @Rule public AppStartupRunRule mAppStartupRunRule = new AppStartupRunRule<>(sHelper.get());
+
+    private static final String RADIO_PACKAGE = "com.android.car.radio";
+    private static final String MEDIA_TEMPLATE = "android.car.intent.action.MEDIA_TEMPLATE";
+    private static final Map<String, String> RADIO_SERVICE_EXTRA_ARGS =
+            Stream.of(
+                            new Object[][] {
+                                {
+                                    "android.car.intent.extra.MEDIA_COMPONENT",
+                                    "com.android.car.radio/com.android.car.radio.service.RadioAppService"
+                                },
+                            })
+                    .collect(Collectors.toMap(data -> (String) data[0], data -> (String) data[1]));
+
+    static HelperAccessor<IAutoGenericAppHelper> sRadioServiceHelper =
+            new HelperAccessor<>(IAutoGenericAppHelper.class);
+
+    static {
+        sRadioServiceHelper.get().setPackage(RADIO_PACKAGE);
+        sRadioServiceHelper.get().setLaunchAction(MEDIA_TEMPLATE, RADIO_SERVICE_EXTRA_ARGS);
+    }
+
+    @BeforeClass
+    public static void setUp() {
+        // Open radio via com.android.car.radio.service.RadioAppService once otherwise the
+        // foreground app could be stuck at some other media apps(e.g. Bluetooth).
+        sRadioServiceHelper.get().open();
+    }
+}
diff --git a/tests/automotive/health/rules/Android.bp b/tests/automotive/health/rules/Android.bp
new file mode 100644
index 0000000..98d529d
--- /dev/null
+++ b/tests/automotive/health/rules/Android.bp
@@ -0,0 +1,32 @@
+//
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+    name: "automotive-health-test-rules",
+    libs: [
+        "platform-test-rules",
+        "platform-test-options",
+    ],
+    static_libs: [
+        "androidx.test.rules",
+    ],
+    srcs: ["src/**/*.java"],
+    sdk_version: "test_current",
+}
diff --git a/tests/automotive/health/rules/src/android/platform/scenario/AppStartupRunRule.java b/tests/automotive/health/rules/src/android/platform/scenario/AppStartupRunRule.java
new file mode 100644
index 0000000..650aa7f
--- /dev/null
+++ b/tests/automotive/health/rules/src/android/platform/scenario/AppStartupRunRule.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2021 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.scenario;
+
+import android.platform.helpers.IAppHelper;
+import android.platform.test.rule.DropCachesRule;
+import android.platform.test.rule.KillAppsRule;
+import android.platform.test.rule.PressHomeRule;
+
+import org.junit.rules.TestRule;
+import org.junit.rules.RuleChain;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+/**
+ * This rule is intended to be used as @Rule, which runs for every test run for measuring app
+ * startup latency.
+ */
+public class AppStartupRunRule<T extends IAppHelper> implements TestRule {
+
+    private final IAppHelper mAppHelper;
+    private final RuleChain mRuleChain;
+
+    public AppStartupRunRule(T appHelper) {
+        mAppHelper = appHelper;
+
+        /**
+         * The following constraints on instrumentation arguments must hold.
+         * +------------+-------+-------+
+         * |  Action    |  Hot  |  Cold |
+         * +------------+-------+-------+
+         * | press-home | true  | false |
+         * +------------+-------+-------+
+         * |  kill-app  | false |  true |
+         * +------------+-------+-------+
+         * | drop-cache | false |  true |
+         * +------------+-------+-------+
+         */
+
+        // RuleChain for maintaining the order of evalution.
+        mRuleChain =
+                RuleChain
+                        // cold startup needs to kill the app and clear cache before every test run.
+                        .outerRule(new KillAppsRule(appHelper.getPackage()))
+                        .around(new DropCachesRule())
+                        // hot startup needs to press home to exit an app after every test run.
+                        .around(new PressHomeRule())
+                        // sleep to make sure that asynchrounous app launch and relevant metric
+                        // reporting by statsd happens before metrics listeners try to collect them.
+                        .around(new SleepAtTestFinishRule(3000));
+    }
+
+    public Statement apply(final Statement base, final Description description) {
+        return mRuleChain.apply(base, description);
+    }
+}
diff --git a/tests/automotive/health/rules/src/android/platform/scenario/SleepAtTestFinishRule.java b/tests/automotive/health/rules/src/android/platform/scenario/SleepAtTestFinishRule.java
new file mode 100644
index 0000000..ebead02
--- /dev/null
+++ b/tests/automotive/health/rules/src/android/platform/scenario/SleepAtTestFinishRule.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2021 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.scenario;
+
+import android.os.SystemClock;
+import android.platform.test.rule.TestWatcher;
+
+import org.junit.runner.Description;
+
+/** This rule will sleep for a given amount of time at the end of each test method. */
+public class SleepAtTestFinishRule extends TestWatcher {
+
+    private final long mMillis;
+
+    public SleepAtTestFinishRule(long millis) {
+        mMillis = millis;
+    }
+
+    @Override
+    protected void finished(Description description) {
+        SystemClock.sleep(mMillis);
+    }
+}
diff --git a/tests/automotive/health/settings/Android.bp b/tests/automotive/health/settings/Android.bp
new file mode 100644
index 0000000..e12673b
--- /dev/null
+++ b/tests/automotive/health/settings/Android.bp
@@ -0,0 +1,32 @@
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library_static {
+    name: "automotive-settings-scenarios",
+    min_sdk_version: "24",
+    srcs: [ "src/**/*.java" ],
+    libs: [
+        "androidx.test.runner",
+        "automotive-settings-app-helper",
+        "app-helpers-auto-interfaces",
+        "common-platform-scenarios",
+        "platform-test-options",
+        "platform-test-rules",
+        "ub-uiautomator",
+    ],
+}
diff --git a/tests/automotive/health/settings/src/android/platform/scenario/settings/OpenApp.java b/tests/automotive/health/settings/src/android/platform/scenario/settings/OpenApp.java
new file mode 100644
index 0000000..19b86a1
--- /dev/null
+++ b/tests/automotive/health/settings/src/android/platform/scenario/settings/OpenApp.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2021 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.scenario.settings;
+
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoSettingHelper;
+import android.platform.test.scenario.annotation.Scenario;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Opens the Settings application and exits after. */
+@Scenario
+@RunWith(JUnit4.class)
+public class OpenApp {
+    static HelperAccessor<IAutoSettingHelper> sHelper =
+            new HelperAccessor<>(IAutoSettingHelper.class);
+
+    @Test
+    public void testOpen() {
+        sHelper.get().open();
+    }
+}
diff --git a/tests/automotive/health/settings/src/android/platform/scenario/settings/ScrollInApp.java b/tests/automotive/health/settings/src/android/platform/scenario/settings/ScrollInApp.java
new file mode 100644
index 0000000..054e8b4
--- /dev/null
+++ b/tests/automotive/health/settings/src/android/platform/scenario/settings/ScrollInApp.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2021 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.scenario.settings;
+
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.IAutoSettingHelper;
+import android.platform.test.scenario.annotation.Scenario;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Scroll down and up in Settings. */
+@Scenario
+@RunWith(JUnit4.class)
+public class ScrollInApp {
+    static HelperAccessor<IAutoSettingHelper> sHelper =
+            new HelperAccessor<>(IAutoSettingHelper.class);
+
+    @Test
+    public void testScrollDownAndUp() {
+        sHelper.get().scrollDownOnePage(500);
+        sHelper.get().scrollUpOnePage(500);
+    }
+}
diff --git a/tests/automotive/health/settings/tests/Android.bp b/tests/automotive/health/settings/tests/Android.bp
new file mode 100644
index 0000000..0f102f5
--- /dev/null
+++ b/tests/automotive/health/settings/tests/Android.bp
@@ -0,0 +1,38 @@
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "AndroidAutomotiveSettingsScenarioTests",
+    min_sdk_version: "24",
+    static_libs: [
+        "androidx.test.runner",
+        "androidx.test.rules",
+        "automotive-settings-scenarios",
+        "automotive-health-test-rules",
+        "automotive-settings-app-helper",
+        "app-helpers-auto-interfaces",
+        "ub-uiautomator",
+        "collector-device-lib-platform",
+        "common-platform-scenarios",
+        "common-platform-scenario-tests",
+        "microbenchmark-device-lib",
+        "platform-test-options",
+    ],
+    srcs: ["src/**/*.java"],
+    test_suites: ["catbox"],
+}
diff --git a/tests/automotive/health/settings/tests/AndroidManifest.xml b/tests/automotive/health/settings/tests/AndroidManifest.xml
new file mode 100644
index 0000000..f06cdc4
--- /dev/null
+++ b/tests/automotive/health/settings/tests/AndroidManifest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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="android.platform.test.scenario.settings" >
+  <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.MANAGE_USERS" />
+  <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
+  <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
+
+  <application>
+    <uses-library android:name="android.test.runner"/>
+  </application>
+  <instrumentation
+      android:name="androidx.test.runner.AndroidJUnitRunner"
+      android:targetPackage="android.platform.test.scenario.settings"
+      android:label="Android Automotive Settings Scenario-Based Tests" />
+</manifest>
\ No newline at end of file
diff --git a/tests/automotive/health/settings/tests/src/android/platform/scenario/settings/OpenAppMicrobenchmark.java b/tests/automotive/health/settings/tests/src/android/platform/scenario/settings/OpenAppMicrobenchmark.java
new file mode 100644
index 0000000..4dbb9bb
--- /dev/null
+++ b/tests/automotive/health/settings/tests/src/android/platform/scenario/settings/OpenAppMicrobenchmark.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2021 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.scenario.settings;
+
+import android.platform.test.scenario.AppStartupRunRule;
+import android.platform.test.microbenchmark.Microbenchmark;
+
+import org.junit.Rule;
+import org.junit.runner.RunWith;
+
+@RunWith(Microbenchmark.class)
+public class OpenAppMicrobenchmark extends OpenApp {
+
+    @Rule public AppStartupRunRule mAppStartupRunRule = new AppStartupRunRule<>(sHelper.get());
+}
diff --git a/tests/automotive/health/settings/tests/src/android/platform/scenario/settings/ScrollInAppMicrobenchmark.java b/tests/automotive/health/settings/tests/src/android/platform/scenario/settings/ScrollInAppMicrobenchmark.java
new file mode 100644
index 0000000..dc55fab
--- /dev/null
+++ b/tests/automotive/health/settings/tests/src/android/platform/scenario/settings/ScrollInAppMicrobenchmark.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2021 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.scenario.settings;
+
+import android.platform.test.microbenchmark.Microbenchmark;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.runner.RunWith;
+
+@RunWith(Microbenchmark.class)
+public class ScrollInAppMicrobenchmark extends ScrollInApp {
+    @BeforeClass
+    public static void openApp() {
+        sHelper.get().open();
+    }
+
+    @AfterClass
+    public static void closeApp() {
+        sHelper.get().exit();
+    }
+}
diff --git a/tests/bootdoa/Android.mk b/tests/bootdoa/Android.mk
new file mode 100644
index 0000000..ff86b92
--- /dev/null
+++ b/tests/bootdoa/Android.mk
@@ -0,0 +1,17 @@
+#
+# Copyright (C) 2021 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.
+
+LOCAL_PATH:= $(call my-dir)
+$(call dist-for-goals, droidcore, $(LOCAL_PATH)/fatal_allowlist)
diff --git a/tests/bootdoa/fatal_allowlist b/tests/bootdoa/fatal_allowlist
new file mode 100644
index 0000000..2bb3e86
--- /dev/null
+++ b/tests/bootdoa/fatal_allowlist
@@ -0,0 +1,2 @@
+pcie\s*:\s*pcie_init.
+WMA\s-->\swmi_unified_attach\s-\ssuccess
diff --git a/tests/example/devcodelab/Android.bp b/tests/example/devcodelab/Android.bp
index a541a24..f7af9c6 100644
--- a/tests/example/devcodelab/Android.bp
+++ b/tests/example/devcodelab/Android.bp
@@ -22,6 +22,6 @@
     srcs: ["src/**/*.java"],
     sdk_version: "current",
 
-    static_libs: ["android-support-test"],
+    static_libs: ["androidx.test.rules"],
     certificate: "platform",
 }
diff --git a/tests/example/devcodelab/AndroidManifest.xml b/tests/example/devcodelab/AndroidManifest.xml
index a55e434..70569ec 100644
--- a/tests/example/devcodelab/AndroidManifest.xml
+++ b/tests/example/devcodelab/AndroidManifest.xml
@@ -23,7 +23,7 @@
         <uses-library android:name="android.test.runner" />
     </application>
 
-    <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
                      android:targetPackage="android.test.example.devcodelab"
                      android:label="Developer Codelab Test"/>
 
diff --git a/tests/example/instrumentation/src/android/test/example/helloworld/HelloWorldTest.java b/tests/example/instrumentation/src/android/test/example/helloworld/HelloWorldTest.java
index a22173d..42ceaac 100644
--- a/tests/example/instrumentation/src/android/test/example/helloworld/HelloWorldTest.java
+++ b/tests/example/instrumentation/src/android/test/example/helloworld/HelloWorldTest.java
@@ -16,9 +16,10 @@
 
 package android.test.example.helloworld;
 
-import androidx.test.filters.SmallTest;
 import android.util.Log;
 
+import androidx.test.filters.SmallTest;
+
 import org.junit.After;
 import org.junit.AfterClass;
 import org.junit.Assert;
diff --git a/tests/functional/applinktests/Android.bp b/tests/functional/applinktests/Android.bp
deleted file mode 100644
index e7a7306..0000000
--- a/tests/functional/applinktests/Android.bp
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright 2016 Google Inc. All Rights Reserved.
-//
-// 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 {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-android_test {
-    name: "AppLinkFunctionalTests",
-
-    sdk_version: "test_current",
-
-    srcs: ["**/*.java"],
-    static_libs: [
-        "launcher-helper-lib",
-        "ub-uiautomator",
-        "platform-test-annotations",
-        "junit",
-    ],
-
-    libs: ["android.test.base.stubs"],
-
-    certificate: "platform",
-}
diff --git a/tests/functional/applinktests/AndroidManifest.xml b/tests/functional/applinktests/AndroidManifest.xml
deleted file mode 100644
index a277696..0000000
--- a/tests/functional/applinktests/AndroidManifest.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
- Copyright 2016 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.functional.applinktests" >
-
-    <uses-sdk android:minSdkVersion="19"
-              android:targetSdkVersion="24" />
-    <application>
-        <uses-library android:name="android.test.runner"/>
-    </application>
-    <instrumentation
-            android:name="android.test.InstrumentationTestRunner"
-            android:targetPackage="com.android.functional.applinktests"
-            android:label="AppLink Functional Tests" />
-</manifest>
diff --git a/tests/functional/applinktests/AndroidTest.xml b/tests/functional/applinktests/AndroidTest.xml
deleted file mode 100644
index 7f6bebc..0000000
--- a/tests/functional/applinktests/AndroidTest.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2017 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<configuration description="Runs AppLink Functional Tests.">
-    <option name="test-suite-tag" value="apct" />
-    <option name="test-suite-tag" value="apct-instrumentation" />
-    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
-        <option name="cleanup-apks" value="true" />
-        <option name="test-file-name" value="AppLinkFunctionalTests.apk" />
-        <option name="test-file-name" value="AppLinkTestApp.apk" />
-    </target_preparer>
-
-    <option name="test-tag" value="ApplinkTests" />
-    <test class="com.android.tradefed.testtype.InstrumentationTest" >
-        <option name="package" value="com.android.functional.applinktests" />
-        <option name="runner" value="android.test.InstrumentationTestRunner" />
-    </test>
-</configuration>
diff --git a/tests/functional/applinktests/src/com/android/functional/applinktests/AppLinkTests.java b/tests/functional/applinktests/src/com/android/functional/applinktests/AppLinkTests.java
deleted file mode 100644
index 57e1382..0000000
--- a/tests/functional/applinktests/src/com/android/functional/applinktests/AppLinkTests.java
+++ /dev/null
@@ -1,275 +0,0 @@
-/*
- * Copyright (C) 2016 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.functional.applinktests;
-
-import android.app.UiAutomation;
-import android.content.Context;
-import android.content.Intent;
-import android.os.ParcelFileDescriptor;
-import android.platform.test.annotations.HermeticTest;
-import android.platform.test.annotations.Presubmit;
-import android.support.test.uiautomator.By;
-import android.support.test.uiautomator.UiDevice;
-import android.support.test.uiautomator.UiObject2;
-import android.support.test.uiautomator.Until;
-import android.test.InstrumentationTestCase;
-import android.util.Log;
-import android.view.accessibility.AccessibilityWindowInfo;
-
-import java.io.BufferedReader;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.util.ArrayList;
-import java.util.List;
-
-public class AppLinkTests extends InstrumentationTestCase {
-    public final String TEST_TAG = "AppLinkFunctionalTest";
-    public final String TEST_PKG_NAME = "com.android.applinktestapp";
-    public final String TEST_APP_NAME = "AppLinkTestApp";
-    public final String YOUTUBE_PKG_NAME = "com.google.android.youtube";
-    public final String HTTP_SCHEME = "http";
-    public final String TEST_HOST = "test.com";
-    public final int TIMEOUT = 1000;
-    private UiDevice mDevice = null;
-    private Context mContext = null;
-    private UiAutomation mUiAutomation = null;
-
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-        mDevice = UiDevice.getInstance(getInstrumentation());
-        mContext = getInstrumentation().getContext();
-        mUiAutomation = getInstrumentation().getUiAutomation();
-        mDevice.setOrientationNatural();
-    }
-
-    // Ensures that default app link setting set to 'undefined' for 3P apps
-    @Presubmit
-    @HermeticTest
-    public void testDefaultAppLinkSettting() throws InterruptedException {
-        String out = getAppLink(TEST_PKG_NAME);
-        assertTrue("Default app link not set to 'undefined' mode", "undefined".equals(out));
-        openLink(HTTP_SCHEME, TEST_HOST);
-        ensureDisambigPresent();
-    }
-
-    // User sets an app to open for a link 'Just Once' and disambig shows up next time too
-    // Once user set to 'always' disambig never shows up
-    public void testUserSetToJustOnceAndAlways() throws InterruptedException {
-        openLink(HTTP_SCHEME, TEST_HOST);
-        ensureDisambigPresent();
-        mDevice.wait(Until.findObject(By.text("AppLinkTestApp")), TIMEOUT).click();
-        mDevice.wait(Until.findObject(By.res("android:id/button_once")), TIMEOUT).click();
-        Thread.sleep(TIMEOUT);
-        verifyForegroundAppPackage(TEST_PKG_NAME);
-        openLink(HTTP_SCHEME, TEST_HOST);
-        assertTrue("Target app isn't the default choice",
-                mDevice.wait(Until.hasObject(By.text("Open with AppLinkTestApp")), TIMEOUT));
-        mDevice.wait(Until.findObject(By.res("android:id/button_once")), TIMEOUT)
-                .clickAndWait(Until.newWindow(), TIMEOUT);
-        Thread.sleep(TIMEOUT);
-        verifyForegroundAppPackage(TEST_PKG_NAME);
-        mDevice.pressHome();
-        // Ensure it doesn't change on second attempt
-        openLink(HTTP_SCHEME, TEST_HOST);
-        // Ensure disambig is present
-        mDevice.wait(Until.findObject(By.res("android:id/button_always")), TIMEOUT).click();
-        mDevice.pressHome();
-        // User chose to set to always and intent is opened in target direct
-        openLink(HTTP_SCHEME, TEST_HOST);
-        verifyForegroundAppPackage(TEST_PKG_NAME);
-    }
-
-    // Ensure verified app always open even candidate but unverified app set to 'always'
-    @Presubmit
-    @HermeticTest
-    public void testVerifiedAppOpenWhenNotVerifiedSetToAlways() throws InterruptedException {
-        setAppLink(TEST_PKG_NAME, "always");
-        setAppLink(YOUTUBE_PKG_NAME, "always");
-        Thread.sleep(TIMEOUT);
-        openLink(HTTP_SCHEME, "youtube.com");
-        verifyForegroundAppPackage(YOUTUBE_PKG_NAME);
-    }
-
-    // Ensure verified app always open even one candidate but unverified app set to 'ask'
-    @Presubmit
-    @HermeticTest
-    public void testVerifiedAppOpenWhenUnverifiedSetToAsk() throws InterruptedException {
-        setAppLink(TEST_PKG_NAME, "ask");
-        setAppLink(YOUTUBE_PKG_NAME, "always");
-        String out = getAppLink(YOUTUBE_PKG_NAME);
-        openLink(HTTP_SCHEME, "youtube.com");
-        verifyForegroundAppPackage(YOUTUBE_PKG_NAME);
-    }
-
-    // Ensure disambig is shown if verified app set to 'never' and unverified app set to 'ask'
-    public void testUserChangeVerifiedLinkHandler() throws InterruptedException {
-        setAppLink(TEST_PKG_NAME, "ask");
-        setAppLink(YOUTUBE_PKG_NAME, "never");
-        Thread.sleep(TIMEOUT);
-        openLink(HTTP_SCHEME, "youtube.com");
-        ensureDisambigPresent();
-        setAppLink(YOUTUBE_PKG_NAME, "always");
-        Thread.sleep(TIMEOUT);
-        openLink(HTTP_SCHEME, "youtube.com");
-        verifyForegroundAppPackage(YOUTUBE_PKG_NAME);
-    }
-
-    // Ensure unverified app always open when unverified app set to always but verified app set to
-    // never
-    @Presubmit
-    @HermeticTest
-    public void testTestAppSetToAlwaysVerifiedSetToNever() throws InterruptedException {
-        setAppLink(TEST_PKG_NAME, "always");
-        setAppLink(YOUTUBE_PKG_NAME, "never");
-        Thread.sleep(TIMEOUT);
-        openLink(HTTP_SCHEME, "youtube.com");
-        verifyForegroundAppPackage(TEST_PKG_NAME);
-    }
-
-    // Test user can modify 'App Link Settings'
-    public void testSettingsChangeUI() throws InterruptedException {
-        Intent intent_as = new Intent(
-                android.provider.Settings.ACTION_APPLICATION_SETTINGS);
-        mContext.startActivity(intent_as);
-        Thread.sleep(TIMEOUT * 5);
-        mDevice.wait(Until.findObject(By.res("com.android.settings:id/advanced")), TIMEOUT)
-                .clickAndWait(Until.newWindow(), TIMEOUT);
-        mDevice.wait(Until.findObject(By.text("Opening links")), TIMEOUT)
-                .clickAndWait(Until.newWindow(), TIMEOUT);
-        mDevice.wait(Until.findObject(By.text("AppLinkTestApp")), TIMEOUT)
-                .clickAndWait(Until.newWindow(), TIMEOUT);
-        mDevice.wait(Until.findObject(By.text("Open supported links")), TIMEOUT)
-                .clickAndWait(Until.newWindow(), TIMEOUT);
-        mDevice.wait(Until.findObject(By.text("Open in this app")), TIMEOUT)
-                .clickAndWait(Until.newWindow(), TIMEOUT);
-        String out = getAppLink(TEST_PKG_NAME);
-        Thread.sleep(TIMEOUT);
-        assertTrue(String.format("Default app link not set to 'always ask' rather set to %s", out),
-                "always".equals(out));
-        mDevice.wait(Until.findObject(By.text("Open supported links")), TIMEOUT)
-                .clickAndWait(Until.newWindow(), TIMEOUT);
-        mDevice.wait(Until.findObject(By.text("Don’t open in this app")), TIMEOUT)
-                .clickAndWait(Until.newWindow(), TIMEOUT);
-        out = getAppLink(TEST_PKG_NAME);
-        Thread.sleep(TIMEOUT);
-        assertTrue(String.format("Default app link not set to 'never' rather set to %s", out),
-                "never".equals(out));
-        mDevice.wait(Until.findObject(By.text("Open supported links")), TIMEOUT)
-                .clickAndWait(Until.newWindow(), TIMEOUT);
-        mDevice.wait(Until.findObject(By.text("Ask every time")), TIMEOUT)
-                .clickAndWait(Until.newWindow(), TIMEOUT);
-        out = getAppLink(TEST_PKG_NAME);
-        Thread.sleep(TIMEOUT);
-        assertTrue(String.format("Default app link not set to 'always ask' rather set to %s", out),
-                "always ask".equals(out));
-    }
-
-    // Ensure system apps that claim to open always for set to always
-    @Presubmit
-    @HermeticTest
-    public void testSysappAppLinkSettings() {
-        // List of system app that are set to 'Always' for certain urls
-        List<String> alwaysOpenApps = new ArrayList<String>();
-        alwaysOpenApps.add("com.android.vending"); // Playstore
-        alwaysOpenApps.add("com.google.android.apps.docs"); // Drive
-        alwaysOpenApps.add("com.google.android.apps.maps"); // Map
-        alwaysOpenApps.add("com.google.android.calendar"); // Calendar
-        alwaysOpenApps.add("com.google.android.music"); // PlayMusic
-        alwaysOpenApps.add("com.google.android.youtube"); // YouTube
-        for (String alwaysOpenApp : alwaysOpenApps) {
-            String out = getAppLink(alwaysOpenApp);
-            assertTrue(String.format("App link for %s should be set to 'Always'", alwaysOpenApp),
-                    "always".equalsIgnoreCase(out));
-        }
-    }
-
-    @Override
-    protected void tearDown() throws Exception {
-        executeShellCommand("pm clear " + TEST_PKG_NAME);
-        executeShellCommand("pm clear " + YOUTUBE_PKG_NAME);
-        executeShellCommand("pm set-app-link " + TEST_PKG_NAME + " undefined");
-        executeShellCommand("pm set-app-link " + YOUTUBE_PKG_NAME + " always");
-        Thread.sleep(TIMEOUT);
-        mDevice.unfreezeRotation();
-        mDevice.pressHome();
-        super.tearDown();
-    }
-
-    // Start an intent to open a test link
-    private void openLink(String scheme, String host) throws InterruptedException {
-        String out = executeShellCommand(String.format(
-                "am start -a android.intent.action.VIEW -d %s://%s/", scheme, host));
-        Thread.sleep(TIMEOUT * 2);
-    }
-
-    // If framework identifies more than one app that can handle a link intent, framework presents a
-    // window to user to choose the app to handle the intent.
-    // This is also known as 'disambig' window
-    private void ensureDisambigPresent() {
-        assertNotNull("Disambig dialog is not shown",
-                mDevice.wait(Until.hasObject(By.res("android:id/resolver_list")),
-                        TIMEOUT));
-        List<UiObject2> resolverApps = mDevice.wait(Until.findObjects(By.res("android:id/text1")),
-                TIMEOUT);
-        assertTrue("There aren't exactly 2 apps to resolve", resolverApps.size() == 2);
-        assertTrue("Resolver apps aren't correct",
-                "AppLinkTestApp".equals(resolverApps.get(0).getText()) &&
-                        "Chrome".equals(resolverApps.get(1).getText()));
-    }
-
-    // Verifies that a certain package is in foreground
-    private void verifyForegroundAppPackage(String pkgName) throws InterruptedException {
-        int counter = 3;
-        List<AccessibilityWindowInfo> windows = null;
-        while (--counter > 0 && windows == null) {
-            windows = mUiAutomation.getWindows();
-            Thread.sleep(TIMEOUT);
-        }
-        assertTrue(String.format("%s is not top activity", "youtube"),
-                windows.get(windows.size() - 1).getRoot().getPackageName().equals(pkgName));
-    }
-
-    // Gets app link for a package
-    private String getAppLink(String pkgName) {
-        return executeShellCommand(String.format("pm get-app-link %s", pkgName));
-    }
-
-    // Sets Openlink settings for a package to passed value
-    private void setAppLink(String pkgName, String valueToBeSet) {
-        executeShellCommand(String.format("pm set-app-link %s %s", pkgName, valueToBeSet));
-    }
-
-    // Executes 'adb shell' command. Converts ParcelFileDescriptor output to String
-    private String executeShellCommand(String command) {
-        if (command == null || command.isEmpty()) {
-            return null;
-        }
-        ParcelFileDescriptor pfd = mUiAutomation.executeShellCommand(command);
-        try (BufferedReader reader = new BufferedReader(
-                new InputStreamReader(new FileInputStream(pfd.getFileDescriptor())))) {
-            String str = reader.readLine();
-            Log.d(TEST_TAG, String.format("Executing command: %s", command));
-            return str;
-        } catch (IOException e) {
-            Log.e(TEST_TAG, e.getMessage());
-        }
-
-        return null;
-    }
-}
diff --git a/tests/functional/calculator/Android.bp b/tests/functional/calculator/Android.bp
index fc77ff0..ead815b 100644
--- a/tests/functional/calculator/Android.bp
+++ b/tests/functional/calculator/Android.bp
@@ -20,7 +20,7 @@
     name: "CalculatorFunctionalTests",
     srcs: ["src/**/*.java"],
     static_libs: [
-        "android-support-test",
+        "androidx.test.rules",
         "launcher-helper-lib",
         "metrics-helper-lib",
         "ub-uiautomator",
diff --git a/tests/functional/calculator/AndroidManifest.xml b/tests/functional/calculator/AndroidManifest.xml
index 0b68441..1805ba0 100644
--- a/tests/functional/calculator/AndroidManifest.xml
+++ b/tests/functional/calculator/AndroidManifest.xml
@@ -29,7 +29,7 @@
     <uses-permission android:name="android.permission.READ_LOGS" />
 
     <instrumentation
-        android:name="android.support.test.runner.AndroidJUnitRunner"
+        android:name="androidx.test.runner.AndroidJUnitRunner"
         android:label="Android Calculator Functional Tests"
         android:targetPackage="com.android.calculator.functional" />
 
diff --git a/tests/functional/devicehealthchecks/assets/bug_map b/tests/functional/devicehealthchecks/assets/bug_map
index 86974f6..f8e614a 100644
--- a/tests/functional/devicehealthchecks/assets/bug_map
+++ b/tests/functional/devicehealthchecks/assets/bug_map
@@ -1,26 +1,31 @@
 <test_name> <regex.no.spaces> <only_bug_number>
-system_app_anr com.google.android.googlequicksearchbox:search 151164399
-system_app_anr com.google.oslo/.service.OsloService 159025912
-system_app_anr com.google.android.as/com.google.android.apps.miphone.aiai.common.superpacks.impl.AiAiSuperpacksService 159058706
-system_app_anr act=android.telephony.action.CARRIER_CONFIG_CHANGED 159921750
+system_app_anr com.google.android.apps.wellbeing*.*ContextManagerRestartBroadcastReceiver_Receiver[\s\S]*cf_x86_phone 166183732
+system_app_anr com.google.android.apps.dreamliner/.dnd.DockConditionProviderService 166174264
+system_app_anr act=android.hardware.usb.action.USB_STATE[\S\s]*cmp=com.google.android.projection.gearhead[\s\S]*ConnectivityEventHandlerImpl\$ConnectivityEventBroadcastReceiver 130956983
+system_app_anr com.google.android.euicc[\s\S]*executing\sservice\scom.google.android.euicc/com.android.euicc.service.EuiccServiceImpl 174479972
+system_app_anr com.google.android.apps.wellbeing*.*ContextManagerRestartBroadcastReceiver_Receiver[\s\S]*cf_x86_64_phone 166183732
 system_app_crash -1\|android\|26\|null\|1000 155073214
 system_app_crash -1\|android\|32\|null\|1000 155073214
-system_app_crash android.database.sqlite.SQLiteCantOpenDatabaseException: 150248286
-system_app_crash com.android.vending:download_service 153462682
-system_app_crash pzd.a.:com.google.android.gms@ 145798275
-system_app_crash com.android.service.ims.RcsService.registerImsCallbacksAndSetAssociatedSubscription 156402275
-system_app_crash com.google.android.inputmethod.latin 157051520
-system_app_crash com.android.vending/com.google.android.finsky.verifier.impl.PackageVerificationService 156670156
-system_app_crash com.google.android.apps.youtube.music.mediabrowser.MusicBrowserService.a 157917208
 system_app_crash android.database.sqlite.SQLiteCloseable.acquireReference 159658068
 system_app_crash com.google.android.gms.backup.component.D2dTransportService 31428310
 system_app_crash Unable\sto\sinstantiate\sapplication\sorg.chromium.chrome.browser.ChromeApplication 161275381
 system_app_crash com.google.android.as.*\s.*\s.*\s.*\s*.*act=android.net.wifi.STATE_CHANGE 161559360
 system_app_crash com.google.android.googlequicksearchbox:search.*\s.*\s.*\s.*\s.*11.26.*\s.*\s.*\s.*\s.*\s\s.*ConcurrentModificationException.*\s.*\s.*\s.*apps.gsa.shared.util.debug.a.g.a 171971925
-system_app_native_crash com.google.android.apps.safetyhub[\s\S]*Scudo\sERROR:\smisaligned\spointer\swhen\sdeallocating\saddress 154358781
-system_app_native_crash com.google.android.providers.media.module 154416156
-system_server_crash void.com.android.server.location.gnss.GnssBatchingProvider.enable 159504970
-SYSTEM_TOMBSTONE android.hardware.vibrator-service.drv2624 151884322
-SYSTEM_TOMBSTONE /vendor/bin/hw/android.hardware.gnss@1.0-service-qti 129282808
-SYSTEM_TOMBSTONE /system/product/priv-app/SafetyHubLprPrebuilt/SafetyHubLprPrebuilt.apk 158504050
-SYSTEM_TOMBSTONE AsyncTask\s+#1\s+>>>\s+com.android.nfc\s+<<<[\S\s]*nfc_main_handle_hal_evt 179110580
+system_app_crash v10804[\s\S]*com.google.android.apps.scone.wifiscorer.WifiScorerService[\s\S]*android.content.Intent.getAction 174277223
+system_app_native_crash HwBinder*.*com.android.bluetooth 155074413
+system_app_native_crash ReferenceQueueD*.*com.google.android.apps.safetyhub 162103095
+system_app_native_crash Binder*.*com.google.android.apps.safetyhub 162104694
+system_app_native_crash Lite\sThread*.*com.google.android.apps.safetyhub 162379378
+system_app_native_crash BG\sThread*.*com.google.android.apps.safetyhub 162381002
+SYSTEM_TOMBSTONE Binder*.*com.google.android.apps.safetyhub 162104694
+SYSTEM_TOMBSTONE ReferenceQueueD*.*com.google.android.apps.safetyhub 162103095
+SYSTEM_TOMBSTONE Lite\sThread*.*com.google.android.apps.safetyhub 162379378
+SYSTEM_TOMBSTONE BG\sThread*.*com.google.android.apps.safetyhub 162381002
+SYSTEM_TOMBSTONE AsyncTask\s+#1\s+>>>\s+com.android.nfc\s+<<<[\S\s]*nfaDeviceManagementCallback[\S\s]*nfc_ncif_cmd_timeout 172057778
+SYSTEM_TOMBSTONE >>>\s+/apex/com.android.os.statsd/bin/statsd\s+<<<[\S\s]*HandleUsingDestroyedMutex 172829930
+SYSTEM_TOMBSTONE audio.service\s+>>>\s+/vendor/bin/hw/android.hardware.audio.service\s+<<<[\S\s]*debuggerd\ssignal[\S\s]*audio_extn_utils_get_snd_card_num 174265816
+SYSTEM_TOMBSTONE imsdatadaemon\s+>>>\s/system/vendor/bin/imsdatadaemon\s<<<[\S\s]*HWAddressSanitizer[\S\s]*libdsi_netctrl 175347022
+SYSTEM_TOMBSTONE com.qualcomm.qti.services.secureui:sui_service\s<<<[\S\s]*null\spointer[\S\s]*libsecureui_svcsock_system 174754036
+SYSTEM_TOMBSTONE >>>\s/vendor/bin/hw/android.hardware.camera.provider@2.6-service-google\s<<<[\S\s]+libGLESv2_adreno 173663856
+SYSTEM_TOMBSTONE (coral|flame|sunfish)[\s\S]+enableInternal\s+>>>\s+com.android.nfc\s<<<[\s\S]+JNI\sFatalError\scalled:\senableInternal 187522327
+SYSTEM_TOMBSTONE android.hardwar*.*/vendor/bin/hw/android.hardware.sensors@2.0-service 163430194
diff --git a/tests/functional/devicehealthchecks/src/com/android/devicehealthchecks/CrashCheckBase.java b/tests/functional/devicehealthchecks/src/com/android/devicehealthchecks/CrashCheckBase.java
index 69119be..191e9b8 100644
--- a/tests/functional/devicehealthchecks/src/com/android/devicehealthchecks/CrashCheckBase.java
+++ b/tests/functional/devicehealthchecks/src/com/android/devicehealthchecks/CrashCheckBase.java
@@ -31,14 +31,19 @@
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.InputMismatchException;
 import java.util.List;
+import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import java.util.Scanner;
 
 abstract class CrashCheckBase {
 
     private static final int MAX_DROPBOX_READ = 4096; // read up to 4K from a dropbox entry
+    private static final int MAX_DROPBOX_READ_ANR = 40960; // read up to 40K for ANR
     private static final int MAX_CRASH_SNIPPET_LINES = 40;
     private static final String INCLUDE_KNOWN_FAILURES = "include_known_failures";
+    private static final Pattern ANR_SUBJECT = Pattern.compile("Subject:");
     private static final String LOG_TAG = CrashCheckBase.class.getSimpleName();
     private Context mContext;
     private KnownFailures mKnownFailures = new KnownFailures();
@@ -69,27 +74,59 @@
         long timestamp = 0;
         DropBoxManager.Entry entry;
         int crashCount = 0;
-        StringBuilder errorDetails = new StringBuilder("Error details:\n");
+        StringBuilder errorDetails = new StringBuilder("\nPlease triage this boot crash:\n");
+        errorDetails.append("go/how-to-triage-devicehealthchecks\n");
+        errorDetails.append("Error Details:\n");
         while (null != (entry = dropbox.getNextEntry(label, timestamp))) {
             String dropboxSnippet;
             try {
-                dropboxSnippet = entry.getText(MAX_DROPBOX_READ);
+                if (label.equals("system_app_anr")) {
+                    dropboxSnippet = entry.getText(MAX_DROPBOX_READ_ANR);
+                } else {
+                    dropboxSnippet = entry.getText(MAX_DROPBOX_READ);
+                }
             } finally {
                 entry.close();
             }
-            KnownFailureItem k = mKnownFailures.findMatchedKnownFailure(label, dropboxSnippet);
-            if (k != null && !mIncludeKnownFailures) {
-                Log.i(
-                        LOG_TAG,
-                        String.format(
-                                "Ignored a known failure, type: %s, pattern: %s, bug: b/%s",
-                                label, k.failurePattern, k.bugNumber));
-            } else {
+            if (dropboxSnippet == null) {
                 crashCount++;
+
                 errorDetails.append(label);
-                errorDetails.append(": ");
-                errorDetails.append(truncate(dropboxSnippet, MAX_CRASH_SNIPPET_LINES));
-                errorDetails.append("    ...\n");
+                errorDetails.append(": (missing details)\n");
+            }
+            else {
+              KnownFailureItem k = mKnownFailures.findMatchedKnownFailure(label, dropboxSnippet);
+              if (k != null && !mIncludeKnownFailures) {
+                  Log.i(
+                          LOG_TAG,
+                          String.format(
+                                  "Ignored a known failure, type: %s, pattern: %s, bug: b/%s",
+                                  label, k.failurePattern, k.bugNumber));
+              } else {
+                  crashCount++;
+                  errorDetails.append(label);
+                  errorDetails.append(": ");
+                    if (label.equals("system_app_anr")) {
+                        // Read Snippet line by line until Subject is found
+                        try (Scanner scanner = new Scanner(dropboxSnippet)) {
+                            while (scanner.hasNextLine()) {
+                                String line = scanner.nextLine();
+                                Matcher matcher = ANR_SUBJECT.matcher(line);
+                                if (matcher.find()) {
+                                    errorDetails.append(line);
+                                    if (scanner.hasNextLine()) {
+                                        errorDetails.append(scanner.nextLine());
+                                    }
+                                    break;
+                                }
+                            }
+                        } catch (InputMismatchException e) {
+                            Log.e(LOG_TAG, "Unable to parse system_app_anr using Scanner");
+                        }
+                    }
+                    errorDetails.append(truncate(dropboxSnippet, MAX_CRASH_SNIPPET_LINES));
+                    errorDetails.append("    ...\n");
+              }
             }
             timestamp = entry.getTimeMillis();
         }
diff --git a/tests/functional/downloadapp/Android.bp b/tests/functional/downloadapp/Android.bp
index afbca5f..43fda1e 100644
--- a/tests/functional/downloadapp/Android.bp
+++ b/tests/functional/downloadapp/Android.bp
@@ -11,7 +11,7 @@
     static_libs: [
         "launcher-helper-lib",
         "ub-uiautomator",
-        "android-support-test",
+        "androidx.test.rules",
     ],
 
     libs: ["android.test.base.stubs"],
diff --git a/tests/functional/downloadapp/AndroidManifest.xml b/tests/functional/downloadapp/AndroidManifest.xml
index 3e46794..14eb154 100644
--- a/tests/functional/downloadapp/AndroidManifest.xml
+++ b/tests/functional/downloadapp/AndroidManifest.xml
@@ -27,7 +27,7 @@
         <uses-library android:name="android.test.runner"/>
     </application>
     <instrumentation
-            android:name="android.support.test.runner.AndroidJUnitRunner"
+            android:name="androidx.test.runner.AndroidJUnitRunner"
             android:targetPackage="com.android.functional.downloadapp"
             android:label="DownloadApp Functional Tests" />
 </manifest>
diff --git a/tests/functional/downloadapp/AndroidTest.xml b/tests/functional/downloadapp/AndroidTest.xml
index 9e2e13e..0c8684e 100644
--- a/tests/functional/downloadapp/AndroidTest.xml
+++ b/tests/functional/downloadapp/AndroidTest.xml
@@ -24,6 +24,6 @@
     <option name="test-tag" value="DownloadAppFunctionalTests" />
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="com.android.functional.downloadapp" />
-        <option name="runner" value="android.support.test.runner.AndroidJUnitRunner" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
     </test>
 </configuration>
diff --git a/tests/functional/notificationtests/Android.bp b/tests/functional/notificationtests/Android.bp
index 697915a..71bb34b 100644
--- a/tests/functional/notificationtests/Android.bp
+++ b/tests/functional/notificationtests/Android.bp
@@ -20,7 +20,7 @@
     name: "NotificationFunctionalTests",
     srcs: ["src/**/*.java"],
     static_libs: [
-        "android-support-test",
+        "androidx.test.rules",
         "launcher-helper-lib",
         "metrics-helper-lib",
         "platform-test-annotations",
diff --git a/tests/functional/notificationtests/AndroidManifest.xml b/tests/functional/notificationtests/AndroidManifest.xml
index 8f0720a..fe0e659 100644
--- a/tests/functional/notificationtests/AndroidManifest.xml
+++ b/tests/functional/notificationtests/AndroidManifest.xml
@@ -32,7 +32,7 @@
     <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
 
     <instrumentation
-        android:name="android.support.test.runner.AndroidJUnitRunner"
+        android:name="androidx.test.runner.AndroidJUnitRunner"
         android:label="Android Notifications Functional Tests"
         android:targetPackage="com.android.notification.functional" />
 
diff --git a/tests/functional/notificationtests/AndroidTest.xml b/tests/functional/notificationtests/AndroidTest.xml
index 720ddd6..dad6a54 100644
--- a/tests/functional/notificationtests/AndroidTest.xml
+++ b/tests/functional/notificationtests/AndroidTest.xml
@@ -23,7 +23,7 @@
     <option name="test-tag" value="android_systemui" />
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="com.android.notification.functional" />
-        <option name="runner" value="android.support.test.runner.AndroidJUnitRunner" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
         <option name="hidden-api-checks" value="false"/>
     </test>
 </configuration>
diff --git a/tests/functional/notificationtests/src/com/android/notification/functional/NotificationHelper.java b/tests/functional/notificationtests/src/com/android/notification/functional/NotificationHelper.java
index 1bd8050..853a884 100644
--- a/tests/functional/notificationtests/src/com/android/notification/functional/NotificationHelper.java
+++ b/tests/functional/notificationtests/src/com/android/notification/functional/NotificationHelper.java
@@ -126,9 +126,11 @@
             throws Exception {
         Log.v(LOG_TAG, "Sending out notification...");
         PendingIntent emptyIntent = PendingIntent.getBroadcast(mContext, 0,
-                new Intent("an.action.that.nobody.will.be.listening.for"), 0);
+                new Intent("an.action.that.nobody.will.be.listening.for"),
+                PendingIntent.FLAG_IMMUTABLE);
         Intent intent = new Intent(Intent.ACTION_VIEW);
-        PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
+        PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent,
+                PendingIntent.FLAG_IMMUTABLE);
         CharSequence subtitle = String.valueOf(System.currentTimeMillis());
         Notification.Builder notification = new Notification.Builder(mContext)
                 .setSmallIcon(R.drawable.stat_notify_email)
@@ -343,7 +345,8 @@
             toastIntent.setAction(ACTION_TOAST + ":" + text); // one per toast message
             toastIntent.putExtra("text", text);
             PendingIntent pi = PendingIntent.getService(
-                    context, 58, toastIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+                    context, 58, toastIntent,
+                    PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
             return pi;
         }
     }
diff --git a/tests/functional/notificationtests/src/com/android/notification/functional/NotificationInteractionTests.java b/tests/functional/notificationtests/src/com/android/notification/functional/NotificationInteractionTests.java
index cc5d164..27707e8 100644
--- a/tests/functional/notificationtests/src/com/android/notification/functional/NotificationInteractionTests.java
+++ b/tests/functional/notificationtests/src/com/android/notification/functional/NotificationInteractionTests.java
@@ -334,7 +334,7 @@
                     .setContentTitle("My notification")
                     .setContentText("Hello World!");
         PendingIntent pi = PendingIntent.getActivity(context, 1,
-                new Intent(Settings.ACTION_SETTINGS), 0);
+                new Intent(Settings.ACTION_SETTINGS), PendingIntent.FLAG_IMMUTABLE);
         mBuilder.setContentIntent(pi);
         mNotificationManager.notify(1, mBuilder.build());
         Thread.sleep(500);
diff --git a/tests/functional/overviewtests/Android.bp b/tests/functional/overviewtests/Android.bp
index 452a1c4..fe7637c 100644
--- a/tests/functional/overviewtests/Android.bp
+++ b/tests/functional/overviewtests/Android.bp
@@ -24,7 +24,7 @@
         "ub-uiautomator",
         "timeresult-helper-lib",
         "launcher-helper-lib",
-        "android-support-test",
+        "androidx.test.rules",
         "platform-test-annotations",
     ],
 
diff --git a/tests/functional/overviewtests/AndroidManifest.xml b/tests/functional/overviewtests/AndroidManifest.xml
index 160eab5..a3982bc 100644
--- a/tests/functional/overviewtests/AndroidManifest.xml
+++ b/tests/functional/overviewtests/AndroidManifest.xml
@@ -25,7 +25,7 @@
           android:targetSdkVersion="23"/>
 
     <instrumentation
-            android:name="android.support.test.runner.AndroidJUnitRunner"
+            android:name="androidx.test.runner.AndroidJUnitRunner"
             android:targetPackage="android.overview.functional"
             android:label="Platform Android Overview Functional Tests" />
 </manifest>
diff --git a/tests/functional/overviewtests/AndroidTest.xml b/tests/functional/overviewtests/AndroidTest.xml
index 6a36bde..ad69236 100644
--- a/tests/functional/overviewtests/AndroidTest.xml
+++ b/tests/functional/overviewtests/AndroidTest.xml
@@ -24,6 +24,6 @@
     <option name="test-tag" value="android_systemui" />
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="android.overview.functional" />
-        <option name="runner" value="android.support.test.runner.AndroidJUnitRunner" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
     </test>
 </configuration>
diff --git a/tests/functional/testapks/applinktestapp/AndroidManifest.xml b/tests/functional/testapks/applinktestapp/AndroidManifest.xml
index 6c0b4ad..37ea9dc 100644
--- a/tests/functional/testapks/applinktestapp/AndroidManifest.xml
+++ b/tests/functional/testapks/applinktestapp/AndroidManifest.xml
@@ -14,32 +14,34 @@
  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.applinktestapp"
-        android:versionCode="1"
-        android:versionName="1.0"
-        android:sharedUserId="com.android.functional.applink" >
+     package="com.android.applinktestapp"
+     android:versionCode="1"
+     android:versionName="1.0"
+     android:sharedUserId="com.android.functional.applink">
 
     <uses-sdk android:minSdkVersion="19"
-          android:targetSdkVersion="24"/>
+         android:targetSdkVersion="24"/>
 
-    <application
-        android:icon="@mipmap/ic_launcher"
-        android:label="AppLinkTestApp" >
-        <activity android:name=".MainActivity" >
+    <application android:icon="@mipmap/ic_launcher"
+         android:label="AppLinkTestApp">
+        <activity android:name=".MainActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-<activity android:name=".MainActivity">
+<activity android:name=".MainActivity"
+     android:exported="true">
     <intent-filter>
-        <action android:name="android.intent.action.VIEW" />
-        <category android:name="android.intent.category.DEFAULT" />
-        <category android:name="android.intent.category.BROWSABLE" />
-        <data android:host="youtube.com" />
-        <data android:scheme="http" />
-        <data android:host="test.com" />
+        <action android:name="android.intent.action.VIEW"/>
+        <category android:name="android.intent.category.DEFAULT"/>
+        <category android:name="android.intent.category.BROWSABLE"/>
+        <data android:host="youtube.com"/>
+        <data android:scheme="http"/>
+        <data android:host="test.com"/>
     </intent-filter>
 </activity>
     </application>
diff --git a/tests/functional/testapks/permissiontestappmv1/AndroidManifest.xml b/tests/functional/testapks/permissiontestappmv1/AndroidManifest.xml
index cf6d152..c82708c 100644
--- a/tests/functional/testapks/permissiontestappmv1/AndroidManifest.xml
+++ b/tests/functional/testapks/permissiontestappmv1/AndroidManifest.xml
@@ -1,20 +1,21 @@
 <?xml version="1.0" encoding="utf-8"?>
+
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.permissiontestappmv1"
-        android:versionCode="1"
-        android:versionName="1.0" >
+     package="com.android.permissiontestappmv1"
+     android:versionCode="1"
+     android:versionName="1.0">
 
     <uses-sdk android:minSdkVersion="23"
-          android:targetSdkVersion="23"/>
+         android:targetSdkVersion="23"/>
 
-    <uses-permission android:name="android.permission.READ_CONTACTS" />
-    <application
-        android:icon="@mipmap/ic_launcher"
-        android:label="PermissionTestAppMV1" >
-        <activity android:name=".MainActivity" >
+    <uses-permission android:name="android.permission.READ_CONTACTS"/>
+    <application android:icon="@mipmap/ic_launcher"
+         android:label="PermissionTestAppMV1">
+        <activity android:name=".MainActivity"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
     </application>
diff --git a/tests/health/scenarios/Android.bp b/tests/health/scenarios/Android.bp
index 2d64396..732a499 100644
--- a/tests/health/scenarios/Android.bp
+++ b/tests/health/scenarios/Android.bp
@@ -27,5 +27,6 @@
         "platform-test-options",
         "platform-test-rules",
         "ub-uiautomator",
+        "microbenchmark-device-lib",
     ],
 }
diff --git a/tests/health/scenarios/src/android/platform/test/scenario/generic/MathWork.java b/tests/health/scenarios/src/android/platform/test/scenario/generic/MathWork.java
new file mode 100644
index 0000000..68e1061
--- /dev/null
+++ b/tests/health/scenarios/src/android/platform/test/scenario/generic/MathWork.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2021 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.scenario.generic;
+
+import android.os.SystemClock;
+import android.platform.test.option.IntegerOption;
+import android.platform.test.option.LongOption;
+import android.platform.test.rule.NaturalOrientationRule;
+import android.platform.test.scenario.annotation.Scenario;
+import android.util.Log;
+
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Do basic math operations for a specified amount of time. */
+@Scenario
+@RunWith(JUnit4.class)
+public class MathWork {
+    private static final String LOG_TAG = MathWork.class.getSimpleName();
+
+    @ClassRule public static final NaturalOrientationRule ORIENT = new NaturalOrientationRule();
+
+    @Rule
+    public final LongOption mDurationMs = new LongOption("math-work-duration_ms").setDefault(5000L);
+
+    @Rule
+    public final LongOption mIntervalMs = new LongOption("math-work-interval_ms").setDefault(100L);
+
+    @Rule
+    public final IntegerOption mThreads = new IntegerOption("math-work-threads").setDefault(20);
+
+    @Rule
+    public final IntegerOption mOperations =
+            new IntegerOption("math-work-operations").setDefault(1000);
+
+    @Test
+    public void testDoMath() {
+        long startTime = SystemClock.uptimeMillis();
+
+        ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(mThreads.get());
+        for (int i = 0; i < mThreads.get(); i++) {
+            Log.v(
+                    LOG_TAG,
+                    String.format(
+                            "Scheduling a repeated math task with operations: %d and interval: %d",
+                            mOperations.get(), mIntervalMs.get()));
+            executor.scheduleAtFixedRate(
+                    new MathRunnable(mOperations.get()),
+                    0,
+                    mIntervalMs.get(),
+                    TimeUnit.MILLISECONDS);
+        }
+
+        SystemClock.sleep(
+                Math.max(0, mDurationMs.get() - (SystemClock.uptimeMillis() - startTime)));
+        executor.shutdownNow();
+        Log.v(LOG_TAG, String.format("Finished performing %d ms of work.", mDurationMs.get()));
+    }
+
+    public static class MathRunnable implements Runnable {
+        private static final double MULTIPLIER = 1.00001;
+
+        private int mOperations;
+
+        public MathRunnable(int operations) {
+            mOperations = operations;
+        }
+
+        @Override
+        public void run() {
+            double x = MULTIPLIER;
+            for (int i = 0; i < mOperations; i++) {
+                x *= MULTIPLIER;
+            }
+        }
+    }
+}
diff --git a/tests/health/scenarios/src/android/platform/test/scenario/generic/OpenAppsFromHome.java b/tests/health/scenarios/src/android/platform/test/scenario/generic/OpenAppsFromHome.java
new file mode 100644
index 0000000..a6642ec
--- /dev/null
+++ b/tests/health/scenarios/src/android/platform/test/scenario/generic/OpenAppsFromHome.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2021 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.scenario.generic;
+
+import android.os.SystemClock;
+import android.platform.test.microbenchmark.Microbenchmark.NoMetricBefore;
+import android.platform.test.option.IntegerOption;
+import android.platform.test.option.StringOption;
+import android.platform.test.rule.NaturalOrientationRule;
+import android.platform.test.rule.UnlockScreenRule;
+import android.platform.test.scenario.annotation.Scenario;
+
+import com.android.launcher3.tapl.AllApps;
+import com.android.launcher3.tapl.AppIcon;
+import com.android.launcher3.tapl.LauncherInstrumentation;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Adds and Opens any application generically from home icon directly, based on the provided
+ * options.
+ */
+@Scenario
+@RunWith(JUnit4.class)
+public class OpenAppsFromHome {
+
+    // Class-level rules
+    @ClassRule public static UnlockScreenRule unlockScreenRule = new UnlockScreenRule();
+
+    @ClassRule public static NaturalOrientationRule orientationRule = new NaturalOrientationRule();
+
+    private static LauncherInstrumentation sLauncher;
+    private static AppIcon sAppIcon;
+
+    @ClassRule
+    public static StringOption sNameOption = new StringOption("app-name").setRequired(true);
+
+    @Rule public StringOption mPkgOption = new StringOption("app-package-name").setRequired(true);
+
+    @ClassRule
+    public static IntegerOption delayTimeOption =
+            new IntegerOption("delay-after-touching-sec").setRequired(false).setDefault(1);
+
+    @BeforeClass
+    public static void setup() throws IOException {
+        sLauncher = new LauncherInstrumentation();
+        final AllApps allApps = sLauncher.pressHome().switchToAllApps();
+        allApps.getAppIcon(sNameOption.get()).dragToWorkspace(false, false);
+        sAppIcon = sLauncher.getWorkspace().getWorkspaceAppIcon(sNameOption.get());
+    }
+
+    @NoMetricBefore
+    public void openWorkspace() {
+        sLauncher.pressHome();
+    }
+
+    @Test
+    public void testOpenApps() {
+        SystemClock.sleep(TimeUnit.SECONDS.toMillis(delayTimeOption.get()));
+        sAppIcon.launch(mPkgOption.get());
+    }
+
+    @AfterClass
+    public static void closeAppAndRemoveIcon() throws IOException {
+        sLauncher.getDevice().executeShellCommand("pm clear com.google.android.apps.nexuslauncher");
+        sLauncher.pressHome();
+    }
+}
diff --git a/tests/health/scenarios/tests/Android.bp b/tests/health/scenarios/tests/Android.bp
index bac2692..0e27a23 100644
--- a/tests/health/scenarios/tests/Android.bp
+++ b/tests/health/scenarios/tests/Android.bp
@@ -63,7 +63,7 @@
     name: "PlatformCommonScenarioTests",
     min_sdk_version: "24",
     static_libs: [
-        "android-support-test",
+        "androidx.test.rules",
         "collector-device-lib-platform",
         "common-platform-scenarios",
         "common-profile-text-protos",
diff --git a/tests/health/scenarios/tests/src/android/platform/test/scenario/generic/MathWorkMicrobenchmark.java b/tests/health/scenarios/tests/src/android/platform/test/scenario/generic/MathWorkMicrobenchmark.java
new file mode 100644
index 0000000..dded76c
--- /dev/null
+++ b/tests/health/scenarios/tests/src/android/platform/test/scenario/generic/MathWorkMicrobenchmark.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2021 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.scenario.generic;
+
+import android.platform.test.microbenchmark.Microbenchmark;
+
+import org.junit.runner.RunWith;
+
+@RunWith(Microbenchmark.class)
+public class MathWorkMicrobenchmark extends MathWork {}
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 03b9da6..2a0c7b1 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
@@ -21,6 +21,7 @@
 import android.platform.test.rule.IorapCompilationRule;
 import android.platform.test.rule.KillAppsRule;
 import android.platform.test.rule.PressHomeRule;
+import android.platform.test.rule.FinishActivitiesWithoutProcessKillRule;
 
 import org.junit.Rule;
 import org.junit.rules.RuleChain;
@@ -35,5 +36,6 @@
                     .around(new DropCachesRule())
                     .around(new CompilationFilterRule(sPkgOption.get()))
                     .around(new PressHomeRule())
-                    .around(new IorapCompilationRule(sPkgOption.get()));
+                    .around(new IorapCompilationRule(sPkgOption.get()))
+                    .around(new FinishActivitiesWithoutProcessKillRule(sPkgOption.get()));
 }
diff --git a/tests/health/scenarios/tests/src/android/platform/test/scenario/generic/OpenAppsFromHomeMicrobenchmark.java b/tests/health/scenarios/tests/src/android/platform/test/scenario/generic/OpenAppsFromHomeMicrobenchmark.java
new file mode 100644
index 0000000..61d8a52
--- /dev/null
+++ b/tests/health/scenarios/tests/src/android/platform/test/scenario/generic/OpenAppsFromHomeMicrobenchmark.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2021 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.scenario.generic;
+
+import android.platform.test.microbenchmark.Microbenchmark;
+
+import org.junit.runner.RunWith;
+
+@RunWith(Microbenchmark.class)
+public class OpenAppsFromHomeMicrobenchmark extends OpenAppsFromHome {}
diff --git a/tests/health/scenarios/tests/src/android/platform/test/scenario/sleep/IdleMemoryMicrobenchmarkBase.java b/tests/health/scenarios/tests/src/android/platform/test/scenario/sleep/IdleMemoryMicrobenchmarkBase.java
deleted file mode 100644
index c6018d5..0000000
--- a/tests/health/scenarios/tests/src/android/platform/test/scenario/sleep/IdleMemoryMicrobenchmarkBase.java
+++ /dev/null
@@ -1,25 +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.platform.test.scenario.sleep;
-
-import android.platform.test.microbenchmark.Microbenchmark;
-
-import org.junit.runner.RunWith;
-
-/** Base class for idle memory microbenchmarks for different processes. */
-@RunWith(Microbenchmark.class)
-public abstract class IdleMemoryMicrobenchmarkBase extends Idle {}
diff --git a/tests/health/scenarios/tests/src/android/platform/test/scenario/sleep/IdleMicrobenchmark.java b/tests/health/scenarios/tests/src/android/platform/test/scenario/sleep/IdleMicrobenchmark.java
new file mode 100644
index 0000000..16d4b04
--- /dev/null
+++ b/tests/health/scenarios/tests/src/android/platform/test/scenario/sleep/IdleMicrobenchmark.java
@@ -0,0 +1,25 @@
+/*
+ * 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.scenario.sleep;
+
+import android.platform.test.microbenchmark.Microbenchmark;
+
+import org.junit.runner.RunWith;
+
+/** Idle microbenchmark. */
+@RunWith(Microbenchmark.class)
+public class IdleMicrobenchmark extends Idle {}
diff --git a/tests/health/scenarios/tests/src/android/platform/test/scenario/sleep/IdleSystemServerMemoryMicrobenchmark.java b/tests/health/scenarios/tests/src/android/platform/test/scenario/sleep/IdleSystemServerMemoryMicrobenchmark.java
index 47d9328..9ae6577 100644
--- a/tests/health/scenarios/tests/src/android/platform/test/scenario/sleep/IdleSystemServerMemoryMicrobenchmark.java
+++ b/tests/health/scenarios/tests/src/android/platform/test/scenario/sleep/IdleSystemServerMemoryMicrobenchmark.java
@@ -24,7 +24,7 @@
 
 /** Idle memory microbenchmark for system server. */
 @RunWith(Microbenchmark.class)
-public class IdleSystemServerMemoryMicrobenchmark extends IdleMemoryMicrobenchmarkBase {
+public class IdleSystemServerMemoryMicrobenchmark extends IdleMicrobenchmark {
     // Method-level rules
     @Rule public GarbageCollectRule gcRule = new GarbageCollectRule("system_server");
 }
diff --git a/tests/health/scenarios/tests/src/android/platform/test/scenario/sleep/IdleSystemUiMemoryMicrobenchmark.java b/tests/health/scenarios/tests/src/android/platform/test/scenario/sleep/IdleSystemUiMemoryMicrobenchmark.java
index debce50..68500c1 100644
--- a/tests/health/scenarios/tests/src/android/platform/test/scenario/sleep/IdleSystemUiMemoryMicrobenchmark.java
+++ b/tests/health/scenarios/tests/src/android/platform/test/scenario/sleep/IdleSystemUiMemoryMicrobenchmark.java
@@ -24,7 +24,7 @@
 
 /** Idle memory microbenchmark for SystemUI. */
 @RunWith(Microbenchmark.class)
-public class IdleSystemUiMemoryMicrobenchmark extends IdleMemoryMicrobenchmarkBase {
+public class IdleSystemUiMemoryMicrobenchmark extends IdleMicrobenchmark {
     // Method-level rules
     @Rule public GarbageCollectRule gcRule = new GarbageCollectRule("com.android.systemui");
 }
diff --git a/tests/jank/UbSystemUiJankTests/Android.bp b/tests/jank/UbSystemUiJankTests/Android.bp
index 524f15d..cfad359 100644
--- a/tests/jank/UbSystemUiJankTests/Android.bp
+++ b/tests/jank/UbSystemUiJankTests/Android.bp
@@ -22,7 +22,7 @@
     srcs: ["src/**/*.java"],
 
     static_libs: [
-        "ub-janktesthelper",
+        "androidx.test.janktesthelper",
         "ub-uiautomator",
         "launcher-helper-lib",
         "timeresult-helper-lib",
diff --git a/tests/jank/UbSystemUiJankTests/AndroidManifest.xml b/tests/jank/UbSystemUiJankTests/AndroidManifest.xml
index e50ff31..d71644c 100644
--- a/tests/jank/UbSystemUiJankTests/AndroidManifest.xml
+++ b/tests/jank/UbSystemUiJankTests/AndroidManifest.xml
@@ -14,643 +14,851 @@
   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="android.platform.systemui.tests.jank">
 
-    <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" />
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="android.platform.systemui.tests.jank">
+
+    <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>
-        <activity android:name=".DummyActivity" android:label="DummyActivity">
+        <activity android:name=".DummyActivity"
+             android:label="DummyActivity">
         </activity>
 
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
         <!-- below are fake activity stubs to inflate launcher all apps icon count-->
-        <activity android:name="android.app.Activity1" android:label="Cupcake">
+        <activity android:name="android.app.Activity1"
+             android:label="Cupcake"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity2" android:label="Donut">
+        <activity android:name="android.app.Activity2"
+             android:label="Donut"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity3" android:label="Eclair">
+        <activity android:name="android.app.Activity3"
+             android:label="Eclair"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity4" android:label="Froyo">
+        <activity android:name="android.app.Activity4"
+             android:label="Froyo"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity5" android:label="Gingerbread">
+        <activity android:name="android.app.Activity5"
+             android:label="Gingerbread"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity6" android:label="Honeycomb">
+        <activity android:name="android.app.Activity6"
+             android:label="Honeycomb"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity7" android:label="Icecream Sandwich">
+        <activity android:name="android.app.Activity7"
+             android:label="Icecream Sandwich"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity8" android:label="Jellybean">
+        <activity android:name="android.app.Activity8"
+             android:label="Jellybean"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity9" android:label="Key Lime Pie">
+        <activity android:name="android.app.Activity9"
+             android:label="Key Lime Pie"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity10" android:label="Lemon Meringue Pie">
+        <activity android:name="android.app.Activity10"
+             android:label="Lemon Meringue Pie"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity11" android:label="Cupcake">
+        <activity android:name="android.app.Activity11"
+             android:label="Cupcake"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity12" android:label="Donut">
+        <activity android:name="android.app.Activity12"
+             android:label="Donut"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity13" android:label="Eclair">
+        <activity android:name="android.app.Activity13"
+             android:label="Eclair"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity14" android:label="Froyo">
+        <activity android:name="android.app.Activity14"
+             android:label="Froyo"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity15" android:label="Gingerbread">
+        <activity android:name="android.app.Activity15"
+             android:label="Gingerbread"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity16" android:label="Honeycomb">
+        <activity android:name="android.app.Activity16"
+             android:label="Honeycomb"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity17" android:label="Icecream Sandwich">
+        <activity android:name="android.app.Activity17"
+             android:label="Icecream Sandwich"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity18" android:label="Jellybean">
+        <activity android:name="android.app.Activity18"
+             android:label="Jellybean"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity19" android:label="Key Lime Pie">
+        <activity android:name="android.app.Activity19"
+             android:label="Key Lime Pie"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity20" android:label="Lemon Meringue Pie">
+        <activity android:name="android.app.Activity20"
+             android:label="Lemon Meringue Pie"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity21" android:label="Cupcake">
+        <activity android:name="android.app.Activity21"
+             android:label="Cupcake"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity22" android:label="Donut">
+        <activity android:name="android.app.Activity22"
+             android:label="Donut"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity23" android:label="Eclair">
+        <activity android:name="android.app.Activity23"
+             android:label="Eclair"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity24" android:label="Froyo">
+        <activity android:name="android.app.Activity24"
+             android:label="Froyo"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity25" android:label="Gingerbread">
+        <activity android:name="android.app.Activity25"
+             android:label="Gingerbread"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity26" android:label="Honeycomb">
+        <activity android:name="android.app.Activity26"
+             android:label="Honeycomb"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity27" android:label="Icecream Sandwich">
+        <activity android:name="android.app.Activity27"
+             android:label="Icecream Sandwich"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity28" android:label="Jellybean">
+        <activity android:name="android.app.Activity28"
+             android:label="Jellybean"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity29" android:label="Key Lime Pie">
+        <activity android:name="android.app.Activity29"
+             android:label="Key Lime Pie"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity30" android:label="Lemon Meringue Pie">
+        <activity android:name="android.app.Activity30"
+             android:label="Lemon Meringue Pie"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity31" android:label="Lemon Meringue Pie">
+        <activity android:name="android.app.Activity31"
+             android:label="Lemon Meringue Pie"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity32" android:label="Lemon Meringue Pie">
+        <activity android:name="android.app.Activity32"
+             android:label="Lemon Meringue Pie"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity33" android:label="Lemon Meringue Pie">
+        <activity android:name="android.app.Activity33"
+             android:label="Lemon Meringue Pie"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity34" android:label="Lemon Meringue Pie">
+        <activity android:name="android.app.Activity34"
+             android:label="Lemon Meringue Pie"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity35" android:label="Lemon Meringue Pie">
+        <activity android:name="android.app.Activity35"
+             android:label="Lemon Meringue Pie"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity36" android:label="Lemon Meringue Pie">
+        <activity android:name="android.app.Activity36"
+             android:label="Lemon Meringue Pie"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity37" android:label="Lemon Meringue Pie">
+        <activity android:name="android.app.Activity37"
+             android:label="Lemon Meringue Pie"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity38" android:label="Lemon Meringue Pie">
+        <activity android:name="android.app.Activity38"
+             android:label="Lemon Meringue Pie"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity39" android:label="Lemon Meringue Pie">
+        <activity android:name="android.app.Activity39"
+             android:label="Lemon Meringue Pie"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity42" android:label="Donut">
+        <activity android:name="android.app.Activity42"
+             android:label="Donut"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity43" android:label="Eclair">
+        <activity android:name="android.app.Activity43"
+             android:label="Eclair"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity44" android:label="Froyo">
+        <activity android:name="android.app.Activity44"
+             android:label="Froyo"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity45" android:label="Gingerbread">
+        <activity android:name="android.app.Activity45"
+             android:label="Gingerbread"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity46" android:label="Honeycomb">
+        <activity android:name="android.app.Activity46"
+             android:label="Honeycomb"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity47" android:label="Icecream Sandwich">
+        <activity android:name="android.app.Activity47"
+             android:label="Icecream Sandwich"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity48" android:label="Jellybean">
+        <activity android:name="android.app.Activity48"
+             android:label="Jellybean"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity49" android:label="Key Lime Pie">
+        <activity android:name="android.app.Activity49"
+             android:label="Key Lime Pie"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity52" android:label="Donut">
+        <activity android:name="android.app.Activity52"
+             android:label="Donut"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity53" android:label="Eclair">
+        <activity android:name="android.app.Activity53"
+             android:label="Eclair"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity54" android:label="Froyo">
+        <activity android:name="android.app.Activity54"
+             android:label="Froyo"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity55" android:label="Gingerbread">
+        <activity android:name="android.app.Activity55"
+             android:label="Gingerbread"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity56" android:label="Honeycomb">
+        <activity android:name="android.app.Activity56"
+             android:label="Honeycomb"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity57" android:label="Icecream Sandwich">
+        <activity android:name="android.app.Activity57"
+             android:label="Icecream Sandwich"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity58" android:label="Jellybean">
+        <activity android:name="android.app.Activity58"
+             android:label="Jellybean"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity59" android:label="Key Lime Pie">
+        <activity android:name="android.app.Activity59"
+             android:label="Key Lime Pie"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity62" android:label="Donut">
+        <activity android:name="android.app.Activity62"
+             android:label="Donut"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity63" android:label="Eclair">
+        <activity android:name="android.app.Activity63"
+             android:label="Eclair"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity64" android:label="Froyo">
+        <activity android:name="android.app.Activity64"
+             android:label="Froyo"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity65" android:label="Gingerbread">
+        <activity android:name="android.app.Activity65"
+             android:label="Gingerbread"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity66" android:label="Honeycomb">
+        <activity android:name="android.app.Activity66"
+             android:label="Honeycomb"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity67" android:label="Icecream Sandwich">
+        <activity android:name="android.app.Activity67"
+             android:label="Icecream Sandwich"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity68" android:label="Jellybean">
+        <activity android:name="android.app.Activity68"
+             android:label="Jellybean"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity69" android:label="Key Lime Pie">
+        <activity android:name="android.app.Activity69"
+             android:label="Key Lime Pie"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity72" android:label="Donut">
+        <activity android:name="android.app.Activity72"
+             android:label="Donut"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity73" android:label="Eclair">
+        <activity android:name="android.app.Activity73"
+             android:label="Eclair"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity74" android:label="Froyo">
+        <activity android:name="android.app.Activity74"
+             android:label="Froyo"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity75" android:label="Gingerbread">
+        <activity android:name="android.app.Activity75"
+             android:label="Gingerbread"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity76" android:label="Honeycomb">
+        <activity android:name="android.app.Activity76"
+             android:label="Honeycomb"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity77" android:label="Icecream Sandwich">
+        <activity android:name="android.app.Activity77"
+             android:label="Icecream Sandwich"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity78" android:label="Jellybean">
+        <activity android:name="android.app.Activity78"
+             android:label="Jellybean"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity79" android:label="Key Lime Pie">
+        <activity android:name="android.app.Activity79"
+             android:label="Key Lime Pie"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity82" android:label="Donut">
+        <activity android:name="android.app.Activity82"
+             android:label="Donut"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity83" android:label="Eclair">
+        <activity android:name="android.app.Activity83"
+             android:label="Eclair"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity84" android:label="Froyo">
+        <activity android:name="android.app.Activity84"
+             android:label="Froyo"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity85" android:label="Gingerbread">
+        <activity android:name="android.app.Activity85"
+             android:label="Gingerbread"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity86" android:label="Honeycomb">
+        <activity android:name="android.app.Activity86"
+             android:label="Honeycomb"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity87" android:label="Icecream Sandwich">
+        <activity android:name="android.app.Activity87"
+             android:label="Icecream Sandwich"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity88" android:label="Jellybean">
+        <activity android:name="android.app.Activity88"
+             android:label="Jellybean"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity89" android:label="Key Lime Pie">
+        <activity android:name="android.app.Activity89"
+             android:label="Key Lime Pie"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity92" android:label="Donut">
+        <activity android:name="android.app.Activity92"
+             android:label="Donut"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity93" android:label="Eclair">
+        <activity android:name="android.app.Activity93"
+             android:label="Eclair"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity94" android:label="Froyo">
+        <activity android:name="android.app.Activity94"
+             android:label="Froyo"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity95" android:label="Gingerbread">
+        <activity android:name="android.app.Activity95"
+             android:label="Gingerbread"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity96" android:label="Honeycomb">
+        <activity android:name="android.app.Activity96"
+             android:label="Honeycomb"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity97" android:label="Icecream Sandwich">
+        <activity android:name="android.app.Activity97"
+             android:label="Icecream Sandwich"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity98" android:label="Jellybean">
+        <activity android:name="android.app.Activity98"
+             android:label="Jellybean"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity99" android:label="Key Lime Pie">
+        <activity android:name="android.app.Activity99"
+             android:label="Key Lime Pie"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity112" android:label="Donut">
+        <activity android:name="android.app.Activity112"
+             android:label="Donut"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity113" android:label="Eclair">
+        <activity android:name="android.app.Activity113"
+             android:label="Eclair"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity114" android:label="Froyo">
+        <activity android:name="android.app.Activity114"
+             android:label="Froyo"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity115" android:label="Gingerbread">
+        <activity android:name="android.app.Activity115"
+             android:label="Gingerbread"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity116" android:label="Honeycomb">
+        <activity android:name="android.app.Activity116"
+             android:label="Honeycomb"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity117" android:label="Icecream Sandwich">
+        <activity android:name="android.app.Activity117"
+             android:label="Icecream Sandwich"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity118" android:label="Jellybean">
+        <activity android:name="android.app.Activity118"
+             android:label="Jellybean"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity119" android:label="Key Lime Pie">
+        <activity android:name="android.app.Activity119"
+             android:label="Key Lime Pie"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity122" android:label="Donut">
+        <activity android:name="android.app.Activity122"
+             android:label="Donut"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity123" android:label="Eclair">
+        <activity android:name="android.app.Activity123"
+             android:label="Eclair"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity124" android:label="Froyo">
+        <activity android:name="android.app.Activity124"
+             android:label="Froyo"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity125" android:label="Gingerbread">
+        <activity android:name="android.app.Activity125"
+             android:label="Gingerbread"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity126" android:label="Honeycomb">
+        <activity android:name="android.app.Activity126"
+             android:label="Honeycomb"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity127" android:label="Icecream Sandwich">
+        <activity android:name="android.app.Activity127"
+             android:label="Icecream Sandwich"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity128" android:label="Jellybean">
+        <activity android:name="android.app.Activity128"
+             android:label="Jellybean"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
-        <activity android:name="android.app.Activity129" android:label="Key Lime Pie">
+        <activity android:name="android.app.Activity129"
+             android:label="Key Lime Pie"
+             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
 
     </application>
 
-    <instrumentation
-            android:name="android.support.test.runner.AndroidJUnitRunner"
-            android:targetPackage="android.platform.systemui.tests.jank"
-            android:label="System UI Jank Tests" />
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="android.platform.systemui.tests.jank"
+         android:label="System UI Jank Tests"/>
 
 </manifest>
diff --git a/tests/jank/UbSystemUiJankTests/src/android/platform/systemui/tests/jank/LauncherJankTests.java b/tests/jank/UbSystemUiJankTests/src/android/platform/systemui/tests/jank/LauncherJankTests.java
index c8509a0..06ec239 100644
--- a/tests/jank/UbSystemUiJankTests/src/android/platform/systemui/tests/jank/LauncherJankTests.java
+++ b/tests/jank/UbSystemUiJankTests/src/android/platform/systemui/tests/jank/LauncherJankTests.java
@@ -20,9 +20,6 @@
 import android.os.Bundle;
 import android.os.Environment;
 import android.os.RemoteException;
-import android.support.test.jank.GfxMonitor;
-import android.support.test.jank.JankTest;
-import android.support.test.jank.JankTestBase;
 import android.support.test.launcherhelper.ILauncherStrategy;
 import android.support.test.launcherhelper.LauncherStrategyFactory;
 import android.support.test.timeresulthelper.TimeResultLogger;
@@ -32,6 +29,10 @@
 import android.support.test.uiautomator.Until;
 import android.system.helpers.OverviewHelper;
 
+import androidx.test.jank.GfxMonitor;
+import androidx.test.jank.JankTest;
+import androidx.test.jank.JankTestBase;
+
 import com.android.launcher3.tapl.AllApps;
 import com.android.launcher3.tapl.LauncherInstrumentation;
 import com.android.launcher3.tapl.Overview;
diff --git a/tests/jank/UbSystemUiJankTests/src/android/platform/systemui/tests/jank/SettingsJankTests.java b/tests/jank/UbSystemUiJankTests/src/android/platform/systemui/tests/jank/SettingsJankTests.java
index 460d67b..e03b583 100644
--- a/tests/jank/UbSystemUiJankTests/src/android/platform/systemui/tests/jank/SettingsJankTests.java
+++ b/tests/jank/UbSystemUiJankTests/src/android/platform/systemui/tests/jank/SettingsJankTests.java
@@ -22,17 +22,17 @@
 import android.os.Environment;
 import android.os.RemoteException;
 import android.os.SystemClock;
-import android.support.test.jank.GfxMonitor;
-import android.support.test.jank.JankTest;
-import android.support.test.jank.JankTestBase;
+import android.support.test.timeresulthelper.TimeResultLogger;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.BySelector;
 import android.support.test.uiautomator.Direction;
 import android.support.test.uiautomator.UiDevice;
 import android.support.test.uiautomator.UiObject2;
 import android.support.test.uiautomator.Until;
-import android.support.test.timeresulthelper.TimeResultLogger;
-import android.os.Bundle;
+
+import androidx.test.jank.GfxMonitor;
+import androidx.test.jank.JankTest;
+import androidx.test.jank.JankTestBase;
 
 import java.io.File;
 import java.io.IOException;
diff --git a/tests/jank/UbSystemUiJankTests/src/android/platform/systemui/tests/jank/SystemUiJankTests.java b/tests/jank/UbSystemUiJankTests/src/android/platform/systemui/tests/jank/SystemUiJankTests.java
index c504517..43228af 100644
--- a/tests/jank/UbSystemUiJankTests/src/android/platform/systemui/tests/jank/SystemUiJankTests.java
+++ b/tests/jank/UbSystemUiJankTests/src/android/platform/systemui/tests/jank/SystemUiJankTests.java
@@ -16,7 +16,7 @@
 
 package android.platform.systemui.tests.jank;
 
-import static android.support.test.InstrumentationRegistry.getInstrumentation;
+import static androidx.test.InstrumentationRegistry.getInstrumentation;
 
 import static org.junit.Assert.assertNotNull;
 
@@ -37,10 +37,6 @@
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.provider.Settings;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.jank.GfxMonitor;
-import android.support.test.jank.JankTest;
-import android.support.test.jank.JankTestBase;
 import android.support.test.launcherhelper.LauncherStrategyFactory;
 import android.support.test.timeresulthelper.TimeResultLogger;
 import android.support.test.uiautomator.By;
@@ -53,6 +49,11 @@
 import android.widget.Button;
 import android.widget.ImageView;
 
+import androidx.test.InstrumentationRegistry;
+import androidx.test.jank.GfxMonitor;
+import androidx.test.jank.JankTest;
+import androidx.test.jank.JankTestBase;
+
 import com.android.launcher3.tapl.LauncherInstrumentation;
 import com.android.launcher3.tapl.Overview;
 
@@ -236,8 +237,12 @@
             }
             builder.setContentText(Integer.toHexString(icon))
                     .setSmallIcon(icon);
-            builder.setContentIntent(PendingIntent.getActivity(
-                    context, 0, new Intent(context, DummyActivity.class), 0));
+            builder.setContentIntent(
+                    PendingIntent.getActivity(
+                            context,
+                            0,
+                            new Intent(context, DummyActivity.class),
+                            PendingIntent.FLAG_MUTABLE_UNAUDITED));
             mNotificationManager.notify(icon, builder.build());
             SystemClock.sleep(sleepBetweenDuration);
             first = false;
@@ -271,8 +276,12 @@
 
     private Action createSmartAction(String actionTitle) {
         Context context = getInstrumentation().getContext();
-        PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0 , new Intent(),
-                PendingIntent.FLAG_UPDATE_CURRENT);
+        PendingIntent pendingIntent =
+                PendingIntent.getBroadcast(
+                        context,
+                        0,
+                        new Intent(),
+                        PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
         Icon icon = Icon.createWithResource(context, ICONS[0]);
         return new Action.Builder(icon, actionTitle, pendingIntent)
                 .setContextual(true)
@@ -284,8 +293,12 @@
                 .setLabel(NOTIFICATION_TEXT)
                 .build();
         Context context = getInstrumentation().getTargetContext();
-        PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0 , new Intent(),
-                PendingIntent.FLAG_UPDATE_CURRENT);
+        PendingIntent pendingIntent =
+                PendingIntent.getBroadcast(
+                        context,
+                        0,
+                        new Intent(),
+                        PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
         Icon icon = Icon.createWithResource(context, ICONS[0]);
         Action action = new Action.Builder(icon, REPLY_TEXT, pendingIntent)
                 .addRemoteInput(remoteInput)
diff --git a/tests/jank/androidtvjanktests/Android.bp b/tests/jank/androidtvjanktests/Android.bp
index 9bacedb..a59614b 100644
--- a/tests/jank/androidtvjanktests/Android.bp
+++ b/tests/jank/androidtvjanktests/Android.bp
@@ -26,8 +26,13 @@
         "timeresult-helper-lib",
         "dpad-util",
     ],
+    libs: [
+        "android.test.runner",
+        "android.test.base",
+    ],
 
-    sdk_version: "23",
+    sdk_version: "30",
+    min_sdk_version: "23",
 
     test_suites: ["device-tests"],
 }
diff --git a/tests/jank/dialer/Android.bp b/tests/jank/dialer/Android.bp
index 428ce2b..f37f226 100644
--- a/tests/jank/dialer/Android.bp
+++ b/tests/jank/dialer/Android.bp
@@ -23,7 +23,7 @@
 
     static_libs: [
         "ub-uiautomator",
-        "ub-janktesthelper",
+        "androidx.test.janktesthelper",
     ],
 
     libs: ["android.test.base.stubs"],
diff --git a/tests/jank/dialer/src/com/android/dialer/janktests/DialerJankTests.java b/tests/jank/dialer/src/com/android/dialer/janktests/DialerJankTests.java
index 61af03e..a3e9f28 100644
--- a/tests/jank/dialer/src/com/android/dialer/janktests/DialerJankTests.java
+++ b/tests/jank/dialer/src/com/android/dialer/janktests/DialerJankTests.java
@@ -16,7 +16,6 @@
 
 package com.android.dialer.janktests;
 
-import android.content.ComponentName;
 import android.content.ContentProviderOperation;
 import android.content.ContentValues;
 import android.content.Intent;
@@ -33,18 +32,16 @@
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
 import android.provider.ContactsContract.RawContacts;
-import android.support.test.jank.GfxMonitor;
-import android.support.test.jank.JankTest;
-import android.support.test.jank.JankTestBase;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.Direction;
 import android.support.test.uiautomator.UiDevice;
 import android.support.test.uiautomator.UiObject2;
 import android.support.test.uiautomator.UiObjectNotFoundException;
 import android.support.test.uiautomator.Until;
-import android.view.View;
 
-import junit.framework.Assert;
+import androidx.test.jank.GfxMonitor;
+import androidx.test.jank.JankTest;
+import androidx.test.jank.JankTestBase;
 
 import java.util.ArrayList;
 import java.util.Random;
diff --git a/tests/jank/jankmicrobenchmark/Android.bp b/tests/jank/jankmicrobenchmark/Android.bp
index 7091a37..715b494 100644
--- a/tests/jank/jankmicrobenchmark/Android.bp
+++ b/tests/jank/jankmicrobenchmark/Android.bp
@@ -22,7 +22,7 @@
 
     static_libs: [
         "ub-uiautomator",
-        "ub-janktesthelper",
+        "androidx.test.janktesthelper",
         "launcher-helper-lib",
         "collector-device-lib",
         "junit",
diff --git a/tests/jank/jankmicrobenchmark/AndroidManifest.xml b/tests/jank/jankmicrobenchmark/AndroidManifest.xml
index 57959ad..2c8d47a 100644
--- a/tests/jank/jankmicrobenchmark/AndroidManifest.xml
+++ b/tests/jank/jankmicrobenchmark/AndroidManifest.xml
@@ -23,7 +23,7 @@
     </application>
 
     <instrumentation
-            android:name="android.support.test.runner.AndroidJUnitRunner"
+            android:name="androidx.test.runner.AndroidJUnitRunner"
             android:targetPackage="com.android.jankmicrobenchmark.janktests"
             android:label="Platform Benchmark Jank Tests" />
 
diff --git a/tests/jank/jankmicrobenchmark/src/com/android/jankmicrobenchmark/janktests/ApiDemoJankTests.java b/tests/jank/jankmicrobenchmark/src/com/android/jankmicrobenchmark/janktests/ApiDemoJankTests.java
index 101eb40..c6e65c5 100644
--- a/tests/jank/jankmicrobenchmark/src/com/android/jankmicrobenchmark/janktests/ApiDemoJankTests.java
+++ b/tests/jank/jankmicrobenchmark/src/com/android/jankmicrobenchmark/janktests/ApiDemoJankTests.java
@@ -18,11 +18,7 @@
 
 import android.content.Intent;
 import android.os.Bundle;
-import android.os.RemoteException;
 import android.os.SystemClock;
-import android.support.test.jank.GfxMonitor;
-import android.support.test.jank.JankTest;
-import android.support.test.jank.JankTestBase;
 import android.support.test.launcherhelper.LauncherStrategyFactory;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.Direction;
@@ -32,6 +28,10 @@
 import android.support.test.uiautomator.Until;
 import android.widget.Button;
 
+import androidx.test.jank.GfxMonitor;
+import androidx.test.jank.JankTest;
+import androidx.test.jank.JankTestBase;
+
 import junit.framework.Assert;
 
 /**
diff --git a/tests/jank/sysapp_wear/Android.bp b/tests/jank/sysapp_wear/Android.bp
index c6b8d3f..9f5667f 100644
--- a/tests/jank/sysapp_wear/Android.bp
+++ b/tests/jank/sysapp_wear/Android.bp
@@ -22,7 +22,7 @@
 
     static_libs: [
         "ub-uiautomator",
-        "ub-janktesthelper",
+        "androidx.test.janktesthelper",
         "junit",
     ],
 
diff --git a/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/AppLauncherFlingJankTest.java b/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/AppLauncherFlingJankTest.java
index fceae07..aa80c27 100644
--- a/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/AppLauncherFlingJankTest.java
+++ b/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/AppLauncherFlingJankTest.java
@@ -17,18 +17,19 @@
 
 import android.content.Context;
 import android.os.Bundle;
-import android.os.RemoteException;
 import android.os.PowerManager;
 import android.os.PowerManager.WakeLock;
-import android.support.test.jank.GfxMonitor;
-import android.support.test.jank.JankTest;
-import android.support.test.jank.JankTestBase;
+import android.os.RemoteException;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.Direction;
 import android.support.test.uiautomator.UiDevice;
 import android.support.test.uiautomator.UiObject2;
 import android.support.test.uiautomator.Until;
 
+import androidx.test.jank.GfxMonitor;
+import androidx.test.jank.JankTest;
+import androidx.test.jank.JankTestBase;
+
 import java.util.concurrent.TimeoutException;
 
 /**
diff --git a/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/CardsJankTest.java b/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/CardsJankTest.java
index f2df0a2..bb8ff8f 100644
--- a/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/CardsJankTest.java
+++ b/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/CardsJankTest.java
@@ -17,11 +17,12 @@
 package com.android.wearable.sysapp.janktests;
 
 import android.os.Bundle;
-import android.support.test.jank.GfxMonitor;
-import android.support.test.jank.JankTest;
-import android.support.test.jank.JankTestBase;
 import android.support.test.uiautomator.UiDevice;
 
+import androidx.test.jank.GfxMonitor;
+import androidx.test.jank.JankTest;
+import androidx.test.jank.JankTestBase;
+
 /**
  * Jank tests for scrolling & swiping off notification cards on wear
  */
diff --git a/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/QuickSettingsJankTest.java b/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/QuickSettingsJankTest.java
index fa9e89f..92d9f43 100644
--- a/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/QuickSettingsJankTest.java
+++ b/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/QuickSettingsJankTest.java
@@ -18,14 +18,15 @@
 
 import android.os.Bundle;
 import android.os.SystemClock;
-import android.support.test.jank.GfxMonitor;
-import android.support.test.jank.JankTest;
-import android.support.test.jank.JankTestBase;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.UiDevice;
 import android.support.test.uiautomator.UiObject2;
 import android.support.test.uiautomator.Until;
 
+import androidx.test.jank.GfxMonitor;
+import androidx.test.jank.JankTest;
+import androidx.test.jank.JankTestBase;
+
 import junit.framework.Assert;
 
 import java.util.concurrent.TimeoutException;
diff --git a/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/SettingsFlingJankTest.java b/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/SettingsFlingJankTest.java
index 97f574f..75e8df7 100644
--- a/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/SettingsFlingJankTest.java
+++ b/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/SettingsFlingJankTest.java
@@ -18,15 +18,17 @@
 
 import android.os.Bundle;
 import android.os.SystemClock;
-import android.support.test.jank.GfxMonitor;
-import android.support.test.jank.JankTest;
-import android.support.test.jank.JankTestBase;
-import android.support.test.jank.WindowAnimationFrameStatsMonitor;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.Direction;
 import android.support.test.uiautomator.UiDevice;
 import android.support.test.uiautomator.UiObject2;
 import android.support.test.uiautomator.Until;
+
+import androidx.test.jank.GfxMonitor;
+import androidx.test.jank.JankTest;
+import androidx.test.jank.JankTestBase;
+import androidx.test.jank.WindowAnimationFrameStatsMonitor;
+
 import java.util.concurrent.TimeoutException;
 
 /**
diff --git a/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/WatchFaceJankTest.java b/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/WatchFaceJankTest.java
index 38436b3..1ecccdf 100644
--- a/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/WatchFaceJankTest.java
+++ b/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/WatchFaceJankTest.java
@@ -17,12 +17,13 @@
 package com.android.wearable.sysapp.janktests;
 
 import android.os.Bundle;
-import android.support.test.jank.GfxMonitor;
-import android.support.test.jank.JankTest;
-import android.support.test.jank.JankTestBase;
 import android.support.test.uiautomator.UiDevice;
 import android.support.test.uiautomator.UiObjectNotFoundException;
 
+import androidx.test.jank.GfxMonitor;
+import androidx.test.jank.JankTest;
+import androidx.test.jank.JankTestBase;
+
 /**
  * Jank tests for WatchFace on clockwork device
  */
diff --git a/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/WatchFacePickerJankTest.java b/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/WatchFacePickerJankTest.java
index 6b985a7..5bc9387 100644
--- a/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/WatchFacePickerJankTest.java
+++ b/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/WatchFacePickerJankTest.java
@@ -17,12 +17,13 @@
 package com.android.wearable.sysapp.janktests;
 
 import android.os.Bundle;
-import android.support.test.jank.GfxMonitor;
-import android.support.test.jank.JankTest;
-import android.support.test.jank.JankTestBase;
 import android.support.test.uiautomator.UiDevice;
 import android.util.Log;
 
+import androidx.test.jank.GfxMonitor;
+import androidx.test.jank.JankTest;
+import androidx.test.jank.JankTestBase;
+
 /**
  * Jank tests for watchFace picker on clockwork device
  */
diff --git a/tests/jank/touch_latency_wear/Android.bp b/tests/jank/touch_latency_wear/Android.bp
index a62676c..5481b92 100644
--- a/tests/jank/touch_latency_wear/Android.bp
+++ b/tests/jank/touch_latency_wear/Android.bp
@@ -22,7 +22,7 @@
 
     static_libs: [
         "ub-uiautomator",
-        "ub-janktesthelper",
+        "androidx.test.janktesthelper",
         "junit",
     ],
 
diff --git a/tests/jank/touch_latency_wear/src/com/android/wearable/touch/janktests/BouncingBallJankTest.java b/tests/jank/touch_latency_wear/src/com/android/wearable/touch/janktests/BouncingBallJankTest.java
index 318bc36..c3216ef 100644
--- a/tests/jank/touch_latency_wear/src/com/android/wearable/touch/janktests/BouncingBallJankTest.java
+++ b/tests/jank/touch_latency_wear/src/com/android/wearable/touch/janktests/BouncingBallJankTest.java
@@ -18,11 +18,12 @@
 
 import android.os.Bundle;
 import android.os.SystemClock;
-import android.support.test.jank.GfxMonitor;
-import android.support.test.jank.JankTest;
-import android.support.test.jank.JankTestBase;
 import android.support.test.uiautomator.UiDevice;
 
+import androidx.test.jank.GfxMonitor;
+import androidx.test.jank.JankTest;
+import androidx.test.jank.JankTestBase;
+
 public class BouncingBallJankTest extends JankTestBase {
 
     private UiDevice mDevice;
diff --git a/tests/jank/uibench/Android.bp b/tests/jank/uibench/Android.bp
index 392b962..a26c434 100644
--- a/tests/jank/uibench/Android.bp
+++ b/tests/jank/uibench/Android.bp
@@ -23,7 +23,7 @@
     static_libs: [
         "collector-device-lib",
         "ub-uiautomator",
-        "ub-janktesthelper",
+        "androidx.test.janktesthelper",
         "junit",
     ],
 
@@ -33,3 +33,21 @@
 
     test_suites: ["device-tests"],
 }
+
+java_library {
+    name: "uibench-test-src",
+    defaults: ["tradefed_errorprone_defaults"],
+
+    srcs: [
+        "src/**/*.java",
+    ],
+
+    static_libs: [
+        "collector-device-lib",
+        "ub-uiautomator",
+        "androidx.test.janktesthelper",
+        "junit",
+    ],
+
+    libs: ["android.test.base.stubs"],
+}
diff --git a/tests/jank/uibench/AndroidManifest.xml b/tests/jank/uibench/AndroidManifest.xml
index da61649..3e4098e 100644
--- a/tests/jank/uibench/AndroidManifest.xml
+++ b/tests/jank/uibench/AndroidManifest.xml
@@ -25,7 +25,7 @@
     </application>
 
     <instrumentation
-            android:name="android.support.test.runner.AndroidJUnitRunner"
+            android:name="androidx.test.runner.AndroidJUnitRunner"
             android:targetPackage="com.android.uibench.janktests"
             android:label="Platform UiBench Jank Tests" />
 </manifest>
diff --git a/tests/jank/uibench/AndroidTest.xml b/tests/jank/uibench/AndroidTest.xml
index 0a75f03..5954a88 100644
--- a/tests/jank/uibench/AndroidTest.xml
+++ b/tests/jank/uibench/AndroidTest.xml
@@ -27,7 +27,7 @@
     <test class="com.android.tradefed.testtype.UiAutomatorTest">
         <option name="instrumentation" value="true"/>
         <option name="package" value="com.android.uibench.janktests" />
-        <option name="runner" value="android.support.test.runner.AndroidJUnitRunner" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
         <option name="isolated-storage" value="false" />
     </test>
 </configuration>
diff --git a/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchJankTests.java b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchJankTests.java
index 1f9eb4d..8a3babd 100644
--- a/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchJankTests.java
+++ b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchJankTests.java
@@ -21,14 +21,15 @@
 import static com.android.uibench.janktests.UiBenchJankTestsHelper.SHORT_EXPECTED_FRAMES;
 
 import android.os.SystemClock;
-import android.support.test.jank.GfxMonitor;
-import android.support.test.jank.JankTest;
-import android.support.test.jank.JankTestBase;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.UiDevice;
 import android.support.test.uiautomator.Until;
 import android.widget.ListView;
 
+import androidx.test.jank.GfxMonitor;
+import androidx.test.jank.JankTest;
+import androidx.test.jank.JankTestBase;
+
 import junit.framework.Assert;
 
 /**
diff --git a/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchJankTestsHelper.java b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchJankTestsHelper.java
index 0dcb4f3..d05b76a 100644
--- a/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchJankTestsHelper.java
+++ b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchJankTestsHelper.java
@@ -55,7 +55,7 @@
     private UiDevice mDevice;
     private Context mContext;
     private DisplayMetrics mDisplayMetrics;
-    protected UiObject2 mContents, mNavigation;
+    protected UiObject2 mContents, mNavigation, mRecyclerView;
 
     private UiBenchJankTestsHelper(Context context, UiDevice device) {
         mContext = context;
@@ -142,9 +142,11 @@
     }
 
     public void slowSingleFlingDown(UiObject2 content) {
-        content.setGestureMargin(getEdgeSensitivity());
+        mRecyclerView =
+                mDevice.wait(Until.findObject(By.res(PACKAGE_NAME, "recyclerView")), TIMEOUT);
+        mRecyclerView.setGestureMargin(getEdgeSensitivity());
         SystemClock.sleep(SHORT_TIMEOUT);
-        content.fling(Direction.DOWN, (int)(SLOW_FLING_SPEED * mDisplayMetrics.density));
+        mRecyclerView.fling(Direction.DOWN, (int) (SLOW_FLING_SPEED * mDisplayMetrics.density));
         mDevice.waitForIdle();
     }
 
diff --git a/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchLeanbackJankTests.java b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchLeanbackJankTests.java
index 95085d1..51bde93 100644
--- a/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchLeanbackJankTests.java
+++ b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchLeanbackJankTests.java
@@ -16,26 +16,23 @@
 
 package com.android.uibench.janktests;
 
+import static com.android.uibench.janktests.UiBenchJankTestsHelper.PACKAGE_NAME;
+import static com.android.uibench.janktests.UiBenchJankTestsHelper.SHORT_EXPECTED_FRAMES;
+
 import android.os.Bundle;
-import android.support.test.jank.GfxMonitor;
-import android.support.test.jank.GfxFrameStatsMonitor;
-import android.support.test.jank.JankTest;
-import android.support.test.jank.JankTestBase;
 import android.support.test.uiautomator.UiDevice;
-import android.text.TextUtils;
 import android.view.KeyEvent;
 
-import static com.android.uibench.janktests.UiBenchJankTestsHelper.SHORT_EXPECTED_FRAMES;
-import static com.android.uibench.janktests.UiBenchJankTestsHelper.PACKAGE_NAME;
+import androidx.test.jank.GfxFrameStatsMonitor;
+import androidx.test.jank.GfxMonitor;
+import androidx.test.jank.JankTest;
+import androidx.test.jank.JankTestBase;
 
 import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
-import java.lang.reflect.Constructor;
-import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
-import java.lang.reflect.Modifier;
 
 /**
  * Jank benchmark tests for leanback
diff --git a/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchRenderingJankTests.java b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchRenderingJankTests.java
index 7a088f0..7f974b4 100644
--- a/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchRenderingJankTests.java
+++ b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchRenderingJankTests.java
@@ -20,13 +20,15 @@
 import static com.android.uibench.janktests.UiBenchJankTestsHelper.PACKAGE_NAME;
 
 import android.os.SystemClock;
-import android.support.test.jank.GfxMonitor;
-import android.support.test.jank.JankTest;
-import android.support.test.jank.JankTestBase;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.UiDevice;
 import android.support.test.uiautomator.Until;
 import android.widget.ListView;
+
+import androidx.test.jank.GfxMonitor;
+import androidx.test.jank.JankTest;
+import androidx.test.jank.JankTestBase;
+
 import junit.framework.Assert;
 
 /**
diff --git a/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchTextJankTests.java b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchTextJankTests.java
index 1e8f8d7..e62f931 100644
--- a/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchTextJankTests.java
+++ b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchTextJankTests.java
@@ -19,14 +19,15 @@
 import static com.android.uibench.janktests.UiBenchJankTestsHelper.EXPECTED_FRAMES;
 import static com.android.uibench.janktests.UiBenchJankTestsHelper.PACKAGE_NAME;
 
-import android.os.SystemClock;
-import android.support.test.jank.GfxMonitor;
-import android.support.test.jank.JankTest;
-import android.support.test.jank.JankTestBase;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.UiDevice;
 import android.support.test.uiautomator.Until;
 import android.widget.ListView;
+
+import androidx.test.jank.GfxMonitor;
+import androidx.test.jank.JankTest;
+import androidx.test.jank.JankTestBase;
+
 import junit.framework.Assert;
 
 /**
diff --git a/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchTransitionsJankTests.java b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchTransitionsJankTests.java
index fccb8c6..4d85fd1 100644
--- a/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchTransitionsJankTests.java
+++ b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchTransitionsJankTests.java
@@ -19,13 +19,15 @@
 import static com.android.uibench.janktests.UiBenchJankTestsHelper.EXPECTED_FRAMES;
 import static com.android.uibench.janktests.UiBenchJankTestsHelper.PACKAGE_NAME;
 
-import android.support.test.jank.GfxMonitor;
-import android.support.test.jank.JankTest;
-import android.support.test.jank.JankTestBase;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.UiDevice;
 import android.support.test.uiautomator.UiObject2;
 import android.support.test.uiautomator.Until;
+
+import androidx.test.jank.GfxMonitor;
+import androidx.test.jank.JankTest;
+import androidx.test.jank.JankTestBase;
+
 import junit.framework.Assert;
 
 /**
diff --git a/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchWebView.java b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchWebView.java
index 28c2ed3..0e0863f 100644
--- a/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchWebView.java
+++ b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchWebView.java
@@ -19,13 +19,14 @@
 import static com.android.uibench.janktests.UiBenchJankTestsHelper.EXPECTED_FRAMES;
 import static com.android.uibench.janktests.UiBenchJankTestsHelper.PACKAGE_NAME;
 
-import android.support.test.jank.GfxMonitor;
-import android.support.test.jank.JankTest;
-import android.support.test.jank.JankTestBase;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.UiDevice;
 import android.support.test.uiautomator.Until;
 
+import androidx.test.jank.GfxMonitor;
+import androidx.test.jank.JankTest;
+import androidx.test.jank.JankTestBase;
+
 /**
  * Jank benchmark WebView tests from UiBench app
  */
diff --git a/tests/jank/uibench_wear/Android.bp b/tests/jank/uibench_wear/Android.bp
index a53f437..a2c1810 100644
--- a/tests/jank/uibench_wear/Android.bp
+++ b/tests/jank/uibench_wear/Android.bp
@@ -22,7 +22,7 @@
 
     static_libs: [
         "ub-uiautomator",
-        "ub-janktesthelper",
+        "androidx.test.janktesthelper",
         "junit",
     ],
 
diff --git a/tests/jank/uibench_wear/src/com/android/wearable/uibench/janktests/UiBenchJankTests.java b/tests/jank/uibench_wear/src/com/android/wearable/uibench/janktests/UiBenchJankTests.java
index d61ddf9..81fca4c 100644
--- a/tests/jank/uibench_wear/src/com/android/wearable/uibench/janktests/UiBenchJankTests.java
+++ b/tests/jank/uibench_wear/src/com/android/wearable/uibench/janktests/UiBenchJankTests.java
@@ -16,13 +16,11 @@
 
 package com.android.wearable.uibench.janktests;
 
-import android.content.Intent;
+import static com.android.wearable.uibench.janktests.UiBenchJankTestsHelper.EXPECTED_FRAMES;
+import static com.android.wearable.uibench.janktests.UiBenchJankTestsHelper.PACKAGE_NAME;
+
 import android.os.Bundle;
-import android.os.RemoteException;
 import android.os.SystemClock;
-import android.support.test.jank.GfxMonitor;
-import android.support.test.jank.JankTest;
-import android.support.test.jank.JankTestBase;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.Direction;
 import android.support.test.uiautomator.UiDevice;
@@ -31,9 +29,10 @@
 import android.support.test.uiautomator.Until;
 import android.widget.ListView;
 
-import com.android.wearable.uibench.janktests.UiBenchJankTestsHelper;
-import static com.android.wearable.uibench.janktests.UiBenchJankTestsHelper.PACKAGE_NAME;
-import static com.android.wearable.uibench.janktests.UiBenchJankTestsHelper.EXPECTED_FRAMES;
+import androidx.test.jank.GfxMonitor;
+import androidx.test.jank.JankTest;
+import androidx.test.jank.JankTestBase;
+
 import junit.framework.Assert;
 
 /**
diff --git a/tests/jank/uibench_wear/src/com/android/wearable/uibench/janktests/UiBenchRenderingJankTests.java b/tests/jank/uibench_wear/src/com/android/wearable/uibench/janktests/UiBenchRenderingJankTests.java
index b893a0a..b352b0e 100644
--- a/tests/jank/uibench_wear/src/com/android/wearable/uibench/janktests/UiBenchRenderingJankTests.java
+++ b/tests/jank/uibench_wear/src/com/android/wearable/uibench/janktests/UiBenchRenderingJankTests.java
@@ -16,13 +16,11 @@
 
 package com.android.wearable.uibench.janktests;
 
-import android.content.Intent;
+import static com.android.wearable.uibench.janktests.UiBenchJankTestsHelper.EXPECTED_FRAMES;
+import static com.android.wearable.uibench.janktests.UiBenchJankTestsHelper.PACKAGE_NAME;
+
 import android.os.Bundle;
-import android.os.RemoteException;
 import android.os.SystemClock;
-import android.support.test.jank.GfxMonitor;
-import android.support.test.jank.JankTest;
-import android.support.test.jank.JankTestBase;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.Direction;
 import android.support.test.uiautomator.UiDevice;
@@ -31,9 +29,10 @@
 import android.support.test.uiautomator.Until;
 import android.widget.ListView;
 
-import com.android.wearable.uibench.janktests.UiBenchJankTestsHelper;
-import static com.android.wearable.uibench.janktests.UiBenchJankTestsHelper.PACKAGE_NAME;
-import static com.android.wearable.uibench.janktests.UiBenchJankTestsHelper.EXPECTED_FRAMES;
+import androidx.test.jank.GfxMonitor;
+import androidx.test.jank.JankTest;
+import androidx.test.jank.JankTestBase;
+
 import junit.framework.Assert;
 
 /**
diff --git a/tests/jank/uibench_wear/src/com/android/wearable/uibench/janktests/UiBenchTextJankTests.java b/tests/jank/uibench_wear/src/com/android/wearable/uibench/janktests/UiBenchTextJankTests.java
index 60b6f3d..89cfa32 100644
--- a/tests/jank/uibench_wear/src/com/android/wearable/uibench/janktests/UiBenchTextJankTests.java
+++ b/tests/jank/uibench_wear/src/com/android/wearable/uibench/janktests/UiBenchTextJankTests.java
@@ -19,13 +19,9 @@
 import static com.android.wearable.uibench.janktests.UiBenchJankTestsHelper.EXPECTED_FRAMES;
 import static com.android.wearable.uibench.janktests.UiBenchJankTestsHelper.PACKAGE_NAME;
 
-import android.os.Build.VERSION;
 import android.os.Bundle;
 import android.os.RemoteException;
 import android.os.SystemClock;
-import android.support.test.jank.GfxMonitor;
-import android.support.test.jank.JankTest;
-import android.support.test.jank.JankTestBase;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.Direction;
 import android.support.test.uiautomator.StaleObjectException;
@@ -33,10 +29,11 @@
 import android.support.test.uiautomator.UiObject2;
 import android.support.test.uiautomator.UiObjectNotFoundException;
 import android.support.test.uiautomator.Until;
-import android.view.KeyEvent;
 import android.widget.ListView;
 
-import com.android.wearable.uibench.janktests.UiBenchJankTestsHelper;
+import androidx.test.jank.GfxMonitor;
+import androidx.test.jank.JankTest;
+import androidx.test.jank.JankTestBase;
 
 import junit.framework.Assert;
 
diff --git a/tests/jank/webview/Android.bp b/tests/jank/webview/Android.bp
index 61097d0..9e3bb7e 100644
--- a/tests/jank/webview/Android.bp
+++ b/tests/jank/webview/Android.bp
@@ -23,7 +23,7 @@
 
     static_libs: [
         "ub-uiautomator",
-        "ub-janktesthelper",
+        "androidx.test.janktesthelper",
     ],
 
     libs: ["android.test.base.stubs"],
diff --git a/tests/jank/webview/src/com/android/webview/chromium/tests/jank/WebViewFlingTest.java b/tests/jank/webview/src/com/android/webview/chromium/tests/jank/WebViewFlingTest.java
index a2de38b..792cb15 100644
--- a/tests/jank/webview/src/com/android/webview/chromium/tests/jank/WebViewFlingTest.java
+++ b/tests/jank/webview/src/com/android/webview/chromium/tests/jank/WebViewFlingTest.java
@@ -19,16 +19,14 @@
 import android.content.Intent;
 import android.net.Uri;
 import android.os.SystemClock;
-import android.support.test.jank.JankTest;
-import android.support.test.jank.JankTestBase;
-import android.support.test.jank.GfxMonitor;
-import android.support.test.jank.WindowContentFrameStatsMonitor;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.Direction;
 import android.support.test.uiautomator.UiDevice;
 import android.support.test.uiautomator.UiObject2;
-import android.support.test.uiautomator.UiScrollable;
-import android.support.test.uiautomator.UiSelector;
+
+import androidx.test.jank.JankTest;
+import androidx.test.jank.JankTestBase;
+import androidx.test.jank.WindowContentFrameStatsMonitor;
 
 import java.io.File;
 import java.io.IOException;
diff --git a/tests/microbenchmarks/uibench/Android.bp b/tests/microbenchmarks/uibench/Android.bp
index bb4e4c6..4bc93c7 100644
--- a/tests/microbenchmarks/uibench/Android.bp
+++ b/tests/microbenchmarks/uibench/Android.bp
@@ -30,9 +30,32 @@
 
     libs: ["android.test.base.stubs"],
 
-    data: [":perfetto_artifacts"],
+    data: [":perfetto_artifacts",
+           ":UiBench",],
 
     sdk_version: "test_current",
 
+    test_config: "configs/uibench-all.xml",
+
+    test_options: {
+        extra_test_configs: ["configs/uibench-dialoglist.xml",
+                             "configs/uibench-invalidate.xml",],
+    },
+
     test_suites: ["device-tests"],
 }
+
+java_library {
+    name: "UiBenchMicrobenchmark-test-src",
+    srcs: ["src/**/*.java"],
+
+    static_libs: [
+        "collector-device-lib",
+        "ub-uiautomator",
+        "junit",
+        "microbenchmark-device-lib",
+        "launcher-helper-lib",
+    ],
+
+    libs: ["android.test.base.stubs"],
+}
diff --git a/tests/microbenchmarks/uibench/AndroidTest.xml b/tests/microbenchmarks/uibench/AndroidTest.xml
deleted file mode 100644
index 7c93cdf..0000000
--- a/tests/microbenchmarks/uibench/AndroidTest.xml
+++ /dev/null
@@ -1,47 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-<configuration description="Runs Platform UiBench Microbenchmark Tests.">
-
-    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
-    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
-        <option name="push-file" key="trace_config_detailed.textproto" value="/data/misc/perfetto-traces/trace_config.textproto" />
-    </target_preparer>
-
-    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
-        <option name="test-file-name" value="UiBenchMicrobenchmark.apk" />
-        <option name="test-file-name" value="UiBench.apk" />
-    </target_preparer>
-
-    <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
-        <option name="pull-pattern-keys" value="perfetto_file_path" />
-    </metrics_collector>
-
-    <option name="post-boot-command" value="am broadcast -a com.google.android.clockwork.action.TEST_MODE" />
-    <option name="post-boot-command" value="setprop debug.hwui.filter_test_overhead true" />
-    <option name="post-boot-command" value="settings put secure accessibility_disable_animations 0" />
-
-    <option name="test-tag" value="UiBenchMicrobenchmarks" />
-
-    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
-        <option name="package" value="com.android.uibench.microbenchmark" />
-        <option name="class" value="com.android.uibench.microbenchmark.UiBenchDialogListFlingMicrobenchmark#testDialogListFling" />
-        <option name="device-listeners" value="android.device.collectors.PerfettoListener,android.device.collectors.SfStatsListener" />
-        <option name="instrumentation-arg" key="perfetto_config_text_proto" value="true" />
-        <option name="instrumentation-arg" key="perfetto_config_file" value="trace_config.textproto" />
-        <option name="instrumentation-arg" key="newRunListenerMode" value="true" />
-    </test>
-
-</configuration>
diff --git a/tests/microbenchmarks/uibench/configs/uibench-all.xml b/tests/microbenchmarks/uibench/configs/uibench-all.xml
new file mode 100644
index 0000000..f9cce2d
--- /dev/null
+++ b/tests/microbenchmarks/uibench/configs/uibench-all.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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="Runs Platform UiBench Microbenchmark Tests.">
+
+    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
+    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+        <option name="push-file" key="trace_config_detailed.textproto" value="/data/misc/perfetto-traces/trace_config.textproto" />
+    </target_preparer>
+
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="UiBenchMicrobenchmark.apk" />
+        <option name="test-file-name" value="UiBench.apk" />
+    </target_preparer>
+
+    <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+        <option name="pull-pattern-keys" value="perfetto_file_path" />
+    </metrics_collector>
+
+    <option name="post-boot-command" value="am broadcast -a com.google.android.clockwork.action.TEST_MODE" />
+    <option name="post-boot-command" value="setprop debug.hwui.filter_test_overhead true" />
+    <option name="post-boot-command" value="settings put secure accessibility_disable_animations 0" />
+
+    <option name="test-tag" value="UiBenchMicrobenchmarks" />
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="instrumentation-arg" key="newRunListenerMode" value="true" />
+        <option name="package" value="com.android.uibench.microbenchmark" />
+        <!-- Uncomment the below option to run only subset of tests -->
+        <!-- <option name="class" value="com.android.uibench.microbenchmark.UiBenchDialogListFlingMicrobenchmark#testDialogListFling" /> -->
+        <option name="device-listeners" value="android.device.collectors.PerfettoListener,android.device.collectors.JankListener,android.device.collectors.SfStatsListener" />
+        <option name="instrumentation-arg" key="iterations" value="2" />
+        <!-- Args are used for renaming the test name with the iteration number -->
+        <option name="instrumentation-arg" key="rename-iterations" value="true" />
+        <option name="instrumentation-arg" key="iteration-separator" value="$" />
+        <!-- Perfetto trace related arguments -->
+        <option name="instrumentation-arg" key="perfetto_config_text_proto" value="true" />
+        <option name="instrumentation-arg" key="perfetto_config_file" value="trace_config.textproto" />
+        <!-- Jank listener related arguments -->
+        <option name="instrumentation-arg" key="jank-listener:jank-package-names" value="com.android.test.uibench" />
+    </test>
+
+</configuration>
diff --git a/tests/microbenchmarks/uibench/configs/uibench-dialoglist.xml b/tests/microbenchmarks/uibench/configs/uibench-dialoglist.xml
new file mode 100644
index 0000000..9fb4616
--- /dev/null
+++ b/tests/microbenchmarks/uibench/configs/uibench-dialoglist.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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="Runs Platform UiBench Microbenchmark Tests.">
+
+    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
+    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+        <option name="push-file" key="trace_config_detailed.textproto" value="/data/misc/perfetto-traces/trace_config.textproto" />
+    </target_preparer>
+
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="UiBenchMicrobenchmark.apk" />
+        <option name="test-file-name" value="UiBench.apk" />
+    </target_preparer>
+
+    <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+        <option name="pull-pattern-keys" value="perfetto_file_path" />
+    </metrics_collector>
+
+    <option name="post-boot-command" value="am broadcast -a com.google.android.clockwork.action.TEST_MODE" />
+    <option name="post-boot-command" value="setprop debug.hwui.filter_test_overhead true" />
+    <option name="post-boot-command" value="settings put secure accessibility_disable_animations 0" />
+
+    <option name="test-tag" value="UiBenchMicrobenchmarks" />
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="instrumentation-arg" key="newRunListenerMode" value="true" />
+        <option name="package" value="com.android.uibench.microbenchmark" />
+        <!-- Uncomment the below option to run only subset of tests -->
+        <option name="class" value="com.android.uibench.microbenchmark.UiBenchDialogListFlingMicrobenchmark#testDialogListFling" />
+        <option name="device-listeners" value="android.device.collectors.PerfettoListener,android.device.collectors.JankListener,android.device.collectors.SfStatsListener" />
+        <option name="instrumentation-arg" key="iterations" value="2" />
+        <!-- Args are used for renaming the test name with the iteration number -->
+        <option name="instrumentation-arg" key="rename-iterations" value="true" />
+        <option name="instrumentation-arg" key="iteration-separator" value="$" />
+        <!-- Perfetto trace related arguments -->
+        <option name="instrumentation-arg" key="perfetto_config_text_proto" value="true" />
+        <option name="instrumentation-arg" key="perfetto_config_file" value="trace_config.textproto" />
+        <!-- Jank listener related arguments -->
+        <option name="instrumentation-arg" key="jank-listener:jank-package-names" value="com.android.test.uibench" />
+    </test>
+
+</configuration>
diff --git a/tests/microbenchmarks/uibench/configs/uibench-invalidate.xml b/tests/microbenchmarks/uibench/configs/uibench-invalidate.xml
new file mode 100644
index 0000000..18ad574
--- /dev/null
+++ b/tests/microbenchmarks/uibench/configs/uibench-invalidate.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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="Runs Platform UiBench Microbenchmark Tests.">
+
+    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
+    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+        <option name="push-file" key="trace_config_detailed.textproto" value="/data/misc/perfetto-traces/trace_config.textproto" />
+    </target_preparer>
+
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="UiBenchMicrobenchmark.apk" />
+        <option name="test-file-name" value="UiBench.apk" />
+    </target_preparer>
+
+    <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+        <option name="pull-pattern-keys" value="perfetto_file_path" />
+    </metrics_collector>
+
+    <option name="post-boot-command" value="am broadcast -a com.google.android.clockwork.action.TEST_MODE" />
+    <option name="post-boot-command" value="setprop debug.hwui.filter_test_overhead true" />
+    <option name="post-boot-command" value="settings put secure accessibility_disable_animations 0" />
+
+    <option name="test-tag" value="UiBenchMicrobenchmarks" />
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="instrumentation-arg" key="newRunListenerMode" value="true" />
+        <option name="package" value="com.android.uibench.microbenchmark" />
+        <!-- Uncomment the below option to run only subset of tests -->
+        <option name="class" value="com.android.uibench.microbenchmark.UiBenchInvalidateMicrobenchmark#testInvalidate" />
+        <option name="device-listeners" value="android.device.collectors.PerfettoListener,android.device.collectors.JankListener,android.device.collectors.SfStatsListener" />
+        <option name="instrumentation-arg" key="iterations" value="2" />
+        <!-- Args are used for renaming the test name with the iteration number -->
+        <option name="instrumentation-arg" key="rename-iterations" value="true" />
+        <option name="instrumentation-arg" key="iteration-separator" value="$" />
+        <!-- Perfetto trace related arguments -->
+        <option name="instrumentation-arg" key="perfetto_config_text_proto" value="true" />
+        <option name="instrumentation-arg" key="perfetto_config_file" value="trace_config.textproto" />
+        <!-- Jank listener related arguments -->
+        <option name="instrumentation-arg" key="jank-listener:jank-package-names" value="com.android.test.uibench" />
+    </test>
+
+</configuration>
diff --git a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchActivityTransitionsAnimationMicrobenchmark.java b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchActivityTransitionsAnimationMicrobenchmark.java
index 03106d2..7cd6a1f 100644
--- a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchActivityTransitionsAnimationMicrobenchmark.java
+++ b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchActivityTransitionsAnimationMicrobenchmark.java
@@ -19,10 +19,12 @@
 import android.platform.helpers.HelperAccessor;
 import android.platform.test.microbenchmark.Microbenchmark;
 import android.platform.test.rule.NaturalOrientationRule;
+import android.platform.test.rule.Dex2oatPressureRule;
 
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -33,6 +35,9 @@
     private static HelperAccessor<IUiBenchJankHelper> sHelper =
             new HelperAccessor<>(IUiBenchJankHelper.class);
 
+    @Rule
+    public Dex2oatPressureRule dex2oatPressureRule = new Dex2oatPressureRule();
+
     @BeforeClass
     public static void openApp() {
         sHelper.get().openActivityTransition();
diff --git a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchBitmapUploadJankMicrobenchmark.java b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchBitmapUploadJankMicrobenchmark.java
index 19cfcf9..abb8417 100644
--- a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchBitmapUploadJankMicrobenchmark.java
+++ b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchBitmapUploadJankMicrobenchmark.java
@@ -20,10 +20,12 @@
 import android.platform.helpers.HelperAccessor;
 import android.platform.test.microbenchmark.Microbenchmark;
 import android.platform.test.rule.NaturalOrientationRule;
+import android.platform.test.rule.Dex2oatPressureRule;
 
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -34,6 +36,9 @@
     private static HelperAccessor<IUiBenchJankHelper> sHelper =
             new HelperAccessor<>(IUiBenchJankHelper.class);
 
+    @Rule
+    public Dex2oatPressureRule dex2oatPressureRule = new Dex2oatPressureRule();
+
     @BeforeClass
     public static void openApp() {
         sHelper.get().openBitmapUpload();
diff --git a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchBrowseContentScrollMicrobenchmark.java b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchBrowseContentScrollMicrobenchmark.java
index 489dbe7..0cb89de 100644
--- a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchBrowseContentScrollMicrobenchmark.java
+++ b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchBrowseContentScrollMicrobenchmark.java
@@ -19,10 +19,12 @@
 import android.platform.helpers.HelperAccessor;
 import android.platform.test.microbenchmark.Microbenchmark;
 import android.platform.test.rule.NaturalOrientationRule;
+import android.platform.test.rule.Dex2oatPressureRule;
 
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -33,6 +35,9 @@
     private static HelperAccessor<IUiBenchJankHelper> sHelper =
             new HelperAccessor<>(IUiBenchJankHelper.class);
 
+    @Rule
+    public Dex2oatPressureRule dex2oatPressureRule = new Dex2oatPressureRule();
+
     @BeforeClass
     public static void openApp() {
         sHelper.get().openLeanbackActivity(true, false, "leanback.BrowseActivity", "Row");
diff --git a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchBrowseContentScrollNoBitmapUploadMicrobenchmark.java b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchBrowseContentScrollNoBitmapUploadMicrobenchmark.java
index c71fa27..a75b2cf 100644
--- a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchBrowseContentScrollNoBitmapUploadMicrobenchmark.java
+++ b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchBrowseContentScrollNoBitmapUploadMicrobenchmark.java
@@ -19,10 +19,12 @@
 import android.platform.helpers.HelperAccessor;
 import android.platform.test.microbenchmark.Microbenchmark;
 import android.platform.test.rule.NaturalOrientationRule;
+import android.platform.test.rule.Dex2oatPressureRule;
 
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -33,6 +35,9 @@
     private static HelperAccessor<IUiBenchJankHelper> sHelper =
             new HelperAccessor<>(IUiBenchJankHelper.class);
 
+    @Rule
+    public Dex2oatPressureRule dex2oatPressureRule = new Dex2oatPressureRule();
+
     @BeforeClass
     public static void openApp() {
         sHelper.get().openLeanbackActivity(false, false, "leanback.BrowseActivity", "Row");
diff --git a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchBrowseFastLaneScrollMicrobenchmark.java b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchBrowseFastLaneScrollMicrobenchmark.java
index 3ac0dc2..f95032e 100644
--- a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchBrowseFastLaneScrollMicrobenchmark.java
+++ b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchBrowseFastLaneScrollMicrobenchmark.java
@@ -19,10 +19,12 @@
 import android.platform.helpers.HelperAccessor;
 import android.platform.test.microbenchmark.Microbenchmark;
 import android.platform.test.rule.NaturalOrientationRule;
+import android.platform.test.rule.Dex2oatPressureRule;
 
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -33,6 +35,9 @@
     private static HelperAccessor<IUiBenchJankHelper> sHelper =
             new HelperAccessor<>(IUiBenchJankHelper.class);
 
+    @Rule
+    public Dex2oatPressureRule dex2oatPressureRule = new Dex2oatPressureRule();
+
     @BeforeClass
     public static void openApp() {
         sHelper.get().openLeanbackActivity(true, true, "leanback.BrowseActivity", "Row");
diff --git a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchBrowseFastLaneScrollNoBitmapUploadMicrobenchmark.java b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchBrowseFastLaneScrollNoBitmapUploadMicrobenchmark.java
index d2d7b35..14df29a 100644
--- a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchBrowseFastLaneScrollNoBitmapUploadMicrobenchmark.java
+++ b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchBrowseFastLaneScrollNoBitmapUploadMicrobenchmark.java
@@ -19,10 +19,12 @@
 import android.platform.helpers.HelperAccessor;
 import android.platform.test.microbenchmark.Microbenchmark;
 import android.platform.test.rule.NaturalOrientationRule;
+import android.platform.test.rule.Dex2oatPressureRule;
 
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -33,6 +35,9 @@
     private static HelperAccessor<IUiBenchJankHelper> sHelper =
             new HelperAccessor<>(IUiBenchJankHelper.class);
 
+    @Rule
+    public Dex2oatPressureRule dex2oatPressureRule = new Dex2oatPressureRule();
+
     @BeforeClass
     public static void openApp() {
         sHelper.get().openLeanbackActivity(false, true, "leanback.BrowseActivity", "Row");
diff --git a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchClippedListViewMicrobenchmark.java b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchClippedListViewMicrobenchmark.java
index 79752c5..8e9ec43 100644
--- a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchClippedListViewMicrobenchmark.java
+++ b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchClippedListViewMicrobenchmark.java
@@ -19,10 +19,12 @@
 import android.platform.helpers.HelperAccessor;
 import android.platform.test.microbenchmark.Microbenchmark;
 import android.platform.test.rule.NaturalOrientationRule;
+import android.platform.test.rule.Dex2oatPressureRule;
 
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -33,6 +35,9 @@
     private static HelperAccessor<IUiBenchJankHelper> sHelper =
             new HelperAccessor<>(IUiBenchJankHelper.class);
 
+    @Rule
+    public Dex2oatPressureRule dex2oatPressureRule = new Dex2oatPressureRule();
+
     @BeforeClass
     public static void openApp() {
         sHelper.get().openClippedListView();
diff --git a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchDialogListFlingMicrobenchmark.java b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchDialogListFlingMicrobenchmark.java
index e65186f..cdcfd34 100644
--- a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchDialogListFlingMicrobenchmark.java
+++ b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchDialogListFlingMicrobenchmark.java
@@ -19,10 +19,12 @@
 import android.platform.helpers.HelperAccessor;
 import android.platform.test.microbenchmark.Microbenchmark;
 import android.platform.test.rule.NaturalOrientationRule;
+import android.platform.test.rule.Dex2oatPressureRule;
 
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -33,6 +35,9 @@
     private static HelperAccessor<IUiBenchJankHelper> sHelper =
             new HelperAccessor<>(IUiBenchJankHelper.class);
 
+    @Rule
+    public Dex2oatPressureRule dex2oatPressureRule = new Dex2oatPressureRule();
+
     @BeforeClass
     public static void openApp() {
         sHelper.get().openDialogList();
diff --git a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchEditTextTypingMicrobenchmark.java b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchEditTextTypingMicrobenchmark.java
index 9e148b2..5bf5ad9 100644
--- a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchEditTextTypingMicrobenchmark.java
+++ b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchEditTextTypingMicrobenchmark.java
@@ -20,10 +20,12 @@
 import android.platform.helpers.HelperAccessor;
 import android.platform.test.microbenchmark.Microbenchmark;
 import android.platform.test.rule.NaturalOrientationRule;
+import android.platform.test.rule.Dex2oatPressureRule;
 
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -34,6 +36,9 @@
     private static HelperAccessor<IUiBenchJankHelper> sHelper =
             new HelperAccessor<>(IUiBenchJankHelper.class);
 
+    @Rule
+    public Dex2oatPressureRule dex2oatPressureRule = new Dex2oatPressureRule();
+
     @BeforeClass
     public static void openApp() {
         sHelper.get().openEditTextTyping();
diff --git a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchFadingEdgeListViewFlingMicrobenchmark.java b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchFadingEdgeListViewFlingMicrobenchmark.java
index 212ef78..c8717a7 100644
--- a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchFadingEdgeListViewFlingMicrobenchmark.java
+++ b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchFadingEdgeListViewFlingMicrobenchmark.java
@@ -19,10 +19,12 @@
 import android.platform.helpers.HelperAccessor;
 import android.platform.test.microbenchmark.Microbenchmark;
 import android.platform.test.rule.NaturalOrientationRule;
+import android.platform.test.rule.Dex2oatPressureRule;
 
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -33,6 +35,9 @@
     private static HelperAccessor<IUiBenchJankHelper> sHelper =
             new HelperAccessor<>(IUiBenchJankHelper.class);
 
+    @Rule
+    public Dex2oatPressureRule dex2oatPressureRule = new Dex2oatPressureRule();
+
     @BeforeClass
     public static void openApp() {
         sHelper.get().openFadingEdgeListView();
diff --git a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchFullscreenOverdrawMicrobenchmark.java b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchFullscreenOverdrawMicrobenchmark.java
index 9c51488..e3b0cfe 100644
--- a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchFullscreenOverdrawMicrobenchmark.java
+++ b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchFullscreenOverdrawMicrobenchmark.java
@@ -20,10 +20,12 @@
 import android.platform.helpers.HelperAccessor;
 import android.platform.test.microbenchmark.Microbenchmark;
 import android.platform.test.rule.NaturalOrientationRule;
+import android.platform.test.rule.Dex2oatPressureRule;
 
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -34,6 +36,9 @@
     private static HelperAccessor<IUiBenchJankHelper> sHelper =
             new HelperAccessor<>(IUiBenchJankHelper.class);
 
+    @Rule
+    public Dex2oatPressureRule dex2oatPressureRule = new Dex2oatPressureRule();
+
     @BeforeClass
     public static void openApp() {
         sHelper.get().openFullscreenOverdraw();
diff --git a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchGLTextureViewMicrobenchmark.java b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchGLTextureViewMicrobenchmark.java
index 1c55438..8e62651 100644
--- a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchGLTextureViewMicrobenchmark.java
+++ b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchGLTextureViewMicrobenchmark.java
@@ -20,10 +20,12 @@
 import android.platform.helpers.HelperAccessor;
 import android.platform.test.microbenchmark.Microbenchmark;
 import android.platform.test.rule.NaturalOrientationRule;
+import android.platform.test.rule.Dex2oatPressureRule;
 
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -34,6 +36,9 @@
     private static HelperAccessor<IUiBenchJankHelper> sHelper =
             new HelperAccessor<>(IUiBenchJankHelper.class);
 
+    @Rule
+    public Dex2oatPressureRule dex2oatPressureRule = new Dex2oatPressureRule();
+
     @BeforeClass
     public static void openApp() {
         sHelper.get().openGLTextureView();
diff --git a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchInflatingEmojiListViewFlingMicrobenchmark.java b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchInflatingEmojiListViewFlingMicrobenchmark.java
index 2faead6..90e72b1 100644
--- a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchInflatingEmojiListViewFlingMicrobenchmark.java
+++ b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchInflatingEmojiListViewFlingMicrobenchmark.java
@@ -19,10 +19,12 @@
 import android.platform.helpers.HelperAccessor;
 import android.platform.test.microbenchmark.Microbenchmark;
 import android.platform.test.rule.NaturalOrientationRule;
+import android.platform.test.rule.Dex2oatPressureRule;
 
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -33,6 +35,9 @@
     private static HelperAccessor<IUiBenchJankHelper> sHelper =
             new HelperAccessor<>(IUiBenchJankHelper.class);
 
+    @Rule
+    public Dex2oatPressureRule dex2oatPressureRule = new Dex2oatPressureRule();
+
     @BeforeClass
     public static void openApp() {
         sHelper.get().openInflatingEmojiListView();
diff --git a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchInflatingHanListViewFlingMicrobenchmark.java b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchInflatingHanListViewFlingMicrobenchmark.java
index 0fea513..a4c57f4 100644
--- a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchInflatingHanListViewFlingMicrobenchmark.java
+++ b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchInflatingHanListViewFlingMicrobenchmark.java
@@ -19,10 +19,12 @@
 import android.platform.helpers.HelperAccessor;
 import android.platform.test.microbenchmark.Microbenchmark;
 import android.platform.test.rule.NaturalOrientationRule;
+import android.platform.test.rule.Dex2oatPressureRule;
 
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -33,6 +35,9 @@
     private static HelperAccessor<IUiBenchJankHelper> sHelper =
             new HelperAccessor<>(IUiBenchJankHelper.class);
 
+    @Rule
+    public Dex2oatPressureRule dex2oatPressureRule = new Dex2oatPressureRule();
+
     @BeforeClass
     public static void openApp() {
         sHelper.get().openInflatingHanListView();
diff --git a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchInflatingListViewFlingMicrobenchmark.java b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchInflatingListViewFlingMicrobenchmark.java
index 8ba8921..c14eb77 100644
--- a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchInflatingListViewFlingMicrobenchmark.java
+++ b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchInflatingListViewFlingMicrobenchmark.java
@@ -19,10 +19,12 @@
 import android.platform.helpers.HelperAccessor;
 import android.platform.test.microbenchmark.Microbenchmark;
 import android.platform.test.rule.NaturalOrientationRule;
+import android.platform.test.rule.Dex2oatPressureRule;
 
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -33,6 +35,9 @@
     private static HelperAccessor<IUiBenchJankHelper> sHelper =
             new HelperAccessor<>(IUiBenchJankHelper.class);
 
+    @Rule
+    public Dex2oatPressureRule dex2oatPressureRule = new Dex2oatPressureRule();
+
     @BeforeClass
     public static void openApp() {
         sHelper.get().openInflatingListView();
diff --git a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchInflatingLongStringListViewFlingMicrobenchmark.java b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchInflatingLongStringListViewFlingMicrobenchmark.java
index 27ae7ce..77c5b6a 100644
--- a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchInflatingLongStringListViewFlingMicrobenchmark.java
+++ b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchInflatingLongStringListViewFlingMicrobenchmark.java
@@ -19,10 +19,12 @@
 import android.platform.helpers.HelperAccessor;
 import android.platform.test.microbenchmark.Microbenchmark;
 import android.platform.test.rule.NaturalOrientationRule;
+import android.platform.test.rule.Dex2oatPressureRule;
 
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -33,6 +35,9 @@
     private static HelperAccessor<IUiBenchJankHelper> sHelper =
             new HelperAccessor<>(IUiBenchJankHelper.class);
 
+    @Rule
+    public Dex2oatPressureRule dex2oatPressureRule = new Dex2oatPressureRule();
+
     @BeforeClass
     public static void openApp() {
         sHelper.get().openInflatingLongStringListView();
diff --git a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchInvalidateMicrobenchmark.java b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchInvalidateMicrobenchmark.java
index 1d28e52..ec1f86d 100644
--- a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchInvalidateMicrobenchmark.java
+++ b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchInvalidateMicrobenchmark.java
@@ -20,10 +20,12 @@
 import android.platform.helpers.HelperAccessor;
 import android.platform.test.microbenchmark.Microbenchmark;
 import android.platform.test.rule.NaturalOrientationRule;
+import android.platform.test.rule.Dex2oatPressureRule;
 
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -34,6 +36,9 @@
     private static HelperAccessor<IUiBenchJankHelper> sHelper =
             new HelperAccessor<>(IUiBenchJankHelper.class);
 
+    @Rule
+    public Dex2oatPressureRule dex2oatPressureRule = new Dex2oatPressureRule();
+
     @BeforeClass
     public static void openApp() {
         sHelper.get().openInvalidate();
diff --git a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchInvalidateTreeMicrobenchmark.java b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchInvalidateTreeMicrobenchmark.java
index 2f333ef..8472b25 100644
--- a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchInvalidateTreeMicrobenchmark.java
+++ b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchInvalidateTreeMicrobenchmark.java
@@ -20,10 +20,12 @@
 import android.platform.helpers.HelperAccessor;
 import android.platform.test.microbenchmark.Microbenchmark;
 import android.platform.test.rule.NaturalOrientationRule;
+import android.platform.test.rule.Dex2oatPressureRule;
 
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -34,6 +36,9 @@
     private static HelperAccessor<IUiBenchJankHelper> sHelper =
             new HelperAccessor<>(IUiBenchJankHelper.class);
 
+    @Rule
+    public Dex2oatPressureRule dex2oatPressureRule = new Dex2oatPressureRule();
+
     @BeforeClass
     public static void openApp() {
         sHelper.get().openInvalidateTree();
diff --git a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchLayoutCacheHighHitrateFlingMicrobenchmark.java b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchLayoutCacheHighHitrateFlingMicrobenchmark.java
index 5314b82..c5982d0 100644
--- a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchLayoutCacheHighHitrateFlingMicrobenchmark.java
+++ b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchLayoutCacheHighHitrateFlingMicrobenchmark.java
@@ -19,10 +19,12 @@
 import android.platform.helpers.HelperAccessor;
 import android.platform.test.microbenchmark.Microbenchmark;
 import android.platform.test.rule.NaturalOrientationRule;
+import android.platform.test.rule.Dex2oatPressureRule;
 
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -33,6 +35,9 @@
     private static HelperAccessor<IUiBenchJankHelper> sHelper =
             new HelperAccessor<>(IUiBenchJankHelper.class);
 
+    @Rule
+    public Dex2oatPressureRule dex2oatPressureRule = new Dex2oatPressureRule();
+
     @BeforeClass
     public static void openApp() {
         sHelper.get().openLayoutCacheHighHitrate();
diff --git a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchLayoutCacheLowHitrateFlingMicrobenchmark.java b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchLayoutCacheLowHitrateFlingMicrobenchmark.java
index 37ca6dc..89ab93d 100644
--- a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchLayoutCacheLowHitrateFlingMicrobenchmark.java
+++ b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchLayoutCacheLowHitrateFlingMicrobenchmark.java
@@ -19,10 +19,12 @@
 import android.platform.helpers.HelperAccessor;
 import android.platform.test.microbenchmark.Microbenchmark;
 import android.platform.test.rule.NaturalOrientationRule;
+import android.platform.test.rule.Dex2oatPressureRule;
 
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -33,6 +35,9 @@
     private static HelperAccessor<IUiBenchJankHelper> sHelper =
             new HelperAccessor<>(IUiBenchJankHelper.class);
 
+    @Rule
+    public Dex2oatPressureRule dex2oatPressureRule = new Dex2oatPressureRule();
+
     @BeforeClass
     public static void openApp() {
         sHelper.get().openLayoutCacheLowHitrate();
diff --git a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchOpenNavigationDrawerMicrobenchmark.java b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchOpenNavigationDrawerMicrobenchmark.java
index a1ed02f..f2d473e 100644
--- a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchOpenNavigationDrawerMicrobenchmark.java
+++ b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchOpenNavigationDrawerMicrobenchmark.java
@@ -19,10 +19,12 @@
 import android.platform.helpers.HelperAccessor;
 import android.platform.test.microbenchmark.Microbenchmark;
 import android.platform.test.rule.NaturalOrientationRule;
+import android.platform.test.rule.Dex2oatPressureRule;
 
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -33,6 +35,9 @@
     private static HelperAccessor<IUiBenchJankHelper> sHelper =
             new HelperAccessor<>(IUiBenchJankHelper.class);
 
+    @Rule
+    public Dex2oatPressureRule dex2oatPressureRule = new Dex2oatPressureRule();
+
     @BeforeClass
     public static void openApp() {
         sHelper.get().openNavigationDrawerActivity();
diff --git a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchOpenNotificationShadeMicrobenchmark.java b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchOpenNotificationShadeMicrobenchmark.java
index b5f5cf6..5be6b36 100644
--- a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchOpenNotificationShadeMicrobenchmark.java
+++ b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchOpenNotificationShadeMicrobenchmark.java
@@ -19,10 +19,12 @@
 import android.platform.helpers.HelperAccessor;
 import android.platform.test.microbenchmark.Microbenchmark;
 import android.platform.test.rule.NaturalOrientationRule;
+import android.platform.test.rule.Dex2oatPressureRule;
 
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -33,6 +35,9 @@
     private static HelperAccessor<IUiBenchJankHelper> sHelper =
             new HelperAccessor<>(IUiBenchJankHelper.class);
 
+    @Rule
+    public Dex2oatPressureRule dex2oatPressureRule = new Dex2oatPressureRule();
+
     @BeforeClass
     public static void openApp() {
         sHelper.get().openNotificationShade();
diff --git a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchResizeHWLayerMicrobenchmark.java b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchResizeHWLayerMicrobenchmark.java
index 8405d8d..3a17f1a 100644
--- a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchResizeHWLayerMicrobenchmark.java
+++ b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchResizeHWLayerMicrobenchmark.java
@@ -20,10 +20,12 @@
 import android.platform.helpers.HelperAccessor;
 import android.platform.test.microbenchmark.Microbenchmark;
 import android.platform.test.rule.NaturalOrientationRule;
+import android.platform.test.rule.Dex2oatPressureRule;
 
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -34,6 +36,9 @@
     private static HelperAccessor<IUiBenchJankHelper> sHelper =
             new HelperAccessor<>(IUiBenchJankHelper.class);
 
+    @Rule
+    public Dex2oatPressureRule dex2oatPressureRule = new Dex2oatPressureRule();
+
     @BeforeClass
     public static void openApp() {
         sHelper.get().openResizeHWLayer();
diff --git a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchSaveLayerAnimationMicrobenchmark.java b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchSaveLayerAnimationMicrobenchmark.java
index e723c4b..93c9033 100644
--- a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchSaveLayerAnimationMicrobenchmark.java
+++ b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchSaveLayerAnimationMicrobenchmark.java
@@ -20,10 +20,12 @@
 import android.platform.helpers.HelperAccessor;
 import android.platform.test.microbenchmark.Microbenchmark;
 import android.platform.test.rule.NaturalOrientationRule;
+import android.platform.test.rule.Dex2oatPressureRule;
 
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -34,6 +36,9 @@
     private static HelperAccessor<IUiBenchJankHelper> sHelper =
             new HelperAccessor<>(IUiBenchJankHelper.class);
 
+    @Rule
+    public Dex2oatPressureRule dex2oatPressureRule = new Dex2oatPressureRule();
+
     @BeforeClass
     public static void openApp() {
         sHelper.get().openSaveLayerInterleaveActivity();
diff --git a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchShadowGridListFlingMicrobenchmark.java b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchShadowGridListFlingMicrobenchmark.java
index e7bd447..8893920 100644
--- a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchShadowGridListFlingMicrobenchmark.java
+++ b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchShadowGridListFlingMicrobenchmark.java
@@ -19,10 +19,12 @@
 import android.platform.helpers.HelperAccessor;
 import android.platform.test.microbenchmark.Microbenchmark;
 import android.platform.test.rule.NaturalOrientationRule;
+import android.platform.test.rule.Dex2oatPressureRule;
 
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -33,6 +35,9 @@
     private static HelperAccessor<IUiBenchJankHelper> sHelper =
             new HelperAccessor<>(IUiBenchJankHelper.class);
 
+    @Rule
+    public Dex2oatPressureRule dex2oatPressureRule = new Dex2oatPressureRule();
+
     @BeforeClass
     public static void openApp() {
         sHelper.get().openRenderingList();
diff --git a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchSlowBindRecyclerViewFlingMicrobenchmark.java b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchSlowBindRecyclerViewFlingMicrobenchmark.java
index 0869bec..f40d024 100644
--- a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchSlowBindRecyclerViewFlingMicrobenchmark.java
+++ b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchSlowBindRecyclerViewFlingMicrobenchmark.java
@@ -19,10 +19,12 @@
 import android.platform.helpers.HelperAccessor;
 import android.platform.test.microbenchmark.Microbenchmark;
 import android.platform.test.rule.NaturalOrientationRule;
+import android.platform.test.rule.Dex2oatPressureRule;
 
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -33,6 +35,9 @@
     private static HelperAccessor<IUiBenchJankHelper> sHelper =
             new HelperAccessor<>(IUiBenchJankHelper.class);
 
+    @Rule
+    public Dex2oatPressureRule dex2oatPressureRule = new Dex2oatPressureRule();
+
     @BeforeClass
     public static void openApp() {
         sHelper.get().openSlowBindRecyclerView();
diff --git a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchSlowNestedRecyclerViewFlingMicrobenchmark.java b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchSlowNestedRecyclerViewFlingMicrobenchmark.java
index 6222488..17943b6 100644
--- a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchSlowNestedRecyclerViewFlingMicrobenchmark.java
+++ b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchSlowNestedRecyclerViewFlingMicrobenchmark.java
@@ -19,10 +19,12 @@
 import android.platform.helpers.HelperAccessor;
 import android.platform.test.microbenchmark.Microbenchmark;
 import android.platform.test.rule.NaturalOrientationRule;
+import android.platform.test.rule.Dex2oatPressureRule;
 
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -33,6 +35,9 @@
     private static HelperAccessor<IUiBenchJankHelper> sHelper =
             new HelperAccessor<>(IUiBenchJankHelper.class);
 
+    @Rule
+    public Dex2oatPressureRule dex2oatPressureRule = new Dex2oatPressureRule();
+
     @BeforeClass
     public static void openApp() {
         sHelper.get().openSlowNestedRecyclerView();
diff --git a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchSlowNestedRecyclerViewInitialFlingMicrobenchmark.java b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchSlowNestedRecyclerViewInitialFlingMicrobenchmark.java
index 1d9f82c..ec0438f 100644
--- a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchSlowNestedRecyclerViewInitialFlingMicrobenchmark.java
+++ b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchSlowNestedRecyclerViewInitialFlingMicrobenchmark.java
@@ -19,10 +19,12 @@
 import android.platform.helpers.HelperAccessor;
 import android.platform.test.microbenchmark.Microbenchmark;
 import android.platform.test.rule.NaturalOrientationRule;
+import android.platform.test.rule.Dex2oatPressureRule;
 
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -33,6 +35,9 @@
     private static HelperAccessor<IUiBenchJankHelper> sHelper =
             new HelperAccessor<>(IUiBenchJankHelper.class);
 
+    @Rule
+    public Dex2oatPressureRule dex2oatPressureRule = new Dex2oatPressureRule();
+
     @BeforeClass
     public static void openApp() {
         sHelper.get().openSlowNestedRecyclerView();
diff --git a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchTrivialAnimationMicrobenchmark.java b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchTrivialAnimationMicrobenchmark.java
index 35c8e94..7bd8c40 100644
--- a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchTrivialAnimationMicrobenchmark.java
+++ b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchTrivialAnimationMicrobenchmark.java
@@ -20,10 +20,12 @@
 import android.platform.helpers.HelperAccessor;
 import android.platform.test.microbenchmark.Microbenchmark;
 import android.platform.test.rule.NaturalOrientationRule;
+import android.platform.test.rule.Dex2oatPressureRule;
 
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -34,6 +36,9 @@
     private static HelperAccessor<IUiBenchJankHelper> sHelper =
             new HelperAccessor<>(IUiBenchJankHelper.class);
 
+    @Rule
+    public Dex2oatPressureRule dex2oatPressureRule = new Dex2oatPressureRule();
+
     @BeforeClass
     public static void openApp() {
         sHelper.get().openTrivialAnimation();
diff --git a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchTrivialListViewFlingMicrobenchmark.java b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchTrivialListViewFlingMicrobenchmark.java
index 63dcde3..8b0efe2 100644
--- a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchTrivialListViewFlingMicrobenchmark.java
+++ b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchTrivialListViewFlingMicrobenchmark.java
@@ -19,10 +19,12 @@
 import android.platform.helpers.HelperAccessor;
 import android.platform.test.microbenchmark.Microbenchmark;
 import android.platform.test.rule.NaturalOrientationRule;
+import android.platform.test.rule.Dex2oatPressureRule;
 
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -33,6 +35,9 @@
     private static HelperAccessor<IUiBenchJankHelper> sHelper =
             new HelperAccessor<>(IUiBenchJankHelper.class);
 
+    @Rule
+    public Dex2oatPressureRule dex2oatPressureRule = new Dex2oatPressureRule();
+
     @BeforeClass
     public static void openApp() {
         sHelper.get().openTrivialListView();
diff --git a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchTrivialRecyclerListViewFlingMicrobenchmark.java b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchTrivialRecyclerListViewFlingMicrobenchmark.java
index ff09693..0abf322 100644
--- a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchTrivialRecyclerListViewFlingMicrobenchmark.java
+++ b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchTrivialRecyclerListViewFlingMicrobenchmark.java
@@ -19,10 +19,12 @@
 import android.platform.helpers.HelperAccessor;
 import android.platform.test.microbenchmark.Microbenchmark;
 import android.platform.test.rule.NaturalOrientationRule;
+import android.platform.test.rule.Dex2oatPressureRule;
 
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -33,6 +35,9 @@
     private static HelperAccessor<IUiBenchJankHelper> sHelper =
             new HelperAccessor<>(IUiBenchJankHelper.class);
 
+    @Rule
+    public Dex2oatPressureRule dex2oatPressureRule = new Dex2oatPressureRule();
+
     @BeforeClass
     public static void openApp() {
         sHelper.get().openTrivialRecyclerView();
diff --git a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchWebViewFlingMicrobenchmark.java b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchWebViewFlingMicrobenchmark.java
index 8744e3b..abadcf3 100644
--- a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchWebViewFlingMicrobenchmark.java
+++ b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchWebViewFlingMicrobenchmark.java
@@ -19,10 +19,12 @@
 import android.platform.helpers.HelperAccessor;
 import android.platform.test.microbenchmark.Microbenchmark;
 import android.platform.test.rule.NaturalOrientationRule;
+import android.platform.test.rule.Dex2oatPressureRule;
 
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -33,6 +35,9 @@
     private static HelperAccessor<IUiBenchJankHelper> sHelper =
             new HelperAccessor<>(IUiBenchJankHelper.class);
 
+    @Rule
+    public Dex2oatPressureRule dex2oatPressureRule = new Dex2oatPressureRule();
+
     @BeforeClass
     public static void openApp() {
         sHelper.get().openScrollableWebView();
diff --git a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchWindowInsetsControllerMicrobenchmark.java b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchWindowInsetsControllerMicrobenchmark.java
index 216ff30..c3a27c4 100644
--- a/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchWindowInsetsControllerMicrobenchmark.java
+++ b/tests/microbenchmarks/uibench/src/com/android/uibench/microbenchmark/UiBenchWindowInsetsControllerMicrobenchmark.java
@@ -23,6 +23,7 @@
 import android.platform.helpers.HelperAccessor;
 import android.platform.test.microbenchmark.Microbenchmark;
 import android.platform.test.rule.NaturalOrientationRule;
+import android.platform.test.rule.Dex2oatPressureRule;
 
 import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.uiautomator.By;
@@ -33,6 +34,7 @@
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -46,6 +48,9 @@
     private static UiObject2 sEditText;
     private static UiDevice sDevice;
 
+    @Rule
+    public Dex2oatPressureRule dex2oatPressureRule = new Dex2oatPressureRule();
+
     @BeforeClass
     public static void openApp() {
         sHelper.get().openWindowInsetsController();
diff --git a/tests/perf/BootHelperApp/Android.bp b/tests/perf/BootHelperApp/Android.bp
index 7238fc0..659925d 100644
--- a/tests/perf/BootHelperApp/Android.bp
+++ b/tests/perf/BootHelperApp/Android.bp
@@ -25,7 +25,7 @@
     sdk_version: "current",
     libs: ["android.test.runner.stubs"],
     static_libs: [
-        "android-support-test",
+        "androidx.test.rules",
         "ub-uiautomator",
     ],
 
diff --git a/tests/perf/BootHelperApp/AndroidManifest.xml b/tests/perf/BootHelperApp/AndroidManifest.xml
index 4539d32..0a87f94 100644
--- a/tests/perf/BootHelperApp/AndroidManifest.xml
+++ b/tests/perf/BootHelperApp/AndroidManifest.xml
@@ -21,7 +21,7 @@
     android:versionName="1.0" >
 
     <instrumentation
-        android:name="android.support.test.runner.AndroidJUnitRunner"
+        android:name="androidx.test.runner.AndroidJUnitRunner"
         android:targetPackage="com.android.boothelper" />
 
     <application>
diff --git a/tests/perf/BootHelperApp/AndroidTest.xml b/tests/perf/BootHelperApp/AndroidTest.xml
index e187c5a..57f4dac 100644
--- a/tests/perf/BootHelperApp/AndroidTest.xml
+++ b/tests/perf/BootHelperApp/AndroidTest.xml
@@ -21,6 +21,6 @@
     <option name="test-tag" value="BootHelperApp" />
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="com.android.boothelper" />
-        <option name="runner" value="android.support.test.runner.AndroidJUnitRunner" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
     </test>
 </configuration>
diff --git a/tests/perf/BootHelperApp/src/com/android/boothelper/BootHelperTest.java b/tests/perf/BootHelperApp/src/com/android/boothelper/BootHelperTest.java
index cec70ba..b213b57 100644
--- a/tests/perf/BootHelperApp/src/com/android/boothelper/BootHelperTest.java
+++ b/tests/perf/BootHelperApp/src/com/android/boothelper/BootHelperTest.java
@@ -23,7 +23,6 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.support.test.InstrumentationRegistry;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.UiDevice;
 import android.support.test.uiautomator.UiObject2;
@@ -31,12 +30,12 @@
 import android.util.Log;
 import android.view.KeyEvent;
 
-import java.util.concurrent.CountDownLatch;
+import androidx.test.InstrumentationRegistry;
 
 import org.junit.Before;
 import org.junit.Test;
 
-
+import java.util.concurrent.CountDownLatch;
 
 public class BootHelperTest {
 
diff --git a/tests/perf/PerfTransitionTest/Android.bp b/tests/perf/PerfTransitionTest/Android.bp
index 911f45d..d5f7b22 100644
--- a/tests/perf/PerfTransitionTest/Android.bp
+++ b/tests/perf/PerfTransitionTest/Android.bp
@@ -25,7 +25,7 @@
     platform_apis: true,
     certificate: "platform",
     static_libs: [
-        "android-support-test",
+        "androidx.test.rules",
         "ub-uiautomator",
         "launcher-helper-lib",
         "sysui-helper",
diff --git a/tests/perf/PerfTransitionTest/AndroidManifest.xml b/tests/perf/PerfTransitionTest/AndroidManifest.xml
index 9b0a5b8..92ead6b 100644
--- a/tests/perf/PerfTransitionTest/AndroidManifest.xml
+++ b/tests/perf/PerfTransitionTest/AndroidManifest.xml
@@ -30,7 +30,7 @@
         android:targetSdkVersion="24" />
 
     <instrumentation
-        android:name="android.support.test.runner.AndroidJUnitRunner"
+        android:name="androidx.test.runner.AndroidJUnitRunner"
         android:targetPackage="com.android.apptransition.tests" />
 
     <application>
diff --git a/tests/perf/PerfTransitionTest/AndroidTest.xml b/tests/perf/PerfTransitionTest/AndroidTest.xml
index 8b83129..f43d80b 100644
--- a/tests/perf/PerfTransitionTest/AndroidTest.xml
+++ b/tests/perf/PerfTransitionTest/AndroidTest.xml
@@ -21,6 +21,6 @@
     <option name="test-tag" value="AppTransitionTests" />
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="com.android.apptransition.tests" />
-        <option name="runner" value="android.support.test.runner.AndroidJUnitRunner" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
     </test>
 </configuration>
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 2de10c2..b18427d 100644
--- a/tests/perf/PerfTransitionTest/src/com/android/apptransition/tests/AppTransitionTests.java
+++ b/tests/perf/PerfTransitionTest/src/com/android/apptransition/tests/AppTransitionTests.java
@@ -24,13 +24,14 @@
 import android.os.Bundle;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
-import android.support.test.InstrumentationRegistry;
 import android.support.test.launcherhelper.ILauncherStrategy;
 import android.support.test.launcherhelper.LauncherStrategyFactory;
-import android.support.test.rule.logging.AtraceLogger;
 import android.support.test.uiautomator.UiDevice;
 import android.util.Log;
 
+import androidx.test.InstrumentationRegistry;
+import androidx.test.rule.logging.AtraceLogger;
+
 import com.android.launcher3.tapl.LauncherInstrumentation;
 import com.android.launcher3.tapl.Workspace;
 
diff --git a/tests/perf/PerfTransitionTest/src/com/android/apptransition/tests/LatencyTests.java b/tests/perf/PerfTransitionTest/src/com/android/apptransition/tests/LatencyTests.java
index e482da7..8f7bae6 100644
--- a/tests/perf/PerfTransitionTest/src/com/android/apptransition/tests/LatencyTests.java
+++ b/tests/perf/PerfTransitionTest/src/com/android/apptransition/tests/LatencyTests.java
@@ -15,15 +15,13 @@
  */
 package com.android.apptransition.tests;
 
-import static android.support.test.InstrumentationRegistry.getInstrumentation;
+import static androidx.test.InstrumentationRegistry.getInstrumentation;
 
 import android.app.Instrumentation;
 import android.os.Bundle;
 import android.os.SystemClock;
 import android.provider.Settings;
-import android.support.test.InstrumentationRegistry;
 import android.support.test.launcherhelper.LauncherStrategyFactory;
-import android.support.test.rule.logging.AtraceLogger;
 import android.support.test.uiautomator.UiDevice;
 import android.system.helpers.LockscreenHelper;
 import android.system.helpers.OverviewHelper;
@@ -32,6 +30,9 @@
 import android.view.Surface;
 import android.view.WindowManagerGlobal;
 
+import androidx.test.InstrumentationRegistry;
+import androidx.test.rule.logging.AtraceLogger;
+
 import com.android.launcher3.tapl.LauncherInstrumentation;
 
 import org.junit.Before;
diff --git a/tests/perf/PerformanceAppTest/Android.bp b/tests/perf/PerformanceAppTest/Android.bp
index ae2d351..8d9a091 100644
--- a/tests/perf/PerformanceAppTest/Android.bp
+++ b/tests/perf/PerformanceAppTest/Android.bp
@@ -25,7 +25,7 @@
     sdk_version: "current",
     certificate: "platform",
     libs: ["android.test.base.stubs"],
-    static_libs: ["android-support-test"],
+    static_libs: ["androidx.test.rules"],
 
     test_suites: ["device-tests"],
 }
diff --git a/tests/perf/PerformanceAppTest/AndroidManifest.xml b/tests/perf/PerformanceAppTest/AndroidManifest.xml
index f526ec7..6729459 100644
--- a/tests/perf/PerformanceAppTest/AndroidManifest.xml
+++ b/tests/perf/PerformanceAppTest/AndroidManifest.xml
@@ -27,7 +27,7 @@
         android:targetSdkVersion="22" />
 
     <instrumentation
-        android:name="android.support.test.runner.AndroidJUnitRunner"
+        android:name="androidx.test.runner.AndroidJUnitRunner"
         android:targetPackage="com.android.performanceapp.tests" />
 
     <application
diff --git a/tests/perf/PerformanceAppTest/AndroidTest.xml b/tests/perf/PerformanceAppTest/AndroidTest.xml
index 09686fa..0e826a4 100644
--- a/tests/perf/PerformanceAppTest/AndroidTest.xml
+++ b/tests/perf/PerformanceAppTest/AndroidTest.xml
@@ -21,6 +21,6 @@
     <option name="test-tag" value="PerformanceAppTest" />
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="com.android.performanceapp.tests" />
-        <option name="runner" value="android.support.test.runner.AndroidJUnitRunner" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
     </test>
 </configuration>
diff --git a/tests/perf/PerformanceAppTest/src/com/android/performanceapp/tests/AppLaunchTests.java b/tests/perf/PerformanceAppTest/src/com/android/performanceapp/tests/AppLaunchTests.java
index 216fcd4..7d108ce 100644
--- a/tests/perf/PerformanceAppTest/src/com/android/performanceapp/tests/AppLaunchTests.java
+++ b/tests/perf/PerformanceAppTest/src/com/android/performanceapp/tests/AppLaunchTests.java
@@ -16,16 +16,6 @@
 
 package com.android.performanceapp.tests;
 
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.ArrayList;
-import java.util.List;
-
 import android.app.ActivityManager;
 import android.app.ActivityManager.RunningAppProcessInfo;
 import android.content.ComponentName;
@@ -38,11 +28,21 @@
 import android.os.Bundle;
 import android.os.Environment;
 import android.os.ParcelFileDescriptor;
-import android.support.test.InstrumentationRegistry;
 import android.test.InstrumentationTestCase;
 import android.test.suitebuilder.annotation.MediumTest;
 import android.util.Log;
 
+import androidx.test.InstrumentationRegistry;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
 /**
  * To test the App launch performance on the given target package for the list of activities. It
  * launches the activities present in the target package the number of times in launch count or it
diff --git a/tests/perf/PerformanceLaunch/AndroidTest.xml b/tests/perf/PerformanceLaunch/AndroidTest.xml
index 2a9469b..019a23b 100644
--- a/tests/perf/PerformanceLaunch/AndroidTest.xml
+++ b/tests/perf/PerformanceLaunch/AndroidTest.xml
@@ -21,6 +21,6 @@
     <option name="test-tag" value="PerformanceLaunch" />
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="com.android.performanceLaunch" />
-        <option name="runner" value="android.support.test.runner.AndroidJUnitRunner" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
     </test>
 </configuration>
diff --git a/utils/dpad/Android.bp b/utils/dpad/Android.bp
index 0e1ce5d..4d01673 100644
--- a/utils/dpad/Android.bp
+++ b/utils/dpad/Android.bp
@@ -24,7 +24,7 @@
     name: "dpad-util",
     libs: [
         "ub-uiautomator",
-        "android-support-test",
+        "androidx.test.rules",
     ],
     srcs: ["src/**/*.java"],
     sdk_version: "current",