Merge "Add binder_thread_stats into native_metric_test_list"
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index e011aa7..ec5af4b 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -7,6 +7,6 @@
 [Hook Scripts]
 checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
 
-ktlint_hook = ${REPO_ROOT}/prebuilts/ktlint/ktlint.py -f ${PREUPLOAD_FILES}
+ktlint_hook = ${REPO_ROOT}/prebuilts/ktlint/ktlint.py --no-verify-format -f ${PREUPLOAD_FILES}
 
 ktfmt_hook = ${REPO_ROOT}/external/ktfmt/ktfmt.py --check -i ${REPO_ROOT}/platform_testing/ktfmt_includes.txt ${PREUPLOAD_FILES}
diff --git a/libraries/app-helpers/interfaces/auto/OWNERS b/libraries/app-helpers/interfaces/auto/OWNERS
index 9ddcc33..4beb638 100644
--- a/libraries/app-helpers/interfaces/auto/OWNERS
+++ b/libraries/app-helpers/interfaces/auto/OWNERS
@@ -1,3 +1,2 @@
-aceyansf@google.com
+schinchalkar@google.com
 smara@google.com
-tongfei@google.com
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 49174ab..7292f95 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
@@ -18,7 +18,7 @@
 
 import java.util.List;
 
-public interface IAutoMediaHelper extends IAppHelper {
+public interface IAutoMediaHelper extends IAppHelper, Scrollable {
     /**
      * Setup expectations: media app is open
      *
@@ -170,4 +170,18 @@
      * @return Error message for no user login
      */
     String getMediaAppUserNotLoggedInErrorMessage();
+
+    /**
+     * Setup expectations: In Media.
+     *
+     * <p>Scroll up on page.
+     */
+    boolean scrollUpOnePage();
+
+    /**
+     * Setup expectations: In Media.
+     *
+     * <p>Scroll down on page.
+     */
+    boolean scrollDownOnePage();
 }
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 faa1035..6fd7e71 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
@@ -243,4 +243,25 @@
      * @return true if pip mode in launhcer.
      */
     public boolean isYouTubePipModeOnLauncher();
+
+    /**
+     * Setup expectation: YouTube is open and the video player is visible on any page.
+     *
+     * @return true if the video is playing.
+     */
+    public boolean isVideoPlaying();
+
+    /**
+     * Setup expectation: YouTube app is open.
+     *
+     * @return true if the floaty player is visible.
+     */
+    public boolean isFloatyPlayerVisible();
+
+    /**
+     * Setup expectation: YouTube is open and the floaty player is visible.
+     *
+     * <p>Clicks the floaty bar's close button to close video player.
+     */
+    public void closeFloatyPlayer();
 }
diff --git a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/ISettingsHomeHelper.java b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/ISettingsHomeHelper.java
index 1129686..67aded4 100644
--- a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/ISettingsHomeHelper.java
+++ b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/ISettingsHomeHelper.java
@@ -29,12 +29,24 @@
     /** This method opens Settings > Accessibility page */
     void goToAccessibility();
 
+    /** This method opens Settings > Apps page */
+    void goToApps();
+
+    /** This method opens Settings > Connected devices page */
+    void goToConnectedDevices();
+
     /** This method opens Settings > Display page */
     void goToDisplay();
 
     /** This method opens Settings > Network & internet page */
     void goToNetworkAndInternet();
 
+    /** This method opens Settings > Notifications page */
+    void goToNotifications();
+
+    /** This method opens Settings > Sound & vibration page */
+    void goToSoundAndVibration();
+
     /** This method opens Settings > Storage page */
     void goToStorage();
 
diff --git a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/apps/ISettingsAllAppsHelper.java b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/apps/ISettingsAllAppsHelper.java
new file mode 100644
index 0000000..2af7001
--- /dev/null
+++ b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/apps/ISettingsAllAppsHelper.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * <p>Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * <p>http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * <p>Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 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.settings.apps;
+
+import android.support.test.uiautomator.Direction;
+
+/** Extends for Settings > Apps > All apps */
+public interface ISettingsAllAppsHelper extends ISettingsAppsHelper {
+
+    /**
+     * Setup expectations: Settings All apps page is open
+     *
+     * <p>This method flings Settings All apps page.
+     */
+    void flingAllApps(Direction direction);
+
+    /**
+     * Setup expectations: Settings All apps page is open
+     *
+     * <p>This method validates Settings All apps page.
+     */
+    void isAllAppsPage();
+}
diff --git a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/apps/ISettingsAppsHelper.java b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/apps/ISettingsAppsHelper.java
new file mode 100644
index 0000000..52b9e47
--- /dev/null
+++ b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/apps/ISettingsAppsHelper.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * <p>Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * <p>http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * <p>Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 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.settings.apps;
+
+import android.platform.helpers.settings.ISettingsHomeHelper;
+
+/** Extends for Settings > Apps */
+public interface ISettingsAppsHelper extends ISettingsHomeHelper {
+
+    /** This method opens Settings > Apps > All apps page */
+    void goToAllApps();
+
+    /**
+     * Setup expectations: Settings Apps page is open
+     *
+     * <p>This method validates Settings Apps page.
+     */
+    void isAppsPage();
+}
diff --git a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/connected_devices/ISettingsConnectedDevicesHelper.java b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/connected_devices/ISettingsConnectedDevicesHelper.java
new file mode 100644
index 0000000..aa3853a
--- /dev/null
+++ b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/connected_devices/ISettingsConnectedDevicesHelper.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * <p>Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * <p>http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * <p>Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 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.settings.connected_devices;
+
+import android.platform.helpers.settings.ISettingsHomeHelper;
+
+/** Extends for Settings > Connected devices */
+public interface ISettingsConnectedDevicesHelper extends ISettingsHomeHelper {
+
+    /** This method opens Settings > Connected devices > USB (Preferences) page */
+    void goToUsb();
+
+    /**
+     * Setup expectations: Settings Connected devices page is open
+     *
+     * <p>This method validates Settings Connected devices page.
+     */
+    void isConnectedDevicesPage();
+}
diff --git a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/connected_devices/ISettingsUsbPreferencesHelper.java b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/connected_devices/ISettingsUsbPreferencesHelper.java
new file mode 100644
index 0000000..94b5304
--- /dev/null
+++ b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/connected_devices/ISettingsUsbPreferencesHelper.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * <p>Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * <p>http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * <p>Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 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.settings.connected_devices;
+
+import android.support.test.uiautomator.Direction;
+
+/** Extends for Settings > Connected devices > USB Preferences */
+public interface ISettingsUsbPreferencesHelper extends ISettingsConnectedDevicesHelper {
+
+    /**
+     * Setup expectations: Settings USB Preferences page is open
+     *
+     * <p>This method flings Settings USB Preferences page.
+     */
+    void flingUsbPreferences(Direction direction);
+
+    /**
+     * Setup expectations: Settings USB Preferences page is open
+     *
+     * <p>This method validates Settings USB Preferences page.
+     */
+    void isUsbPreferencesPage();
+}
diff --git a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/display/ISettingsDisplayHelper.java b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/display/ISettingsDisplayHelper.java
index bccb085..c328714 100644
--- a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/display/ISettingsDisplayHelper.java
+++ b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/display/ISettingsDisplayHelper.java
@@ -26,6 +26,9 @@
      */
     void flingDisplay(Direction direction);
 
+    /** This method opens Settings > Display > Display size and text page */
+    void goToDisplaySizeAndText();
+
     /**
      * Setup expectations: Settings Display page is open
      *
diff --git a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/display/ISettingsDisplaySizeAndTextHelper.java b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/display/ISettingsDisplaySizeAndTextHelper.java
new file mode 100644
index 0000000..9d87d51
--- /dev/null
+++ b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/display/ISettingsDisplaySizeAndTextHelper.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * <p>Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * <p>http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * <p>Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 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.settings.display;
+
+import android.support.test.uiautomator.Direction;
+
+/** Extends for Settings > Display > Display size and text */
+public interface ISettingsDisplaySizeAndTextHelper extends ISettingsDisplayHelper {
+
+    /**
+     * Setup expectations: Settings Display size and text page is open
+     *
+     * <p>This method flings Settings Display size and text page.
+     */
+    void flingDisplaySizeAndText(Direction direction);
+
+    /**
+     * Setup expectations: Settings Display size and text page is open
+     *
+     * <p>This method validates Settings Display size and text page.
+     */
+    void isDisplaySizeAndTextPage();
+}
diff --git a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/network_and_internet/ISettingsHotspotAndTetheringHelper.java b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/network_and_internet/ISettingsHotspotAndTetheringHelper.java
new file mode 100644
index 0000000..864988e
--- /dev/null
+++ b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/network_and_internet/ISettingsHotspotAndTetheringHelper.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * <p>Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * <p>http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * <p>Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 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.settings.network_and_internet;
+
+/** Extends for Settings > Network & internet > Hotspot & tethering */
+public interface ISettingsHotspotAndTetheringHelper extends ISettingsNetworkAndInternetHelper {
+
+    /**
+     * Setup expectations: Settings Hotspot & tethering page is open
+     *
+     * <p>This method validates Settings Hotspot & tethering page.
+     */
+    void isHotspotAndTetheringPage();
+}
diff --git a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/network_and_internet/ISettingsInternetHelper.java b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/network_and_internet/ISettingsInternetHelper.java
index 557f63b..273b03e 100644
--- a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/network_and_internet/ISettingsInternetHelper.java
+++ b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/network_and_internet/ISettingsInternetHelper.java
@@ -31,4 +31,11 @@
      * <p>This method validates Settings Internet page.
      */
     void isInternetPage();
+
+    /**
+     * Setup expectations: Settings Internet page is open
+     *
+     * <p>This method waits the "Searching For Networks..." text gone.
+     */
+    void waitSearchingForNetworksGone();
 }
diff --git a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/network_and_internet/ISettingsNetworkAndInternetHelper.java b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/network_and_internet/ISettingsNetworkAndInternetHelper.java
index fc43839..7dc16ba 100644
--- a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/network_and_internet/ISettingsNetworkAndInternetHelper.java
+++ b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/network_and_internet/ISettingsNetworkAndInternetHelper.java
@@ -26,6 +26,9 @@
      */
     void flingNetworkAndInternet(Direction direction);
 
+    /** This method opens Settings > Network & internet > Hotspot & tethering page */
+    void goToHotspotAndTethering();
+
     /** This method opens Settings > Network & internet > Internet page */
     void goToInternet();
 
diff --git a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/notifications/ISettingsAppNotificationsHelper.java b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/notifications/ISettingsAppNotificationsHelper.java
new file mode 100644
index 0000000..c8843b6
--- /dev/null
+++ b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/notifications/ISettingsAppNotificationsHelper.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * <p>Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * <p>http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * <p>Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 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.settings.notifications;
+
+/** Extends for Settings > Notifications > App notifications */
+public interface ISettingsAppNotificationsHelper extends ISettingsNotificationsHelper {
+
+    /**
+     * Setup expectations: Settings App notifications page is open
+     *
+     * <p>This method validates Settings App notifications page.
+     */
+    void isAppNotificationsPage();
+}
diff --git a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/notifications/ISettingsNotificationsHelper.java b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/notifications/ISettingsNotificationsHelper.java
new file mode 100644
index 0000000..7afc8cc
--- /dev/null
+++ b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/notifications/ISettingsNotificationsHelper.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * <p>Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * <p>http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * <p>Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 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.settings.notifications;
+
+import android.platform.helpers.settings.ISettingsHomeHelper;
+import android.support.test.uiautomator.Direction;
+
+/** Extends for Settings > Notifications */
+public interface ISettingsNotificationsHelper extends ISettingsHomeHelper {
+
+    /**
+     * Setup expectations: Settings Notifications page is open
+     *
+     * <p>This method flings Settings Notifications page.
+     */
+    void flingNotifications(Direction direction);
+
+    /** This method opens Settings > Notifications > App settings (notifications) page */
+    void goToAppSettings();
+
+    /**
+     * Setup expectations: Settings Notifications page is open
+     *
+     * <p>This method validates Settings Notifications page.
+     */
+    void isNotificationsPage();
+}
diff --git a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/sound_and_vibration/ISettingsPhoneRingtoneHelper.java b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/sound_and_vibration/ISettingsPhoneRingtoneHelper.java
new file mode 100644
index 0000000..01e31e9
--- /dev/null
+++ b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/sound_and_vibration/ISettingsPhoneRingtoneHelper.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * <p>Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * <p>http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * <p>Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 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.settings.sound_and_vibration;
+
+import android.support.test.uiautomator.Direction;
+
+/** Extends for Settings > Sound & vibration > Phone ringtone */
+public interface ISettingsPhoneRingtoneHelper extends ISettingsSoundAndVibrationHelper {
+
+    /**
+     * Setup expectations: Settings Phone ringtone page is open
+     *
+     * <p>This method flings Settings Phone ringtone page.
+     */
+    void flingPhoneRingtone(Direction direction);
+
+    /**
+     * Setup expectations: Settings Phone ringtone page is open
+     *
+     * <p>This method validates Settings Phone ringtone page.
+     */
+    void isPhoneRingtonePage();
+}
diff --git a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/sound_and_vibration/ISettingsSoundAndVibrationHelper.java b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/sound_and_vibration/ISettingsSoundAndVibrationHelper.java
new file mode 100644
index 0000000..9112457
--- /dev/null
+++ b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/settings/sound_and_vibration/ISettingsSoundAndVibrationHelper.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * <p>Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * <p>http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * <p>Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 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.settings.sound_and_vibration;
+
+import android.platform.helpers.settings.ISettingsHomeHelper;
+import android.support.test.uiautomator.Direction;
+
+/** Extends for Settings > Sound & vibration */
+public interface ISettingsSoundAndVibrationHelper extends ISettingsHomeHelper {
+
+    /**
+     * Setup expectations: Settings Sound & vibration page is open
+     *
+     * <p>This method flings Settings Sound & vibration page.
+     */
+    void flingSoundAndVibration(Direction direction);
+
+    /** This method opens Settings > Sound & vibration > Phone ringtone page */
+    void goToPhoneRingtone();
+
+    /**
+     * Setup expectations: Settings Sound & vibration page is open
+     *
+     * <p>This method validates Settings Sound & vibration page.
+     */
+    void isSoundAndVibrationPage();
+}
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
index 380b330..ec0bd53 100644
--- 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
@@ -26,6 +26,8 @@
 import android.car.drivingstate.CarDrivingStateEvent;
 import android.car.drivingstate.CarDrivingStateManager;
 import android.content.Context;
+import android.os.SystemClock;
+
 import androidx.test.InstrumentationRegistry;
 
 import java.util.regex.Matcher;
@@ -40,6 +42,7 @@
             "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 static final int UI_RESPONSE_WAIT_MS = 1000;
 
     private Car mCar;
     private Context mContext;
@@ -208,6 +211,7 @@
             executeShellCommand(ENABLE_PARKING_MODE);
         } else {
             executeShellCommand(ENABLE_DRIVING_MODE);
+            SystemClock.sleep(UI_RESPONSE_WAIT_MS);
         }
     }
 
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
index 1c61a6f..494448e 100644
--- 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
@@ -21,12 +21,17 @@
 import android.app.Instrumentation;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
+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.UiObject2;
 
 public class SettingsAppInfoHelperImpl extends AbstractAutoStandardAppHelper
         implements IAutoAppInfoSettingsHelper {
+
+    private static final int SHORT_UI_RESPONSE_TIME = 1000;
+
     public SettingsAppInfoHelperImpl(Instrumentation instr) {
         super(instr);
     }
@@ -152,9 +157,20 @@
                 scrollAndFindUiObject(permissions_selector, getScrollScreenIndex());
         clickAndWaitForWindowUpdate(
                 getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), permissions_menu);
+
+        UiObject2 scroller_object =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.SETTINGS,
+                                AutoConfigConstants.APPS_SETTINGS,
+                                AutoConfigConstants.ALLOWED_TEXT));
+        scroller_object.swipe(Direction.UP, 1.0f, 500);
         BySelector permission_selector = By.text(permission);
         UiObject2 permission_menu =
                 scrollAndFindUiObject(permission_selector, getScrollScreenIndex());
+        if (permission_menu == null) {
+            throw new RuntimeException("Cannot find the permission_selector" + permission);
+        }
         clickAndWaitForWindowUpdate(
                 getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), permission_menu);
         if (state == State.ENABLE) {
@@ -185,6 +201,8 @@
                     getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE),
                     dont_allow_anyway_btn);
         }
+
+        SystemClock.sleep(SHORT_UI_RESPONSE_TIME);
         pressBack();
         pressBack();
     }
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
index 8e530f4..bac3d38 100644
--- a/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoConfigConstants.java
+++ b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoConfigConstants.java
@@ -95,6 +95,7 @@
     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";
+    public static final String ALLOWED_TEXT = "ALLOWED_TEXT";
     // Date and Time
     public static final String DATE_AND_TIME_SETTINGS = "DATE_AND_TIME";
     public static final String SET_TIME_AUTOMATICALLY = "SET_TIME_AUTOMATICALLY";
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
index 6d0f043..c622cc6 100644
--- a/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoSettingsConfigUtility.java
+++ b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoSettingsConfigUtility.java
@@ -27,6 +27,7 @@
     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 SETTINGS_SHAREDLIBRARY_PACKAGE = "com.android.car.ui.sharedlibrary";
     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";
@@ -276,7 +277,7 @@
                 new AutoConfigResource(
                         AutoConfigConstants.RESOURCE_ID,
                         "car_ui_recycler_view",
-                        SETTING_INTELLIGENCE_PACKAGE));
+                        SETTINGS_SHAREDLIBRARY_PACKAGE));
         fullSettingsConfiguration.addResource(
                 AutoConfigConstants.UP_BUTTON,
                 new AutoConfigResource(AutoConfigConstants.DESCRIPTION, "Scroll up"));
@@ -385,6 +386,9 @@
         appsAndNotificationsSettingsConfiguration.addResource(
                 AutoConfigConstants.DONT_ALLOW_ANYWAY_BUTTON,
                 new AutoConfigResource(AutoConfigConstants.TEXT, "Don’t allow anyway"));
+        appsAndNotificationsSettingsConfiguration.addResource(
+                AutoConfigConstants.ALLOWED_TEXT,
+                new AutoConfigResource(AutoConfigConstants.TEXT, "Allowed"));
         mSettingsConfigMap.put(
                 AutoConfigConstants.APPS_SETTINGS, appsAndNotificationsSettingsConfiguration);
     }
diff --git a/libraries/compatibility-common-util/Android.bp b/libraries/compatibility-common-util/Android.bp
index 892f5af..6095f31 100644
--- a/libraries/compatibility-common-util/Android.bp
+++ b/libraries/compatibility-common-util/Android.bp
@@ -24,6 +24,7 @@
     srcs: ["src/**/*.java"],
 
     static_libs: [
+        "error_prone_annotations",
         "guava",
         "junit",
     ],
@@ -39,6 +40,7 @@
     srcs: ["src/**/*.java"],
     host_supported: true,
     libs: [
+        "error_prone_annotations",
         "junit",
         "guava",
         "json-prebuilt",
diff --git a/libraries/compatibility-common-util/src/com/android/compatibility/common/util/BackupUtils.java b/libraries/compatibility-common-util/src/com/android/compatibility/common/util/BackupUtils.java
index c636427..9648e47 100644
--- a/libraries/compatibility-common-util/src/com/android/compatibility/common/util/BackupUtils.java
+++ b/libraries/compatibility-common-util/src/com/android/compatibility/common/util/BackupUtils.java
@@ -20,6 +20,8 @@
 import static org.junit.Assert.fail;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.io.Closeables;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 
 import java.io.BufferedReader;
 import java.io.IOException;
@@ -55,8 +57,9 @@
 
     /**
      * Kicks off adb shell {@param command} and return an {@link InputStream} with the command
-     * output stream.
+     * output stream. The return value can be ignored and there might no need to close it.
      */
+    @CanIgnoreReturnValue
     protected abstract InputStream executeShellCommand(String command) throws IOException;
 
     public void executeShellCommandSync(String command) throws IOException {
@@ -64,7 +67,10 @@
     }
 
     public String getShellCommandOutput(String command) throws IOException {
-        return StreamUtil.readInputStream(executeShellCommand(command));
+        InputStream inputStream = executeShellCommand(command);
+        String result = StreamUtil.readInputStream(inputStream);
+        Closeables.closeQuietly(inputStream);
+        return result;
     }
 
     /** Executes shell command "bmgr backupnow <package>" and assert success. */
@@ -268,6 +274,7 @@
         while ((str = br.readLine()) != null) {
             out.append(str).append("\n");
         }
+        Closeables.closeQuietly(in);
         return out.toString();
     }
 
@@ -282,7 +289,7 @@
             throw new RuntimeException("non-parsable output setting bmgr enabled: " + output);
         }
 
-        executeShellCommand("bmgr enable " + enable);
+        Closeables.closeQuietly(executeShellCommand("bmgr enable " + enable));
         return previouslyEnabled;
     }
 
@@ -291,7 +298,7 @@
      */
     public boolean enableBackupForUser(boolean enable, int userId) throws IOException {
         boolean previouslyEnabled = isBackupEnabledForUser(userId);
-        executeShellCommand(String.format("bmgr --user %d enable %b", userId, enable));
+        executeShellCommandSync(String.format("bmgr --user %d enable %b", userId, enable));
         return previouslyEnabled;
     }
 
diff --git a/libraries/compatibility-common-util/src/com/android/compatibility/common/util/BusinessLogicExecutor.java b/libraries/compatibility-common-util/src/com/android/compatibility/common/util/BusinessLogicExecutor.java
index 8905fce..d72785e 100644
--- a/libraries/compatibility-common-util/src/com/android/compatibility/common/util/BusinessLogicExecutor.java
+++ b/libraries/compatibility-common-util/src/com/android/compatibility/common/util/BusinessLogicExecutor.java
@@ -16,6 +16,10 @@
 
 package com.android.compatibility.common.util;
 
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+
+import org.junit.AssumptionViolatedException;
+
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.util.ArrayList;
@@ -24,8 +28,6 @@
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
-import org.junit.AssumptionViolatedException;
-
 /**
  * Resolves methods provided by the BusinessLogicService and invokes them
  */
@@ -120,15 +122,17 @@
 
     /**
      * Execute a business logic method.
+     *
      * @param method the name of the method to invoke. Must include fully qualified name of the
-     * enclosing class, followed by '.', followed by the name of the method
+     *     enclosing class, followed by '.', followed by the name of the method
      * @param args the string arguments to supply to the method
      * @return the return value of the method invoked (type Boolean if method is a condition)
      * @throws RuntimeException when failing to resolve or invoke the method
      */
-    protected Object invokeMethod(String method, String... args) throws ClassNotFoundException,
-            IllegalAccessException, InstantiationException, InvocationTargetException,
-            NoSuchMethodException {
+    @CanIgnoreReturnValue
+    protected Object invokeMethod(String method, String... args)
+            throws ClassNotFoundException, IllegalAccessException, InstantiationException,
+                    InvocationTargetException, NoSuchMethodException {
         // Method names served by the BusinessLogic service should assume format
         // classname.methodName, but also handle format classname#methodName since test names use
         // this format
diff --git a/libraries/compatibility-common-util/src/com/android/compatibility/common/util/CrashUtils.java b/libraries/compatibility-common-util/src/com/android/compatibility/common/util/CrashUtils.java
index 5e0d127..49fdf6a 100644
--- a/libraries/compatibility-common-util/src/com/android/compatibility/common/util/CrashUtils.java
+++ b/libraries/compatibility-common-util/src/com/android/compatibility/common/util/CrashUtils.java
@@ -16,7 +16,14 @@
 
 package com.android.compatibility.common.util;
 
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
 import java.io.File;
+import java.math.BigInteger;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -27,10 +34,6 @@
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
 import java.util.stream.Stream;
-import java.math.BigInteger;
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
 
 /** Contains helper functions and shared constants for crash parsing. */
 public class CrashUtils {
@@ -368,72 +371,99 @@
             backtraceExcludes = new ArrayList();
         }
 
+        /** Sets the min address. */
+        @CanIgnoreReturnValue
         public Config setMinAddress(BigInteger minCrashAddress) {
             this.minCrashAddress = minCrashAddress;
             return this;
         }
 
+        /** Check the min address. */
+        @CanIgnoreReturnValue
         public Config checkMinAddress(boolean checkMinAddress) {
             this.checkMinAddress = checkMinAddress;
             return this;
         }
 
+        /** Set the signals. */
+        @CanIgnoreReturnValue
         public Config setSignals(String... signals) {
             this.signals = new ArrayList(Arrays.asList(signals));
             return this;
         }
 
+        /** Appends signals. */
+        @CanIgnoreReturnValue
         public Config appendSignals(String... signals) {
             Collections.addAll(this.signals, signals);
             return this;
         }
 
+        /** Set the abort message includes. */
+        @CanIgnoreReturnValue
         public Config setAbortMessageIncludes(String... abortMessages) {
             this.abortMessageIncludes = new ArrayList<>(toPatterns(abortMessages));
             return this;
         }
 
+        /** Set the abort message includes. */
+        @CanIgnoreReturnValue
         public Config setAbortMessageIncludes(Pattern... abortMessages) {
             this.abortMessageIncludes = new ArrayList<>(Arrays.asList(abortMessages));
             return this;
         }
 
+        /** Appends the abort message includes. */
+        @CanIgnoreReturnValue
         public Config appendAbortMessageIncludes(String... abortMessages) {
             this.abortMessageIncludes.addAll(toPatterns(abortMessages));
             return this;
         }
 
+        /** Appends the abort message includes. */
+        @CanIgnoreReturnValue
         public Config appendAbortMessageIncludes(Pattern... abortMessages) {
             Collections.addAll(this.abortMessageIncludes, abortMessages);
             return this;
         }
 
+        /** Sets the abort message excludes. */
+        @CanIgnoreReturnValue
         public Config setAbortMessageExcludes(String... abortMessages) {
             this.abortMessageExcludes = new ArrayList<>(toPatterns(abortMessages));
             return this;
         }
 
+        /** Sets the abort message excludes. */
+        @CanIgnoreReturnValue
         public Config setAbortMessageExcludes(Pattern... abortMessages) {
             this.abortMessageExcludes = new ArrayList<>(Arrays.asList(abortMessages));
             return this;
         }
 
+        /** Appends the process patterns. */
+        @CanIgnoreReturnValue
         public Config appendAbortMessageExcludes(String... abortMessages) {
             this.abortMessageExcludes.addAll(toPatterns(abortMessages));
             return this;
         }
 
+        /** Appends the abort message excludes. */
+        @CanIgnoreReturnValue
         public Config appendAbortMessageExcludes(Pattern... abortMessages) {
             Collections.addAll(this.abortMessageExcludes, abortMessages);
             return this;
         }
 
-
+        /** Sets the process patterns. */
+        @CanIgnoreReturnValue
         public Config setProcessPatterns(String... processPatternStrings) {
             this.processPatterns = new ArrayList<>(toPatterns(processPatternStrings));
             return this;
         }
 
+        /** Sets the process patterns. */
+        @CanIgnoreReturnValue
         public Config setProcessPatterns(Pattern... processPatterns) {
             this.processPatterns = new ArrayList(Arrays.asList(processPatterns));
             return this;
@@ -443,16 +473,22 @@
             return Collections.unmodifiableList(processPatterns);
         }
 
+        /** Appends the process patterns. */
+        @CanIgnoreReturnValue
         public Config appendProcessPatterns(String... processPatternStrings) {
             this.processPatterns.addAll(toPatterns(processPatternStrings));
             return this;
         }
 
+        /** Appends the process patterns. */
+        @CanIgnoreReturnValue
         public Config appendProcessPatterns(Pattern... processPatterns) {
             Collections.addAll(this.processPatterns, processPatterns);
             return this;
         }
 
+        /** Sets which backtraces should be included. */
+        @CanIgnoreReturnValue
         public Config setBacktraceIncludes(BacktraceFilterPattern... patterns) {
             this.backtraceIncludes = new ArrayList<>(Arrays.asList(patterns));
             return this;
@@ -462,11 +498,15 @@
             return Collections.unmodifiableList(this.backtraceIncludes);
         }
 
+        /** Append which backtraces should be included. */
+        @CanIgnoreReturnValue
         public Config appendBacktraceIncludes(BacktraceFilterPattern... patterns) {
             Collections.addAll(this.backtraceIncludes, patterns);
             return this;
         }
 
+        /** Sets which backtraces should be excluded. */
+        @CanIgnoreReturnValue
         public Config setBacktraceExcludes(BacktraceFilterPattern... patterns) {
             this.backtraceExcludes = new ArrayList<>(Arrays.asList(patterns));
             return this;
@@ -476,6 +516,8 @@
             return Collections.unmodifiableList(this.backtraceExcludes);
         }
 
+        /** Appends which backtraces should be excluded. */
+        @CanIgnoreReturnValue
         public Config appendBacktraceExcludes(BacktraceFilterPattern... patterns) {
             Collections.addAll(this.backtraceExcludes, patterns);
             return this;
diff --git a/libraries/compatibility-common-util/src/com/android/compatibility/common/util/LogcatInspector.java b/libraries/compatibility-common-util/src/com/android/compatibility/common/util/LogcatInspector.java
index 4614f18..bd7b00c 100644
--- a/libraries/compatibility-common-util/src/com/android/compatibility/common/util/LogcatInspector.java
+++ b/libraries/compatibility-common-util/src/com/android/compatibility/common/util/LogcatInspector.java
@@ -32,7 +32,7 @@
      */
     public String mark(String tag) throws IOException {
         String uniqueString = ":::" + UUID.randomUUID().toString();
-        executeShellCommand("log -t " + tag + " " + uniqueString);
+        Closeables.closeQuietly(executeShellCommand("log -t " + tag + " " + uniqueString));
         // This is to guarantee that we only return after the string has been logged, otherwise
         // in practice the case where calling Log.?(<message1>) right after clearAndMark() resulted
         // in <message1> appearing before the unique identifier. It's not guaranteed per the docs
diff --git a/libraries/compatibility-common-util/src/com/android/compatibility/common/util/MultipartForm.java b/libraries/compatibility-common-util/src/com/android/compatibility/common/util/MultipartForm.java
index c311492..11e2c58 100644
--- a/libraries/compatibility-common-util/src/com/android/compatibility/common/util/MultipartForm.java
+++ b/libraries/compatibility-common-util/src/com/android/compatibility/common/util/MultipartForm.java
@@ -16,6 +16,8 @@
 
 package com.android.compatibility.common.util;
 
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
@@ -52,6 +54,7 @@
      * @param value the attribute's value.
      * @return the {@link MultipartForm} for easy chaining.
      */
+    @CanIgnoreReturnValue
     public MultipartForm addFormValue(String name, String value) {
         mFormValues.put(name, value);
         return this;
@@ -65,6 +68,7 @@
      * @param data The file's data
      * @return the {@link MultipartForm} for easy chaining.
      */
+    @CanIgnoreReturnValue
     public MultipartForm addFormFile(String name, String fileName, byte[] data) {
         mName = name;
         mFileName = fileName;
diff --git a/libraries/compatibility-common-util/src/com/android/compatibility/common/util/ReadElf.java b/libraries/compatibility-common-util/src/com/android/compatibility/common/util/ReadElf.java
index 2bd401d..138100f 100644
--- a/libraries/compatibility-common-util/src/com/android/compatibility/common/util/ReadElf.java
+++ b/libraries/compatibility-common-util/src/com/android/compatibility/common/util/ReadElf.java
@@ -16,6 +16,8 @@
 
 package com.android.compatibility.common.util;
 
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+
 import java.io.File;
 import java.io.IOException;
 import java.io.RandomAccessFile;
@@ -1208,6 +1210,8 @@
         return mFile.read() & 0xff;
     }
 
+    /** Gets the symbol by name. */
+    @CanIgnoreReturnValue
     public Symbol getSymbol(String name) {
         if (mSymbols == null) {
             try {
@@ -1227,6 +1231,8 @@
         return mSymbols.get(name);
     }
 
+    /** Gets a dynamic symbol by name. */
+    @CanIgnoreReturnValue
     public Symbol getDynamicSymbol(String name) throws IOException {
         if (mDynamicSymbols == null) {
             try {
diff --git a/libraries/compatibility-common-util/src/com/android/compatibility/common/util/ResultHandler.java b/libraries/compatibility-common-util/src/com/android/compatibility/common/util/ResultHandler.java
index 2ed9a32..580cdf9 100644
--- a/libraries/compatibility-common-util/src/com/android/compatibility/common/util/ResultHandler.java
+++ b/libraries/compatibility-common-util/src/com/android/compatibility/common/util/ResultHandler.java
@@ -51,6 +51,7 @@
 import javax.xml.transform.TransformerFactory;
 import javax.xml.transform.stream.StreamResult;
 import javax.xml.transform.stream.StreamSource;
+
 /**
  * Handles conversion of results to/from files.
  *
@@ -597,7 +598,7 @@
         switch (retryStatus) {
             case NotRetry: case RetryWithChecksum:
                 // Do not disrupt the process if there is a problem generating checksum.
-                ChecksumReporter.tryCreateChecksum(resultDir, invocationResult);
+                boolean unused = ChecksumReporter.tryCreateChecksum(resultDir, invocationResult);
                 break;
             case RetryWithoutChecksum:
                 // If the previous run has an invalid checksum file,
diff --git a/libraries/health/rules/Android.bp b/libraries/health/rules/Android.bp
index a915e16..711fec8 100644
--- a/libraries/health/rules/Android.bp
+++ b/libraries/health/rules/Android.bp
@@ -32,6 +32,7 @@
         "statsd-helper",
         "health-testing-utils",
         "ub-uiautomator",
+        "uiautomator-helpers",
     ],
     srcs: [
         "src/**/*.java",
diff --git a/libraries/health/rules/src/android/platform/test/rule/AnnotationUtils.kt b/libraries/health/rules/src/android/platform/test/rule/AnnotationUtils.kt
new file mode 100644
index 0000000..00436cd
--- /dev/null
+++ b/libraries/health/rules/src/android/platform/test/rule/AnnotationUtils.kt
@@ -0,0 +1,11 @@
+package android.platform.test.rule
+
+/** Checks if the class, or any of its superclasses, have [annotation]. */
+fun <T> Class<T>?.hasAnnotation(annotation: Class<out Annotation>): Boolean =
+    if (this == null) {
+        false
+    } else if (isAnnotationPresent(annotation)) {
+        true
+    } else {
+        superclass.hasAnnotation(annotation)
+    }
diff --git a/libraries/health/rules/src/android/platform/test/rule/ArtifactSaver.java b/libraries/health/rules/src/android/platform/test/rule/ArtifactSaver.java
index 3b456fc..b8ee0e5 100644
--- a/libraries/health/rules/src/android/platform/test/rule/ArtifactSaver.java
+++ b/libraries/health/rules/src/android/platform/test/rule/ArtifactSaver.java
@@ -45,12 +45,17 @@
     }
 
     static File artifactFile(Description description, String prefix, String ext) {
+        String suffix = description.getMethodName();
+        if (suffix == null) {
+            // Can happen when the description is from a ClassRule
+            suffix = "EntireClassExecution";
+        }
         return artifactFile(
                 prefix
                         + "-"
                         + description.getTestClass().getSimpleName()
                         + "."
-                        + description.getMethodName()
+                        + suffix
                         + "."
                         + ext);
     }
diff --git a/libraries/health/rules/src/android/platform/test/rule/BaseOrientationRule.kt b/libraries/health/rules/src/android/platform/test/rule/BaseOrientationRule.kt
index 5c6f96d..92a8052 100644
--- a/libraries/health/rules/src/android/platform/test/rule/BaseOrientationRule.kt
+++ b/libraries/health/rules/src/android/platform/test/rule/BaseOrientationRule.kt
@@ -20,9 +20,10 @@
 import android.platform.test.rule.Orientation.PORTRAIT
 import android.platform.test.rule.RotationUtils.clearOrientationOverride
 import android.platform.test.rule.RotationUtils.setOrientationOverride
-import android.platform.test.util.HealthTestingUtils.waitForNullDiag
+import android.platform.test.util.HealthTestingUtils.waitForCondition
+import android.platform.test.util.HealthTestingUtils.waitForValueToSettle
+import android.util.Log
 import androidx.test.InstrumentationRegistry
-import androidx.test.uiautomator.By
 import androidx.test.uiautomator.UiDevice
 import com.android.launcher3.tapl.LauncherInstrumentation
 import org.junit.runner.Description
@@ -36,21 +37,32 @@
 /**
  * Possible device orientations.
  *
- * See [Rect.orientation] for their definitions.
+ * See [UiDevice.orientation] for their definitions.
  */
 enum class Orientation {
     LANDSCAPE,
     PORTRAIT,
 }
 
-private val Rect.orientation: Orientation
+/** Returns whether the device is landscape or portrait , based on display dimensions. */
+val UiDevice.orientation: Orientation
     get() =
-        if (width() > height()) {
+        if (displayWidth > displayHeight) {
             LANDSCAPE
         } else {
             PORTRAIT
         }
 
+// This makes sure that the orientation stabilised before returning it.
+private val UiDevice.stableOrientation: Orientation
+    get() =
+        waitForValueToSettle(
+            /* errorMessage= */ { "Device orientation didn't settle" },
+            /* supplier */ { orientation },
+            /* minimumSettleTime= */ 1_000,
+            /* timeoutMs= */ 5_000
+        )
+
 /** Uses launcher rect to decide which rotation to apply to match [expectedOrientation]. */
 sealed class BaseOrientationRule constructor(private val expectedOrientation: Orientation) :
     TestWatcher() {
@@ -81,19 +93,15 @@
      * change orientation in the middle.
      */
     fun setOrientationOverride(expectedOrientation: Orientation) {
-        device.pressHome()
         launcher.setEnableRotation(true)
-        if (launcherVisibleBounds?.orientation == expectedOrientation) {
+        if (device.stableOrientation == expectedOrientation) {
             return
         }
         changeOrientation()
-        waitForNullDiag {
-            when (launcherVisibleBounds?.orientation) {
-                expectedOrientation -> null // No error == success.
-                null -> "Launcher is not found"
-                else -> "Visible orientation is not ${expectedOrientation.name}"
-            }
+        waitForCondition({ "Visible orientation did not become  ${expectedOrientation.name}" }) {
+            device.stableOrientation == expectedOrientation
         }
+        log("Rotation override set to ${expectedOrientation.name}")
     }
 
     private fun changeOrientation() {
@@ -108,12 +116,8 @@
         device.setOrientationNatural()
         launcher.setEnableRotation(false)
         device.unfreezeRotation()
+        log("Rotation override cleared.")
     }
 
-    private val launcherVisibleBounds: Rect?
-        get() {
-            val launcher =
-                device.findObject(By.res("android", "content").pkg(device.launcherPackageName))
-            return launcher?.visibleBounds
-        }
+    private fun log(message: String) = Log.d("RotationUtils", message)
 }
diff --git a/libraries/health/rules/src/android/platform/test/rule/DeviceTypeRule.kt b/libraries/health/rules/src/android/platform/test/rule/DeviceTypeRule.kt
index ed03ad5..72c7dde 100644
--- a/libraries/health/rules/src/android/platform/test/rule/DeviceTypeRule.kt
+++ b/libraries/health/rules/src/android/platform/test/rule/DeviceTypeRule.kt
@@ -29,13 +29,14 @@
 import org.junit.runners.model.Statement
 
 /**
- * Rule that allow some tests to be executed only on [FoldableOnly], [LargeScreenOnly] or
- * [SmallScreenOnly] devices.
+ * Rule that allow some tests to be executed only on [FoldableOnly], [LargeScreenOnly], [TabletOnly]
+ * or [SmallScreenOnly] devices.
  */
 class DeviceTypeRule : TestRule {
 
     private val isFoldable = isFoldable()
     private val isLargeScreen = isLargeScreen()
+    private val isTablet = isTablet()
 
     override fun apply(base: Statement, description: Description): Statement {
         val smallScreenAnnotation = description.getAnnotation(SmallScreenOnly::class.java)
@@ -59,11 +60,17 @@
             )
         }
 
+        if (description.getAnnotation(TabletOnly::class.java) != null && !isTablet) {
+            return createAssumptionViolatedStatement(
+                "Skipping test on ${Build.PRODUCT} as it is not a tablet."
+            )
+        }
+
         return base
     }
 }
 
-private fun isFoldable(): Boolean {
+internal fun isFoldable(): Boolean {
     return getInstrumentation()
         .targetContext
         .resources
@@ -71,11 +78,16 @@
         .isNotEmpty()
 }
 
-private fun isLargeScreen(): Boolean {
+/** Returns whether the device default display is currently considered large screen. */
+fun isLargeScreen(): Boolean {
     val sizeDp = getUiDevice().displaySizeDp
     return sizeDp.x >= LARGE_SCREEN_DP_THRESHOLD && sizeDp.y >= LARGE_SCREEN_DP_THRESHOLD
 }
 
+internal fun isTablet(): Boolean {
+    return (isLargeScreen() && !isFoldable())
+}
+
 private fun createAssumptionViolatedStatement(message: String) =
     object : Statement() {
         override fun evaluate() {
@@ -102,3 +114,6 @@
 
 /** The test will run only on foldables. */
 @Retention(RUNTIME) @Target(ANNOTATION_CLASS, CLASS) annotation class FoldableOnly
+
+/** The test will run only on tablets. */
+@Retention(RUNTIME) @Target(ANNOTATION_CLASS, CLASS) annotation class TabletOnly
diff --git a/libraries/health/rules/src/android/platform/test/rule/KeepScreenAwakeRule.kt b/libraries/health/rules/src/android/platform/test/rule/KeepScreenAwakeRule.kt
new file mode 100644
index 0000000..81ecdc0
--- /dev/null
+++ b/libraries/health/rules/src/android/platform/test/rule/KeepScreenAwakeRule.kt
@@ -0,0 +1,21 @@
+package android.platform.test.rule
+
+import org.junit.runner.Description
+
+/**
+ * Making sure "Stay awake" setting from Developer settings is set so the screen doesn't turn off
+ * while tests are running
+ *
+ * Setting value is bit-based with 4 bits responsible for different types of charging. So the value
+ * is device-dependent but non-zero value means the settings is on.
+ * See [Settings.STAY_ON_WHILE_PLUGGED_IN] for more information.
+ */
+class KeepScreenAwakeRule : TestWatcher() {
+
+    override fun starting(description: Description?) {
+        val result = executeShellCommand("settings get global stay_on_while_plugged_in").trim()
+        if (result == "0") {
+            throw AssertionError("'Stay awake' option in developer settings should be enabled")
+        }
+    }
+}
diff --git a/libraries/health/rules/src/android/platform/test/rule/OrientationRule.kt b/libraries/health/rules/src/android/platform/test/rule/OrientationRule.kt
index c91b9db..761ab26 100644
--- a/libraries/health/rules/src/android/platform/test/rule/OrientationRule.kt
+++ b/libraries/health/rules/src/android/platform/test/rule/OrientationRule.kt
@@ -15,54 +15,71 @@
  */
 package android.platform.test.rule
 
+import android.platform.test.rule.DeviceTypeFilter.ANY
 import android.platform.test.rule.OrientationRule.Landscape
 import android.platform.test.rule.OrientationRule.Portrait
+import kotlin.annotation.AnnotationRetention.RUNTIME
+import kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS
+import kotlin.annotation.AnnotationTarget.CLASS
 import org.junit.rules.TestRule
 import org.junit.runner.Description
 import org.junit.runners.model.Statement
 
 /**
- * This rule will lock orientation before running a test class and unlock after. The orientation is
- * natural by default, and landscape or portrait if the test or one of its superclasses is marked
- * with the [Landscape] or [Portrait] annotation, .
+ * Locks orientation before running a test class and unlock after.
+ *
+ * The orientation is natural by default, and landscape or portrait if the test or one of its
+ * superclasses is marked with the [Landscape] or [Portrait] annotation.
+ *
+ * Important: if screen dimensions change in between the test, it is not guaranteed the orientation
+ * will match the one set. For example, if a two screens foldable device uses the [Portrait]
+ * annotation while folded, and then the screen is changed to a bigger one, it might result in the
+ * new orientation to be landscape instead (as the portrait orientation was leaving the device with
+ * the natural orientation, but with the big screen natural orientation is landscape).
  */
 class OrientationRule : TestRule {
 
     override fun apply(base: Statement, description: Description): Statement {
-        val testClass = description.testClass
-
-        val hasLandscapeAnnotation = testClass.hasAnnotation(Landscape::class.java)
-        val hasPortraitAnnotation = testClass.hasAnnotation(Portrait::class.java)
-        if (hasLandscapeAnnotation && hasPortraitAnnotation) {
-            throw IllegalStateException(
-                "Both @Portrait and @Landscape annotations at the same time are not yet supported."
-            )
-        }
+        val shouldSetLandscape = description.shouldSetLandscape()
+        val shouldSetPortrait = description.shouldSetPortrait()
 
         val orientationRule =
-            if (hasLandscapeAnnotation) {
-                LandscapeOrientationRule()
-            } else if (hasPortraitAnnotation) {
-                PortraitOrientationRule()
-            } else NaturalOrientationRule()
+            when {
+                shouldSetPortrait && shouldSetLandscape ->
+                    error("Can't set to both portrait and landscape. Double check test annotation.")
+                shouldSetLandscape -> LandscapeOrientationRule()
+                shouldSetPortrait -> PortraitOrientationRule()
+                else -> NaturalOrientationRule()
+            }
 
         return orientationRule.apply(base, description)
     }
 
-    private fun <T> Class<T>?.hasAnnotation(annotation: Class<out Annotation>): Boolean =
-        if (this == null) {
-            false
-        } else if (isAnnotationPresent(annotation)) {
-            true
-        } else {
-            superclass.hasAnnotation(annotation)
-        }
+    private fun Description.shouldSetLandscape(): Boolean =
+        getAnnotation(Landscape::class.java)?.deviceType?.any { it.match() } ?: false
 
-    @Retention(AnnotationRetention.RUNTIME)
-    @Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS)
-    annotation class Landscape
+    private fun Description.shouldSetPortrait(): Boolean =
+        getAnnotation(Portrait::class.java)?.deviceType?.any { it.match() } ?: false
 
-    @Retention(AnnotationRetention.RUNTIME)
-    @Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS)
-    annotation class Portrait
+    /**
+     * The orientation is applied only if the device type is within one of those in [deviceType].
+     */
+    @Retention(RUNTIME)
+    @Target(ANNOTATION_CLASS, CLASS)
+    annotation class Landscape(val deviceType: Array<DeviceTypeFilter> = [ANY])
+
+    /**
+     * The orientation is applied only if the device type is within one of those in [deviceType].
+     */
+    @Retention(RUNTIME)
+    @Target(ANNOTATION_CLASS, CLASS)
+    annotation class Portrait(val deviceType: Array<DeviceTypeFilter> = [ANY])
+}
+
+enum class DeviceTypeFilter(val match: () -> Boolean) {
+    TABLET({ isTablet() }),
+    FOLDABLE({ isFoldable() }),
+    LARGE_SCREEN({ isLargeScreen() }),
+    SMALL_SCREEN({ !isLargeScreen() }),
+    ANY({ true })
 }
diff --git a/libraries/health/rules/src/android/platform/test/rule/PlatinumRule.java b/libraries/health/rules/src/android/platform/test/rule/PlatinumRule.java
index f31d295..f8bab42 100644
--- a/libraries/health/rules/src/android/platform/test/rule/PlatinumRule.java
+++ b/libraries/health/rules/src/android/platform/test/rule/PlatinumRule.java
@@ -58,7 +58,8 @@
             flavor =
                     UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
                             .executeShellCommand("getprop ro.build.flavor")
-                            .replaceAll("\\s", "");
+                            .replaceAll("\\s", "")
+                            .replace("-userdebug", "");
         } catch (IOException e) {
             throw new RuntimeException(e);
         }
diff --git a/libraries/health/rules/src/android/platform/test/rule/PortraitLandscapeRule.kt b/libraries/health/rules/src/android/platform/test/rule/PortraitLandscapeRule.kt
new file mode 100644
index 0000000..fbebe26
--- /dev/null
+++ b/libraries/health/rules/src/android/platform/test/rule/PortraitLandscapeRule.kt
@@ -0,0 +1,35 @@
+package android.platform.test.rule
+
+import android.platform.test.rule.Orientation.LANDSCAPE
+import android.platform.test.rule.Orientation.PORTRAIT
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * Makes each test of the class that uses this rule execute twice, in [Orientation.LANDSCAPE] and
+ * [Orientation.PORTRAIT] orientation.
+ */
+class PortraitLandscapeRule : TestRule {
+
+    override fun apply(base: Statement, description: Description): Statement =
+        object : Statement() {
+            override fun evaluate() {
+                try {
+                    base.runInOrientation(PORTRAIT)
+                    base.runInOrientation(LANDSCAPE)
+                } finally {
+                    RotationUtils.clearOrientationOverride()
+                }
+            }
+        }
+
+    private fun Statement.runInOrientation(orientation: Orientation) {
+        RotationUtils.setOrientationOverride(orientation)
+        try {
+            evaluate()
+        } catch (e: Throwable) {
+            throw Exception("Test failed while in $orientation", e)
+        }
+    }
+}
diff --git a/libraries/health/rules/src/android/platform/test/rule/PresubmitRule.java b/libraries/health/rules/src/android/platform/test/rule/PresubmitRule.java
index fad674f..662c12d 100644
--- a/libraries/health/rules/src/android/platform/test/rule/PresubmitRule.java
+++ b/libraries/health/rules/src/android/platform/test/rule/PresubmitRule.java
@@ -63,7 +63,8 @@
             flavor =
                     UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
                             .executeShellCommand("getprop ro.build.flavor")
-                            .replaceAll("\\s", "");
+                            .replaceAll("\\s", "")
+                            .replace("-userdebug", "");
         } catch (IOException e) {
             throw new RuntimeException(e);
         }
diff --git a/libraries/health/rules/src/android/platform/test/rule/ScreenRecordRule.java b/libraries/health/rules/src/android/platform/test/rule/ScreenRecordRule.java
deleted file mode 100644
index b082184..0000000
--- a/libraries/health/rules/src/android/platform/test/rule/ScreenRecordRule.java
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * 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 androidx.test.InstrumentationRegistry.getInstrumentation;
-
-import android.app.Instrumentation;
-import android.app.UiAutomation;
-import android.os.ParcelFileDescriptor;
-import android.util.Log;
-
-import androidx.test.uiautomator.UiDevice;
-
-import org.junit.rules.TestRule;
-import org.junit.runner.Description;
-import org.junit.runners.model.Statement;
-
-import java.io.File;
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * Rule which captures a screen record for a test. After adding this rule to the test class, apply
- * the annotation @ScreenRecord to individual tests
- */
-public class ScreenRecordRule implements TestRule {
-
-    private static final String TAG = "ScreenRecordRule";
-
-    public static void runWithRecording(ThrowingRunnable runnable, Description description)
-            throws Throwable {
-        Instrumentation inst = getInstrumentation();
-        UiAutomation automation = inst.getUiAutomation();
-        UiDevice device = UiDevice.getInstance(inst);
-
-        File outputFile = ArtifactSaver.artifactFile(description, "ScreenRecord", "mp4");
-        device.executeShellCommand("killall screenrecord");
-        ParcelFileDescriptor output = automation.executeShellCommand("screenrecord " + outputFile);
-        String screenRecordPid = device.executeShellCommand("pidof screenrecord");
-        try {
-            runnable.run();
-        } finally {
-            device.executeShellCommand("kill -INT " + screenRecordPid);
-            Log.e(TAG, "Screenrecord captured at: " + outputFile);
-            output.close();
-        }
-        automation.executeShellCommand("rm " + outputFile);
-    }
-
-    @Override
-    public Statement apply(Statement base, Description description) {
-        if (description.getAnnotation(ScreenRecord.class) == null) {
-            return base;
-        }
-
-        return new Statement() {
-            @Override
-            public void evaluate() throws Throwable {
-                runWithRecording(base::evaluate, description);
-            }
-        };
-    }
-
-    /** Interface to indicate that the test should capture screenrecord */
-    @Retention(RetentionPolicy.RUNTIME)
-    @Target(ElementType.METHOD)
-    public @interface ScreenRecord {}
-
-    public interface ThrowingRunnable {
-        void run() throws Throwable;
-    }
-}
diff --git a/libraries/health/rules/src/android/platform/test/rule/ScreenRecordRule.kt b/libraries/health/rules/src/android/platform/test/rule/ScreenRecordRule.kt
new file mode 100644
index 0000000..65e0c8a
--- /dev/null
+++ b/libraries/health/rules/src/android/platform/test/rule/ScreenRecordRule.kt
@@ -0,0 +1,172 @@
+/*
+ * 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 android.app.UiAutomation
+import android.os.ParcelFileDescriptor
+import android.os.ParcelFileDescriptor.AutoCloseInputStream
+import android.platform.test.rule.ScreenRecordRule.Companion.SCREEN_RECORDING_TEST_LEVEL_OVERRIDE_KEY
+import android.platform.uiautomator_helpers.DeviceHelpers.shell
+import android.platform.uiautomator_helpers.DeviceHelpers.uiDevice
+import android.platform.uiautomator_helpers.FailedEnsureException
+import android.platform.uiautomator_helpers.WaitUtils.ensureThat
+import android.util.Log
+import androidx.test.InstrumentationRegistry.getInstrumentation
+import androidx.test.platform.app.InstrumentationRegistry
+import java.io.File
+import java.lang.annotation.Retention
+import java.lang.annotation.RetentionPolicy
+import java.nio.file.Files
+import kotlin.annotation.AnnotationTarget.CLASS
+import kotlin.annotation.AnnotationTarget.FUNCTION
+import kotlin.annotation.AnnotationTarget.PROPERTY_GETTER
+import kotlin.annotation.AnnotationTarget.PROPERTY_SETTER
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * Rule which captures a screen record for a test.
+ *
+ * After adding this rule to the test class either:
+ * - apply the annotation [ScreenRecord] to individual tests or classes
+ * - pass the [SCREEN_RECORDING_TEST_LEVEL_OVERRIDE_KEY] or
+ * [SCREEN_RECORDING_CLASS_LEVEL_OVERRIDE_KEY] instrumentation argument. e.g. `adb shell am
+ * instrument -w -e <key> true <test>`).
+ */
+class ScreenRecordRule : TestRule {
+
+    private val automation: UiAutomation = getInstrumentation().uiAutomation
+
+    override fun apply(base: Statement, description: Description): Statement {
+        if (!shouldRecordScreen(description)) {
+            log("Not recording the screen.")
+            return base
+        }
+        return object : Statement() {
+            override fun evaluate() {
+                runWithRecording(description) { base.evaluate() }
+            }
+        }
+    }
+
+    private fun shouldRecordScreen(description: Description): Boolean {
+        return if (description.isTest) {
+            description.getAnnotation(ScreenRecord::class.java) != null ||
+                testLevelOverrideEnabled()
+        } else { // class level
+            description.testClass.hasAnnotation(ScreenRecord::class.java) ||
+                classLevelOverrideEnabled()
+        }
+    }
+
+    private fun classLevelOverrideEnabled() =
+        screenRecordOverrideEnabled(SCREEN_RECORDING_CLASS_LEVEL_OVERRIDE_KEY)
+    private fun testLevelOverrideEnabled() =
+        screenRecordOverrideEnabled(SCREEN_RECORDING_TEST_LEVEL_OVERRIDE_KEY)
+    /**
+     * This is needed to enable screen recording when a parameter is passed to the instrumentation,
+     * avoid having to recompile the test.
+     */
+    private fun screenRecordOverrideEnabled(key: String): Boolean {
+        val args = InstrumentationRegistry.getArguments()
+        val override = args.getString(key, "false").toBoolean()
+        if (override) {
+            log("Screen recording enabled due to $key param.")
+        }
+        return override
+    }
+
+    private fun runWithRecording(description: Description?, runnable: () -> Unit) {
+        val outputFile = ArtifactSaver.artifactFile(description, "ScreenRecord", "mp4")
+        log("Executing test with screen recording. Output file=$outputFile")
+
+        if (screenRecordingInProgress()) {
+            Log.w(
+                TAG,
+                "Multiple screen recording in progress (pids=\"$screenrecordPids\"). " +
+                    "This might cause performance issues."
+            )
+        }
+        // --bugreport adds the timestamp as overlay
+        val screenRecordingFileDescriptor =
+            automation.executeShellCommand("screenrecord --verbose --bugreport $outputFile")
+        // Getting latest PID as there might be multiple screenrecording in progress.
+        val screenRecordPid = screenrecordPids.max()
+        try {
+            runnable()
+        } finally {
+            // Doesn't crash if the file doesn't exist, as we want the command output to be logged.
+            outputFile.tryWaitingForFileToExists()
+
+            val killOutput = uiDevice.shell("kill -INT $screenRecordPid")
+
+            val screenRecordOutput = screenRecordingFileDescriptor.readAllAndClose()
+            log(
+                """
+                screenrecord killed (kill command output="$killOutput")
+                Screen recording captured at: $outputFile
+                File size: ${Files.size(outputFile.toPath()) / 1024} KB
+                screenrecord command output:
+
+                """.trimIndent() +
+                    screenRecordOutput.prependIndent("   ")
+            )
+        }
+
+        if (screenRecordingInProgress()) {
+            Log.w(
+                TAG,
+                "Other screen recordings are in progress after this is done. " +
+                    "(pids=\"$screenrecordPids\")."
+            )
+        }
+    }
+
+    private fun File.tryWaitingForFileToExists() {
+        try {
+            ensureThat("Recording output created") { exists() }
+        } catch (e: FailedEnsureException) {
+            Log.e(TAG, "Recording not created successfully.", e)
+        }
+    }
+
+    private fun screenRecordingInProgress() = screenrecordPids.isNotEmpty()
+
+    private val screenrecordPids: List<String>
+        get() = uiDevice.shell("pidof screenrecord").split(" ")
+
+    /** Interface to indicate that the test should capture screenrecord */
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target(FUNCTION, CLASS, PROPERTY_GETTER, PROPERTY_SETTER)
+    annotation class ScreenRecord
+
+    private fun log(s: String) = Log.d(TAG, s)
+
+    // Reads all from the stream and closes it.
+    private fun ParcelFileDescriptor.readAllAndClose(): String =
+        AutoCloseInputStream(this).use { inputStream ->
+            inputStream.bufferedReader().use { it.readText() }
+        }
+
+    companion object {
+        private const val TAG = "ScreenRecordRule"
+        private const val SCREEN_RECORDING_TEST_LEVEL_OVERRIDE_KEY =
+            "screen-recording-always-enabled-test-level"
+        private const val SCREEN_RECORDING_CLASS_LEVEL_OVERRIDE_KEY =
+            "screen-recording-always-enabled-class-level"
+    }
+}
diff --git a/libraries/health/utils/Android.bp b/libraries/health/utils/Android.bp
index 827838a..7ef6ca1 100644
--- a/libraries/health/utils/Android.bp
+++ b/libraries/health/utils/Android.bp
@@ -24,6 +24,7 @@
     libs: [
         "androidx.test.runner",
         "ub-uiautomator",
+        "uiautomator-helpers",
     ],
     min_sdk_version: "26",
 }
diff --git a/libraries/health/utils/src/android/platform/test/util/HealthTestingUtils.java b/libraries/health/utils/src/android/platform/test/util/HealthTestingUtils.java
index e207e7c..00551ad 100644
--- a/libraries/health/utils/src/android/platform/test/util/HealthTestingUtils.java
+++ b/libraries/health/utils/src/android/platform/test/util/HealthTestingUtils.java
@@ -16,12 +16,10 @@
 
 package android.platform.test.util;
 
-import android.os.SystemClock;
+import android.platform.uiautomator_helpers.WaitUtils;
 import android.support.test.uiautomator.StaleObjectException;
 
-import org.junit.Assert;
-
-import java.util.Objects;
+import java.time.Duration;
 import java.util.Optional;
 import java.util.function.Supplier;
 
@@ -29,7 +27,6 @@
 public class HealthTestingUtils {
 
     private static final String TAG = "HealthTestingUtils";
-    private static final int SLEEP_MS = 100;
     private static final int WAIT_TIME_MS = 10000;
     private static final int DEFAULT_SETTLE_TIME_MS = 3000;
 
@@ -99,7 +96,7 @@
                 errorMessage,
                 () -> {
                     try {
-                        return Optional.of(resultProducer.get());
+                        return Optional.ofNullable(resultProducer.get());
                     } catch (StaleObjectException e) {
                         return Optional.empty();
                     }
@@ -125,28 +122,18 @@
      */
     public static void waitForCondition(
             Supplier<String> message, Condition condition, long timeoutMs) {
-        final long startTime = SystemClock.uptimeMillis();
-        while (SystemClock.uptimeMillis() < startTime + timeoutMs) {
-            try {
-                if (condition.isTrue()) {
-                    return;
-                }
-            } catch (Throwable t) {
-                throw new RuntimeException(t);
-            }
-            SystemClock.sleep(SLEEP_MS);
-        }
 
-        // Check once more before failing.
-        try {
-            if (condition.isTrue()) {
-                return;
-            }
-        } catch (Throwable t) {
-            throw new RuntimeException(t);
-        }
-
-        Assert.fail(message.get());
+        WaitUtils.ensureThat(
+                "waitForCondition",
+                /* timeout= */ Duration.ofMillis(timeoutMs),
+                /* errorProvider= */ message::get,
+                /* condition= */ () -> {
+                    try {
+                        return condition.isTrue();
+                    } catch (Throwable t) {
+                        throw new RuntimeException(t);
+                    }
+                });
     }
 
     /** @see HealthTestingUtils#waitForValueToSettle */
@@ -170,26 +157,11 @@
             Supplier<T> supplier,
             long minimumSettleTime,
             long timeoutMs) {
-        final long startTime = SystemClock.uptimeMillis();
-        long settledSince = startTime;
-        T previousValue = null;
-
-        while (SystemClock.uptimeMillis() < startTime + timeoutMs) {
-            T newValue = supplier.get();
-            final long currentTime = SystemClock.uptimeMillis();
-
-            if (!Objects.equals(previousValue, newValue)) {
-                settledSince = currentTime;
-                previousValue = newValue;
-            } else if (currentTime >= settledSince + minimumSettleTime) {
-                return previousValue;
-            }
-
-            SystemClock.sleep(SLEEP_MS);
-        }
-
-        Assert.fail(errorMessage.get());
-
-        return null;
+        return WaitUtils.waitForNullableValueToSettle(
+                "waitForValueToSettle",
+                /* minimumSettleTime= */ Duration.ofMillis(minimumSettleTime),
+                /* timeout= */ Duration.ofMillis(timeoutMs),
+                /* errorProvider= */ errorMessage::get,
+                /* supplier */ supplier::get);
     }
 }
diff --git a/libraries/screenshot/src/androidTest/java/platform/test/screenshot/GoldenImagePathManagerTest.kt b/libraries/screenshot/src/androidTest/java/platform/test/screenshot/GoldenImagePathManagerTest.kt
index e402698..d0abf5d 100644
--- a/libraries/screenshot/src/androidTest/java/platform/test/screenshot/GoldenImagePathManagerTest.kt
+++ b/libraries/screenshot/src/androidTest/java/platform/test/screenshot/GoldenImagePathManagerTest.kt
@@ -63,7 +63,7 @@
 
         val pc2 = getSimplePathConfig()
         val pcResolvedRelativePath2 = pc2.resolveRelativePath(context)
-        assertThat(pcResolvedRelativePath2).isEqualTo("cuttlefish/")
+        assertThat(pcResolvedRelativePath2).startsWith("cuttlefish")
     }
 
     @Test
diff --git a/libraries/screenshot/src/androidTest/java/platform/test/screenshot/ScreenshotTestRuleTest.kt b/libraries/screenshot/src/androidTest/java/platform/test/screenshot/ScreenshotTestRuleTest.kt
index 333d9bf..2302499 100644
--- a/libraries/screenshot/src/androidTest/java/platform/test/screenshot/ScreenshotTestRuleTest.kt
+++ b/libraries/screenshot/src/androidTest/java/platform/test/screenshot/ScreenshotTestRuleTest.kt
@@ -51,7 +51,7 @@
 @MediumTest
 class ScreenshotTestRuleTest {
 
-    val customizedAssetsPath = "libraries/screenshot/src/androidTest/assets"
+    val customizedAssetsPath = "platform_testing/libraries/screenshot/src/androidTest/assets"
 
     @get:Rule
     val rule = ScreenshotTestRule(
@@ -273,6 +273,41 @@
         assertThat(rule.getPathOnDeviceFor(RESULT_BIN_PROTO, goldenIdentifier).exists()).isTrue()
     }
 
+    @Test
+    fun screenshotAsserterHooks_successfulRun() {
+        var preRan = false
+        var postRan = false
+        val bitmap = loadBitmap("round_rect_green")
+        val asserter = ScreenshotRuleAsserter.Builder(rule)
+            .setOnBeforeScreenshot {preRan = true}
+            .setOnAfterScreenshot {postRan = true}
+            .setScreenshotProvider {bitmap}
+            .build()
+        asserter.assertGoldenImage("round_rect_green")
+        assertThat(preRan).isTrue()
+        assertThat(postRan).isTrue()
+    }
+
+    @Test
+    fun screenshotAsserterHooks_assertionException() {
+        var preRan = false
+        var postRan = false
+        val bitmap = loadBitmap("round_rect_green")
+        val asserter = ScreenshotRuleAsserter.Builder(rule)
+            .setOnBeforeScreenshot {preRan = true}
+            .setOnAfterScreenshot {postRan = true}
+            .setScreenshotProvider {
+                throw RuntimeException()
+                bitmap
+            }
+            .build()
+        try {
+            asserter.assertGoldenImage("round_rect_green")
+        } catch (e: RuntimeException) {}
+        assertThat(preRan).isTrue()
+        assertThat(postRan).isTrue()
+    }
+
     @After
     fun after() {
         // Clear all files we generated so we don't have dependencies between tests
diff --git a/libraries/screenshot/src/main/java/platform/test/screenshot/DeviceEmulationRule.kt b/libraries/screenshot/src/main/java/platform/test/screenshot/DeviceEmulationRule.kt
index 366dd71..4522fd1 100644
--- a/libraries/screenshot/src/main/java/platform/test/screenshot/DeviceEmulationRule.kt
+++ b/libraries/screenshot/src/main/java/platform/test/screenshot/DeviceEmulationRule.kt
@@ -16,6 +16,7 @@
 
 package platform.test.screenshot
 
+import android.app.UiAutomation
 import android.app.UiModeManager
 import android.content.Context
 import android.os.UserHandle
@@ -41,6 +42,7 @@
 class DeviceEmulationRule(private val spec: DeviceEmulationSpec) : TestRule {
 
     private val instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val uiAutomation = instrumentation.uiAutomation
 
     override fun apply(base: Statement, description: Description): Statement {
         // The statement which calls beforeTest() before running the test and afterTest()
@@ -58,6 +60,9 @@
     }
 
     private fun beforeTest() {
+        // Make sure that we are in natural orientation (rotation 0) before we set the screen size
+        uiAutomation.setRotation(UiAutomation.ROTATION_FREEZE_0)
+
         // Emulate the display size and density.
         val display = spec.display
         val density = display.densityDpi
@@ -109,6 +114,9 @@
         uiModeManager.setApplicationNightMode(UiModeManager.MODE_NIGHT_AUTO)
 
         instrumentation.resetInTouchMode()
+
+        // Unfreeze locked rotation
+        uiAutomation.setRotation(UiAutomation.ROTATION_UNFREEZE);
     }
 }
 
diff --git a/libraries/screenshot/src/main/java/platform/test/screenshot/GoldenImagePathManager.kt b/libraries/screenshot/src/main/java/platform/test/screenshot/GoldenImagePathManager.kt
index 3d76912..22a45bd 100644
--- a/libraries/screenshot/src/main/java/platform/test/screenshot/GoldenImagePathManager.kt
+++ b/libraries/screenshot/src/main/java/platform/test/screenshot/GoldenImagePathManager.kt
@@ -58,7 +58,7 @@
  */
 open class GoldenImagePathManager @JvmOverloads constructor(
     open val appContext: Context,
-    open val assetsPathRelativeToRepo: String = "assets",
+    open val assetsPathRelativeToBuildRoot: String = "assets",
     open val deviceLocalPath: String = getDeviceOutputDirectory(appContext),
     open val pathConfig: PathConfig = getSimplePathConfig()
 ) {
diff --git a/libraries/screenshot/src/main/java/platform/test/screenshot/ScreenshotTestRule.kt b/libraries/screenshot/src/main/java/platform/test/screenshot/ScreenshotTestRule.kt
index 98200ea..52629e0 100644
--- a/libraries/screenshot/src/main/java/platform/test/screenshot/ScreenshotTestRule.kt
+++ b/libraries/screenshot/src/main/java/platform/test/screenshot/ScreenshotTestRule.kt
@@ -176,7 +176,7 @@
         if (expected == null) {
             reportResult(
                 status = ScreenshotResultProto.DiffResult.Status.MISSING_REFERENCE,
-                assetsPathRelativeToRepo = goldenImagePathManager.assetsPathRelativeToRepo,
+                assetsPathRelativeToRepo = goldenImagePathManager.assetsPathRelativeToBuildRoot,
                 goldenIdentifier = goldenIdentifier,
                 actual = actual
             )
@@ -190,7 +190,7 @@
         if (actual.width != expected.width || actual.height != expected.height) {
             reportResult(
                 status = ScreenshotResultProto.DiffResult.Status.FAILED,
-                assetsPathRelativeToRepo = goldenImagePathManager.assetsPathRelativeToRepo,
+                assetsPathRelativeToRepo = goldenImagePathManager.assetsPathRelativeToBuildRoot,
                 goldenIdentifier = goldenIdentifier,
                 actual = actual,
                 expected = expected
@@ -217,7 +217,7 @@
 
         reportResult(
             status = status,
-            assetsPathRelativeToRepo = goldenImagePathManager.assetsPathRelativeToRepo,
+            assetsPathRelativeToRepo = goldenImagePathManager.assetsPathRelativeToBuildRoot,
             goldenIdentifier = goldenIdentifier,
             actual = actual,
             comparisonStatistics = comparisonResult.comparisonStatistics,
@@ -304,8 +304,10 @@
                 "${testIdentifier}_expected_$imageSuffix"
             OutputFileType.IMAGE_DIFF ->
                 "${testIdentifier}_diff_$imageSuffix"
-            OutputFileType.RESULT_PROTO -> "${testIdentifier}_$resultProtoFileSuffix"
-            OutputFileType.RESULT_BIN_PROTO -> "${testIdentifier}_$resultBinaryProtoFileSuffix"
+            OutputFileType.RESULT_PROTO ->
+                "${testIdentifier}_${goldenIdentifier}_$resultProtoFileSuffix"
+            OutputFileType.RESULT_BIN_PROTO ->
+                "${testIdentifier}_${goldenIdentifier}_$resultBinaryProtoFileSuffix"
         }
         return File(goldenImagePathManager.deviceLocalPath, fileName)
     }
@@ -422,14 +424,28 @@
 class ScreenshotRuleAsserter private constructor(private val rule: ScreenshotTestRule) : ScreenshotAsserter {
     // use the most constraining matcher as default
     private var matcher: BitmapMatcher = PixelPerfectMatcher()
+    private var beforeScreenshot: Runnable? = null
+    private var afterScreenshot: Runnable? = null
     // use the instrumentation screenshot as default
     private var screenShotter: BitmapSupplier = { Screenshot.capture().bitmap }
     override fun assertGoldenImage(goldenId: String) {
-        rule.assertBitmapAgainstGolden(screenShotter(), goldenId, matcher)
+        beforeScreenshot?.run();
+        try {
+            rule.assertBitmapAgainstGolden(screenShotter(), goldenId, matcher)
+        }
+        finally {
+            afterScreenshot?.run();
+        }
     }
 
     override fun assertGoldenImage(goldenId: String, areas: List<Rect>) {
-        rule.assertBitmapAgainstGolden(screenShotter(), goldenId, matcher, areas)
+        beforeScreenshot?.run();
+        try {
+            rule.assertBitmapAgainstGolden(screenShotter(), goldenId, matcher, areas)
+        }
+        finally {
+            afterScreenshot?.run();
+        }
     }
 
     class Builder(private val rule: ScreenshotTestRule) {
@@ -444,6 +460,16 @@
             return this
         }
 
+        fun setOnBeforeScreenshot(run: Runnable): Builder {
+            asserter.beforeScreenshot = run
+            return this
+        }
+
+        fun setOnAfterScreenshot(run: Runnable): Builder {
+            asserter.afterScreenshot = run
+            return this
+        }
+
         fun build(): ScreenshotAsserter {
             val built = asserter
             asserter = ScreenshotRuleAsserter(rule)
diff --git a/libraries/sts-common-util/host-side/Android.bp b/libraries/sts-common-util/host-side/Android.bp
index e89fde3..5e7e27f 100644
--- a/libraries/sts-common-util/host-side/Android.bp
+++ b/libraries/sts-common-util/host-side/Android.bp
@@ -24,11 +24,13 @@
         "src/**/*.java",
     ],
     static_libs: [
+        "auto_value_annotations",
         "sts-common-util-lib",
         "sts-libtombstone_proto-java",
         "truth-prebuilt",
         "xz-java",
     ],
+    plugins: ["auto_value_plugin"],
     libs: [
         "compatibility-host-util",
         "compatibility-tradefed",
@@ -52,3 +54,42 @@
         "libprotobuf-java-lite",
     ],
 }
+
+// Turn off various doclava warnings when generating
+// the docs. These are the same warnings that are
+// turned off in tools/tradefed/core
+tradefed_docs_only_args = " -hide 101 -hide 111 -hide 113 -hide 125 -hide 126 -hide 127 -hide 128 "
+
+droiddoc_host {
+    name: "sts-docs",
+    srcs: [
+        "src/**/*.java",
+    ],
+    libs: [
+        "auto_value_annotations",
+        "sts-common-util-lib",
+        "sts-libtombstone_proto-java",
+        "truth-prebuilt",
+        "xz-java",
+        "compatibility-host-util",
+        "compatibility-tradefed",
+        "guava",
+        "tradefed",
+    ],
+    custom_template: "droiddoc-templates-sdk",
+    // These settings are for integrating the javadoc with Devsite. See go/generate-tradefed-docs
+    hdf: [
+        "sac true",
+        "devices true",
+        "android.whichdoc online",
+        "css.path /reference/assets/css/doclava-devsite.css",
+        "book.root toc",
+        "book.path /_book.yaml",
+    ],
+    args: tradefed_docs_only_args +
+          "-yaml _toc.yaml " +
+          "-apidocsdir reference/sts/ " +
+          "-werror " +
+          "-package " +
+          "-devsite ",
+}
diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/CommandUtil.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/CommandUtil.java
index aaa9a90..3064e28 100644
--- a/libraries/sts-common-util/host-side/src/com/android/sts/common/CommandUtil.java
+++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/CommandUtil.java
@@ -16,19 +16,21 @@
 
 package com.android.sts.common;
 
-import static org.junit.Assert.assertEquals;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.junit.Assume.assumeThat;
 
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.util.CommandResult;
 import com.android.tradefed.util.CommandStatus;
 
+/** Collection of utilities to help run commands on device via adb */
 public final class CommandUtil {
 
     private CommandUtil() {}
 
     /**
-     * Execute shell command on device, throws AssertionError if command does not return 0.
+     * Execute shell command on device, throws AssumptionError if command does not return 0.
      *
      * @param device the device to use
      * @param cmd the command to run
@@ -40,7 +42,7 @@
     }
 
     /**
-     * Execute shell command on device, throws AssertionError if command does not return 0.
+     * Execute shell command on device, throws AssumptionError if command does not return 0.
      *
      * @param device the device to use
      * @param cmd the command to run
@@ -61,7 +63,7 @@
                 String.format(
                         "cmd failed: %s\ncode: %s\nstdout:\n%s\nstderr:\n%s",
                         cmd, res.getExitCode(), res.getStdout(), res.getStderr());
-        assertEquals(failMsg, CommandStatus.SUCCESS, res.getStatus());
+        assumeThat(failMsg, res.getStatus(), equalTo(CommandStatus.SUCCESS));
         return res;
     }
 }
diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/FridaUtils.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/FridaUtils.java
index 2790be3..c0c9f99 100644
--- a/libraries/sts-common-util/host-side/src/com/android/sts/common/FridaUtils.java
+++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/FridaUtils.java
@@ -17,16 +17,34 @@
 package com.android.sts.common;
 
 import static com.android.sts.common.CommandUtil.runAndCheck;
-import static java.util.stream.Collectors.toList;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.fail;
 
+import static java.util.stream.Collectors.toList;
+
+import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.sts.common.util.FridaUtilsBusinessLogicHandler;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.RunUtil;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonParser;
+
+import org.tukaani.xz.XZInputStream;
+
 import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileNotFoundException;
-import java.io.InputStreamReader;
 import java.io.IOException;
+import java.io.InputStreamReader;
 import java.net.URL;
 import java.net.URLConnection;
 import java.nio.charset.StandardCharsets;
@@ -37,26 +55,11 @@
 import java.util.Map;
 import java.util.Optional;
 import java.util.UUID;
-import java.util.concurrent.TimeoutException;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 import java.util.stream.Stream;
-import org.tukaani.xz.XZInputStream;
 
-import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
-import com.android.sts.common.util.FridaUtilsBusinessLogicHandler;
-import com.android.tradefed.build.IBuildInfo;
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.util.FileUtil;
-import com.android.tradefed.util.RunUtil;
-import com.google.gson.JsonArray;
-import com.google.gson.JsonElement;
-import com.google.gson.JsonParseException;
-import com.google.gson.JsonParser;
-
-// import org.json.JSONException;
-
+/** AutoCloseable that downloads and push frida and scripts to device and cleans up when done */
 public class FridaUtils implements AutoCloseable {
     private static final String PRODUCT_CPU_ABI_KEY = "ro.product.cpu.abi";
     private static final String PRODUCT_CPU_ABILIST_KEY = "ro.product.cpu.abilist";
@@ -195,7 +198,13 @@
     public void close() throws DeviceNotAvailableException, TimeoutException {
         device.enableAdbRoot();
         for (Integer pid : runningPids) {
-            ProcessUtil.killPid(device, pid.intValue(), 10_000L);
+            try {
+                ProcessUtil.killPid(device, pid.intValue(), 10_000L);
+            } catch (ProcessUtil.KillException e) {
+                if (e.getReason() != ProcessUtil.KillException.Reason.NO_SUCH_PROCESS) {
+                    CLog.e(e);
+                }
+            }
         }
         for (String file : fridaFiles) {
             device.deleteFile(file);
diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/MallocDebug.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/MallocDebug.java
new file mode 100644
index 0000000..12665b2
--- /dev/null
+++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/MallocDebug.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.sts.common;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeNoException;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.concurrent.TimeoutException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Utilities to setup malloc debug options on a process, check for malloc debug errors, and cleaning
+ * up afterwards.
+ */
+public class MallocDebug implements AutoCloseable {
+    private static final String LOG_TAG = MallocDebug.class.getSimpleName();
+    private static final String MALLOC_DEBUG_OPTIONS_PROP = "libc.debug.malloc.options";
+    private static final String MALLOC_DEBUG_PROGRAM_PROP = "libc.debug.malloc.program";
+    private static final Pattern[] mallocDebugErrorPatterns = {
+        Pattern.compile("^.*HAS A CORRUPTED FRONT GUARD.*$", Pattern.MULTILINE),
+        Pattern.compile("^.*HAS A CORRUPTED REAR GUARD.*$", Pattern.MULTILINE),
+        Pattern.compile("^.*USED AFTER FREE.*$", Pattern.MULTILINE),
+        Pattern.compile("^.*leaked block of size.*$", Pattern.MULTILINE),
+        Pattern.compile("^.*UNKNOWN POINTER \\(free\\).*$", Pattern.MULTILINE),
+        Pattern.compile("^.*HAS INVALID TAG.*$", Pattern.MULTILINE),
+    };
+
+    private ITestDevice device;
+    private String processName;
+    private AutoCloseable setMallocDebugOptionsProperty;
+    private AutoCloseable setAttachedProgramProperty;
+    private AutoCloseable killProcess;
+
+    private MallocDebug(
+            ITestDevice device, String mallocDebugOption, String processName, boolean isService)
+            throws DeviceNotAvailableException, TimeoutException, ProcessUtil.KillException {
+        this.device = device;
+        this.processName = processName;
+
+        // It's an error if this is called while something else is also doing malloc debug.
+        assertNull(
+                MALLOC_DEBUG_OPTIONS_PROP + " is already set!",
+                device.getProperty(MALLOC_DEBUG_OPTIONS_PROP));
+        CommandUtil.runAndCheck(device, "logcat -c");
+
+        try {
+            this.setMallocDebugOptionsProperty =
+                    SystemUtil.withProperty(device, MALLOC_DEBUG_OPTIONS_PROP, mallocDebugOption);
+            this.setAttachedProgramProperty =
+                    SystemUtil.withProperty(device, MALLOC_DEBUG_PROGRAM_PROP, processName);
+
+            // Kill and wait for the process to come back if we're attaching to a service
+            this.killProcess = null;
+            if (isService) {
+                this.killProcess = ProcessUtil.withProcessKill(device, processName, null);
+                ProcessUtil.waitProcessRunning(device, processName);
+            }
+        } catch (Throwable e1) {
+            try {
+                if (setMallocDebugOptionsProperty != null) {
+                    setMallocDebugOptionsProperty.close();
+                }
+                if (setAttachedProgramProperty != null) {
+                    setAttachedProgramProperty.close();
+                }
+            } catch (Exception e2) {
+                CLog.e(e2);
+                fail(
+                        "Could not enable malloc debug. Additionally, there was an"
+                                + " exception while trying to reset device state. Tests after"
+                                + " this may not work as expected!\n"
+                                + e2);
+            }
+            assumeNoException("Could not enable malloc debug", e1);
+        }
+    }
+
+    @Override
+    public void close() throws Exception {
+        device.waitForDeviceAvailable();
+        setMallocDebugOptionsProperty.close();
+        setAttachedProgramProperty.close();
+        if (killProcess != null) {
+            try {
+                killProcess.close();
+                ProcessUtil.waitProcessRunning(device, processName);
+            } catch (TimeoutException e) {
+                assumeNoException(
+                        "Could not restart '" + processName + "' after disabling malloc debug", e);
+            }
+        }
+        String logcat = CommandUtil.runAndCheck(device, "logcat -d *:S malloc_debug:V").getStdout();
+        assertNoMallocDebugErrors(logcat);
+    }
+
+    /**
+     * Restart the given service and enable malloc debug on it, asserting no malloc debug error upon
+     * closing.
+     *
+     * @param device the device to use
+     * @param mallocDebugOptions value to set libc.debug.malloc.options to.
+     * @param processName the service process to attach libc malloc debug to. Should be running.
+     * @return The AutoCloseable object that will restart/unattach the service, disable libc malloc
+     *     debug, and check for malloc debug errors when closed.
+     */
+    public static AutoCloseable withLibcMallocDebugOnService(
+            ITestDevice device, String mallocDebugOptions, String processName)
+            throws DeviceNotAvailableException, IllegalArgumentException, TimeoutException,
+                ProcessUtil.KillException {
+        if (processName == null || processName.isEmpty()) {
+            throw new IllegalArgumentException("Service processName can't be empty");
+        }
+        return new MallocDebug(device, mallocDebugOptions, processName, true);
+    }
+
+    /**
+     * Set up so that malloc debug will attach to the given processName, and assert no malloc debug
+     * error upon closing. Note that processName will need to be manually launched after this call.
+     *
+     * @param device the device to use
+     * @param mallocDebugOptions value to set libc.debug.malloc.options to.
+     * @param processName the process to attach libc malloc debug to. Should not be running yet.
+     * @return The AutoCloseable object that will disable libc malloc debug and check for malloc
+     *     debug errors when closed.
+     */
+    public static AutoCloseable withLibcMallocDebugOnNewProcess(
+            ITestDevice device, String mallocDebugOptions, String processName)
+            throws DeviceNotAvailableException, IllegalArgumentException, TimeoutException,
+                ProcessUtil.KillException {
+        if (processName == null || processName.isEmpty()) {
+            throw new IllegalArgumentException("processName can't be empty");
+        }
+        if (ProcessUtil.pidsOf(device, processName).isPresent()) {
+            throw new IllegalArgumentException(processName + " is already running!");
+        }
+        return new MallocDebug(device, mallocDebugOptions, processName, false);
+    }
+
+    /**
+     * Start attaching libc malloc debug to all processes launching after this call, asserting no
+     * malloc debug error upon closing.
+     *
+     * @param device the device to use
+     * @param mallocDebugOptions value to set libc.debug.malloc.options to.
+     * @return The AutoCloseable object that will disable libc malloc debug and check for malloc
+     *     debug errors when closed.
+     */
+    public static AutoCloseable withLibcMallocDebugOnAllNewProcesses(
+            ITestDevice device, String mallocDebugOptions)
+            throws DeviceNotAvailableException, TimeoutException, ProcessUtil.KillException {
+        return new MallocDebug(device, mallocDebugOptions, null, false);
+    }
+
+    static void assertNoMallocDebugErrors(String logcat) {
+        ImmutableList.Builder<String> mallocDebugErrors = new ImmutableList.Builder<String>();
+        for (Pattern p : mallocDebugErrorPatterns) {
+            Matcher m = p.matcher(logcat);
+            while (m.find()) {
+                mallocDebugErrors.add(m.group());
+            }
+        }
+        assertArrayEquals(
+                "Found malloc debug errors.", new String[] {}, mallocDebugErrors.build().toArray());
+    }
+}
diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/NativePoc.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/NativePoc.java
new file mode 100644
index 0000000..1aabaa9
--- /dev/null
+++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/NativePoc.java
@@ -0,0 +1,321 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.sts.common;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.core.IsNot.not;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assume.assumeThat;
+
+import static java.util.stream.Collectors.joining;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/** Setup and run a native PoC, asserting exit conditions */
+@AutoValue
+public abstract class NativePoc {
+    static final long DEFAULT_POC_TIMEOUT_SECONDS = 60;
+    static final String TMP_PATH = "/data/local/tmp/";
+    static final String RESOURCE_ROOT = "/";
+    static final int BUF_SIZE = 65536;
+
+    abstract String pocName();
+    abstract ImmutableList<String> args();
+    abstract ImmutableMap<String, String> envVars();
+    abstract boolean useDefaultLdLibraryPath();
+    abstract long timeoutSeconds();
+    abstract ImmutableList<String> resources();
+    abstract String resourcePushLocation();
+    abstract boolean only32();
+    abstract boolean only64();
+    abstract AfterFunction after();
+    abstract NativePocAsserter asserter();
+    abstract boolean assumePocExitSuccess();
+
+    NativePoc() {}
+
+    public static Builder builder() {
+        return new AutoValue_NativePoc.Builder()
+                .args(ImmutableList.of())
+                .envVars(ImmutableMap.of())
+                .useDefaultLdLibraryPath(false)
+                .timeoutSeconds(DEFAULT_POC_TIMEOUT_SECONDS)
+                .resources(ImmutableList.of())
+                .resourcePushLocation(TMP_PATH)
+                .after((res) -> {})
+                .only32(false)
+                .only64(false)
+                .asserter(new NativePocAsserter() {})
+                .assumePocExitSuccess(true);
+    }
+
+    @AutoValue.Builder
+    public abstract static class Builder {
+        /** Name of executable to be uploaded and run. Do not include "_sts??" suffix. */
+        public abstract Builder pocName(String value);
+
+        abstract String pocName();
+
+        /** List of arguments to be passed to the executable PoC */
+        public abstract Builder args(List<String> value);
+        /** List of arguments to be passed to the executable PoC */
+        public abstract Builder args(String... value);
+
+        /** Map of environment variables to be set before running the PoC */
+        public abstract Builder envVars(Map<String, String> value);
+
+        abstract ImmutableMap<String, String> envVars();
+
+        /** Whether to include /system/lib64 and /system/lib in LD_LIBRARY_PATH */
+        public abstract Builder useDefaultLdLibraryPath(boolean value);
+
+        abstract boolean useDefaultLdLibraryPath();
+
+        /**
+         * How long to let the PoC run before terminating
+         *
+         * @param value how many seconds to let the native PoC run before it's terminated
+         * @param reason explain why a different timeout amount is needed instead of the default
+         *     {@link #DEFAULT_POC_TIMEOUT_SECONDS}. Generally used for PoCs that tries to exploit
+         *     race conditions.
+         * @return this Builder instance
+         */
+        public Builder timeoutSeconds(long value, String reason) {
+            return timeoutSeconds(value);
+        }
+
+        abstract Builder timeoutSeconds(long value);
+
+        /** List of java resources to extract and upload to the device */
+        public abstract Builder resources(List<String> value);
+        /** List of java resources to extract and upload to the device */
+        public abstract Builder resources(String... value);
+
+        /** Where to upload extracted Java resources to. Defaults to where the PoC is uploaded */
+        public abstract Builder resourcePushLocation(String value);
+
+        abstract String resourcePushLocation();
+
+        /** Force using 32-bit version of the PoC executable */
+        public Builder only32() {
+            return only32(true);
+        }
+
+        abstract Builder only32(boolean value);
+
+        /** Force using 64-bit version of the PoC executable */
+        public Builder only64() {
+            return only64(true);
+        }
+
+        abstract Builder only64(boolean value);
+
+        /**
+         * Function to run after the PoC finishes executing but before assertion or cleanups.
+         *
+         * <p>This is typically used to wait for side effects of the PoC that may happen after the
+         * PoC process itself finished, e.g. waiting for a crashdump to be written to file or for a
+         * service to crash.
+         */
+        public abstract Builder after(AfterFunction value);
+
+        /** A {@link NativePocAsserter} to check PoC execution results or side-effect */
+        public abstract Builder asserter(NativePocAsserter value);
+
+        /** Whether to throw an assumption failure when PoC does not return 0. Defaults true */
+        public abstract Builder assumePocExitSuccess(boolean value);
+
+        abstract NativePoc autoBuild();
+
+        /** Build an immutable NativePoc object */
+        public NativePoc build() {
+            if (useDefaultLdLibraryPath()) {
+                updateLdLibraryPath();
+            }
+            if (!resourcePushLocation().endsWith("/")) {
+                resourcePushLocation(resourcePushLocation() + "/");
+            }
+            NativePoc nativePoc = autoBuild();
+            assertFalse("both only32 & only64 are set!", nativePoc.only32() && nativePoc.only64());
+            assertNotNull("pocName not set!", nativePoc.pocName());
+            return nativePoc;
+        }
+
+        private void updateLdLibraryPath() {
+            String key = "LD_LIBRARY_PATH";
+            String newVal;
+            if (envVars().containsKey(key)) {
+                newVal = envVars().get(key) + ":/system/lib64:/system/lib";
+            } else {
+                newVal = "/system/lib64:/system/lib";
+            }
+            Map<String, String> newMap =
+                    new HashMap<>() {
+                        {
+                            putAll(envVars());
+                            put(key, newVal);
+                        }
+                    };
+            envVars(ImmutableMap.copyOf(newMap));
+        }
+    }
+
+    /**
+     * Execute the PoC with the given parameters and assertions.
+     *
+     * @param test the instance of BaseHostJUnit4Test this is running in. Usually called with "this"
+     *     if called from an STS test.
+     */
+    public void run(final BaseHostJUnit4Test test) throws Exception {
+        CLog.d("Trying to start NativePoc: %s", this.toString());
+        CommandResult res = runPocAndAssert(test);
+        assumeThat(
+                "PoC timed out. You may want to make it faster or specify timeout amount",
+                res.getStatus(),
+                not(CommandStatus.TIMED_OUT));
+        if (assumePocExitSuccess()) {
+            assumeThat(
+                    "PoC did not exit with success. stderr: " + res.getStderr(),
+                    res.getStatus(),
+                    is(CommandStatus.SUCCESS));
+        }
+    }
+
+    private CommandResult runPocAndAssert(final BaseHostJUnit4Test test) throws Exception {
+        ITestDevice device = test.getDevice();
+
+        try (AutoCloseable aPoc = withPoc(test, device);
+                AutoCloseable aRes = withResourcesUpload(device);
+                AutoCloseable aAssert = asserter().withAutoCloseable(this, device)) {
+            // Setup environment variable shell command prefix
+            String envStr =
+                    envVars().keySet().stream()
+                            .map(k -> String.format("%s='%s'", k, escapeQuote(envVars().get(k))))
+                            .collect(joining(" "));
+
+            // Setup command arguments string for shell
+            String argStr = args().stream().map(s -> escapeQuote(s)).collect(joining(" "));
+
+            // Run the command
+            CommandResult res =
+                    device.executeShellV2Command(
+                            String.format("cd %s; %s ./%s %s", TMP_PATH, envStr, pocName(), argStr),
+                            timeoutSeconds(),
+                            TimeUnit.SECONDS,
+                            0 /* retryAttempts */);
+            CLog.d(
+                    "PoC exit code: %d\nPoC stdout:\n%s\nPoC stderr:\n%s\n",
+                    res.getExitCode(), res.getStdout(), res.getStderr());
+
+            after().run(res);
+            asserter().checkCmdResult(res);
+            return res;
+        }
+    }
+
+    private static String escapeQuote(String s) {
+        return s.replace("'", "'\"'\"'");
+    }
+
+    private AutoCloseable withPoc(final BaseHostJUnit4Test test, final ITestDevice device)
+            throws DeviceNotAvailableException, FileNotFoundException {
+        PocPusher pocPusher =
+                new PocPusher().setDevice(device).setBuild(test.getBuild()).setAbi(test.getAbi());
+        if (only32()) {
+            pocPusher.only32();
+        }
+        if (only64()) {
+            pocPusher.only64();
+        }
+        final String remoteFile = TMP_PATH + pocName();
+        pocPusher.pushFile(pocName() + "_sts", remoteFile);
+        device.executeShellV2Command(String.format("chmod 777 '%s'", remoteFile));
+        CommandUtil.runAndCheck(device, String.format("test -r '%s'", remoteFile));
+        CommandUtil.runAndCheck(device, String.format("test -w '%s'", remoteFile));
+        CommandUtil.runAndCheck(device, String.format("test -x '%s'", remoteFile));
+
+        return new AutoCloseable() {
+            @Override
+            public void close() throws DeviceNotAvailableException {
+                device.deleteFile(remoteFile);
+            }
+        };
+    }
+
+    private AutoCloseable withResourcesUpload(final ITestDevice device)
+            throws DeviceNotAvailableException, IOException {
+        for (String resource : resources()) {
+            File resTmpFile = File.createTempFile("STSNativePoc", "");
+            try {
+                try (InputStream in =
+                                NativePoc.class.getResourceAsStream(RESOURCE_ROOT + resource);
+                        OutputStream out =
+                                new BufferedOutputStream(new FileOutputStream(resTmpFile))) {
+                    byte[] buf = new byte[BUF_SIZE];
+                    int chunkSize;
+                    while ((chunkSize = in.read(buf)) != -1) {
+                        out.write(buf, 0, chunkSize);
+                    }
+                }
+
+                device.pushFile(resTmpFile, resourcePushLocation() + resource);
+            } finally {
+                resTmpFile.delete();
+            }
+        }
+
+        return new AutoCloseable() {
+            @Override
+            public void close() throws DeviceNotAvailableException {
+                tryRemoveResources(device);
+            }
+        };
+    }
+
+    private void tryRemoveResources(ITestDevice device) throws DeviceNotAvailableException {
+        for (String resource : resources()) {
+            device.deleteFile(resourcePushLocation() + resource);
+        }
+    }
+
+    /** Lambda construct to run after PoC finished executing but before assertion and cleanup. */
+    public static interface AfterFunction {
+        void run(CommandResult res) throws Exception;
+    }
+}
diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/NativePocAsserter.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/NativePocAsserter.java
new file mode 100644
index 0000000..3d6838a
--- /dev/null
+++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/NativePocAsserter.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.sts.common;
+
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.util.CommandResult;
+
+/** Interface for an asserter to use with {@link NativePoc#asserter}. */
+public interface NativePocAsserter {
+    /** Called before a PoC runs, returns an AutoCloseable that closes after the PoC finishes */
+    public default AutoCloseable withAutoCloseable(NativePoc nativePoc, ITestDevice device)
+            throws Exception {
+        return new AutoCloseable() {
+            @Override
+            public void close() {}
+        };
+    }
+
+    /** Called after the PoC finishes */
+    public default void checkCmdResult(CommandResult result) {}
+}
diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/NativePocCrashAsserter.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/NativePocCrashAsserter.java
new file mode 100644
index 0000000..10bf414
--- /dev/null
+++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/NativePocCrashAsserter.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.sts.common;
+
+import com.android.sts.common.util.TombstoneUtils;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+
+public class NativePocCrashAsserter implements NativePocAsserter {
+    private TombstoneUtils.Config tombstoneConfig;
+    private final boolean checkPocCrashes;
+
+    /** Returns a NativePocAsserter that checks the listed processes for any security crashes. */
+    public static NativePocAsserter assertNoCrashIn(String... patterns) {
+        return new NativePocCrashAsserter(
+                new TombstoneUtils.Config().setProcessPatterns(patterns), false);
+    }
+
+    /** Returns a NativePocAsserter that makes sure the Poc does not have a security crash. */
+    public static NativePocAsserter assertNoCrash() {
+        return new NativePocCrashAsserter(new TombstoneUtils.Config(), true);
+    }
+
+    /**
+     * Returns a NativePocAsserter that makes sure there is no security crash detected accoridng to
+     * the given TombstoneUtils.Config
+     */
+    public static NativePocAsserter assertNoCrash(TombstoneUtils.Config config) {
+        return new NativePocCrashAsserter(config, false);
+    }
+
+    private NativePocCrashAsserter(TombstoneUtils.Config config, boolean checkPocCrashes) {
+        this.tombstoneConfig = config;
+        this.checkPocCrashes = checkPocCrashes;
+    }
+
+    @Override
+    public AutoCloseable withAutoCloseable(NativePoc nativePoc, ITestDevice device)
+            throws DeviceNotAvailableException {
+        if (checkPocCrashes) {
+            tombstoneConfig.setProcessPatterns(nativePoc.pocName());
+        }
+        return TombstoneUtils.withAssertNoSecurityCrashes(device, tombstoneConfig);
+    }
+}
diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/NativePocMallocDebugAsserter.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/NativePocMallocDebugAsserter.java
new file mode 100644
index 0000000..4c15e9a
--- /dev/null
+++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/NativePocMallocDebugAsserter.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.sts.common;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+
+import java.util.Optional;
+import java.util.concurrent.TimeoutException;
+
+public class NativePocMallocDebugAsserter implements NativePocAsserter {
+    private final String mallocDebugOptions;
+    private final Optional<String> mallocDebugOnService;
+
+    /**
+     * Returns a NativePocAsserter that attaches libc malloc debug to a service before running the
+     * PoC and checks for any malloc debug error on that service while the poc runs.
+     */
+    public static NativePocAsserter assertNoMallocDebugErrorOnService(
+            String options, String service) {
+        return new NativePocMallocDebugAsserter(options, service);
+    }
+
+    /**
+     * Returns a NativePocAsserter that attaches libc malloc debug to the PoC and checks for any
+     * malloc debug error while the poc runs.
+     */
+    public static NativePocAsserter assertNoMallocDebugErrorOnPoc(String options) {
+        return new NativePocMallocDebugAsserter(options, null);
+    }
+
+    private NativePocMallocDebugAsserter(String options, String service) {
+        this.mallocDebugOptions = options;
+        this.mallocDebugOnService = Optional.ofNullable(service);
+    }
+
+    @Override
+    public AutoCloseable withAutoCloseable(NativePoc nativePoc, ITestDevice device)
+            throws DeviceNotAvailableException, TimeoutException, ProcessUtil.KillException {
+        if (mallocDebugOnService.isPresent()) {
+            return MallocDebug.withLibcMallocDebugOnService(
+                    device, mallocDebugOptions, mallocDebugOnService.get());
+        } else {
+            return MallocDebug.withLibcMallocDebugOnNewProcess(
+                    device, mallocDebugOptions, nativePoc.pocName());
+        }
+    }
+}
diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/NativePocStatusAsserter.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/NativePocStatusAsserter.java
new file mode 100644
index 0000000..51c57d6
--- /dev/null
+++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/NativePocStatusAsserter.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.sts.common;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import com.android.tradefed.util.CommandResult;
+
+public class NativePocStatusAsserter {
+    private static final int VULNERABLE_EXIT_CODE = 113;
+
+    /** Return a {@link NativePocAsserter} that makes sure PoC did not exit with given code. */
+    public static NativePocAsserter assertNotExitCode(final int badExitCode) {
+        return new NativePocAsserter() {
+            @Override
+            public void checkCmdResult(CommandResult result) {
+                assertNotEquals(
+                        "PoC exited with bad exit code.",
+                        (long) badExitCode,
+                        (long) result.getExitCode());
+            }
+        };
+    }
+
+    /** Return a {@link NativePocAsserter} that makes sure PoC did not exit with code 113. */
+    public static NativePocAsserter assertNotVulnerableExitCode() {
+        return assertNotExitCode(VULNERABLE_EXIT_CODE);
+    }
+
+    /** Return a {@link NativePocAsserter} that makes sure PoC exited with given code. */
+    public static NativePocAsserter assertExitCode(final int exitCode) {
+        return new NativePocAsserter() {
+            @Override
+            public void checkCmdResult(CommandResult result) {
+                assertEquals(
+                        "PoC did not exit with expected exit code.",
+                        (long) exitCode,
+                        (long) result.getExitCode());
+            }
+        };
+    }
+}
diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/PocPusher.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/PocPusher.java
index 7f6dd2c..ac21770 100644
--- a/libraries/sts-common-util/host-side/src/com/android/sts/common/PocPusher.java
+++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/PocPusher.java
@@ -16,16 +16,8 @@
 
 package com.android.sts.common;
 
-import org.junit.runner.Description;
-
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.Set;
-import java.io.File;
-import java.io.FileNotFoundException;
-
-import org.junit.runner.Description;
-import org.junit.rules.TestWatcher;
+import static org.junit.Assert.*;
+import static org.junit.Assume.*;
 
 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
 import com.android.tradefed.build.IBuildInfo;
@@ -34,9 +26,16 @@
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.testtype.IAbi;
 
-import static org.junit.Assume.*;
-import static org.junit.Assert.*;
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
 
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+/** Utilities to help push a native PoC executable to the device */
 public class PocPusher extends TestWatcher {
     private ITestDevice device = null;
     private CompatibilityBuildHelper buildHelper = null;
diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/ProcessUtil.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/ProcessUtil.java
index 84dc87c..55f47ac 100644
--- a/libraries/sts-common-util/host-side/src/com/android/sts/common/ProcessUtil.java
+++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/ProcessUtil.java
@@ -16,8 +16,10 @@
 
 package com.android.sts.common;
 
+
 import com.android.ddmlib.Log;
 import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.IFileEntry;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.util.CommandResult;
 import com.android.tradefed.util.CommandStatus;
@@ -31,7 +33,27 @@
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
+/** Various helpers to find, wait, and kill processes on the device */
 public final class ProcessUtil {
+    public static class KillException extends Exception {
+        public enum Reason {
+            UNKNOWN,
+            INVALID_SIGNAL,
+            INSUFFICIENT_PERMISSIONS,
+            NO_SUCH_PROCESS;
+        }
+
+        private Reason reason;
+
+        public KillException(String errorMessage, Reason r) {
+            super(errorMessage);
+            this.reason = r;
+        }
+
+        public Reason getReason() {
+            return this.reason;
+        }
+    }
 
     private static final String LOG_TAG = ProcessUtil.class.getSimpleName();
 
@@ -55,7 +77,10 @@
         CommandResult pgrepRes =
                 device.executeShellV2Command(String.format("pgrep -f -l %s", pgrepRegex));
         if (pgrepRes.getStatus() != CommandStatus.SUCCESS) {
-            Log.w(LOG_TAG, String.format("pgrep failed with stderr: %s", pgrepRes.getStderr()));
+            Log.d(
+                    LOG_TAG,
+                    String.format(
+                            "pgrep '%s' failed with stderr: %s", pgrepRegex, pgrepRes.getStderr()));
             return Optional.empty();
         }
         Map<Integer, String> pidToCommand = new HashMap<>();
@@ -69,6 +94,26 @@
     }
 
     /**
+     * Get a single pid matching a pattern passed to `pgrep`. Throw an {@link
+     * IllegalArgumentException} when there are more than one PID matching the pattern.
+     *
+     * @param device the device to use
+     * @param pgrepRegex a String representing the regex for pgrep
+     * @return an Optional Integer of the pid; empty if pgrep did not return EXIT_SUCCESS
+     */
+    public static Optional<Integer> pidOf(ITestDevice device, String pgrepRegex)
+            throws DeviceNotAvailableException, IllegalArgumentException {
+        Optional<Map<Integer, String>> pids = pidsOf(device, pgrepRegex);
+        if (!pids.isPresent()) {
+            return Optional.empty();
+        } else if (pids.get().size() == 1) {
+            return Optional.of(pids.get().keySet().iterator().next());
+        } else {
+            throw new IllegalArgumentException("More than one process found for: " + pgrepRegex);
+        }
+    }
+
+    /**
      * Wait until a running process is found for a given regex.
      *
      * @param device the device to use
@@ -135,7 +180,8 @@
      * @param pid the id of the process to wait until exited
      */
     public static void waitPidExited(ITestDevice device, int pid)
-            throws TimeoutException, DeviceNotAvailableException {
+            throws TimeoutException, DeviceNotAvailableException,
+                KillException {
         waitPidExited(device, pid, PROCESS_WAIT_TIMEOUT_MS);
     }
 
@@ -148,7 +194,8 @@
      * @param timeoutMs how long to wait before throwing a TimeoutException
      */
     public static void waitPidExited(ITestDevice device, int pid, long timeoutMs)
-            throws TimeoutException, DeviceNotAvailableException {
+            throws TimeoutException, DeviceNotAvailableException,
+                KillException {
         long endTime = System.currentTimeMillis() + timeoutMs;
         CommandResult res = null;
         while (true) {
@@ -157,7 +204,9 @@
             if (res.getStatus() != CommandStatus.SUCCESS) {
                 String err = res.getStderr();
                 if (!err.contains("No such process")) {
-                    throw new RuntimeException("kill -0 returned stderr: " + err);
+                    throw new KillException(
+                            "kill -0 returned stderr: " + err,
+                            KillException.Reason.NO_SUCH_PROCESS);
                 }
                 // the process is most likely killed
                 return;
@@ -181,7 +230,8 @@
      * @param timeoutMs how long to wait before throwing a TimeoutException
      */
     public static void killPid(ITestDevice device, int pid, long timeoutMs)
-            throws DeviceNotAvailableException, TimeoutException {
+            throws DeviceNotAvailableException, TimeoutException,
+                KillException {
         killPid(device, pid, 9, timeoutMs);
     }
 
@@ -194,8 +244,22 @@
      * @param timeoutMs how long to wait before throwing a TimeoutException
      */
     public static void killPid(ITestDevice device, int pid, int signal, long timeoutMs)
-            throws DeviceNotAvailableException, TimeoutException {
-        CommandUtil.runAndCheck(device, String.format("kill -%d %d", signal, pid));
+            throws DeviceNotAvailableException, TimeoutException,
+                KillException {
+        CommandResult res =
+            device.executeShellV2Command(String.format("kill -%d %d", signal, pid));
+        if (res.getStatus() != CommandStatus.SUCCESS) {
+            String err = res.getStderr();
+            if (err.contains("invalid signal specification")) {
+                throw new KillException(err, KillException.Reason.INVALID_SIGNAL);
+            } else if (err.contains("Operation not permitted")) {
+                throw new KillException(err, KillException.Reason.INSUFFICIENT_PERMISSIONS);
+            } else if (err.contains("No such process")) {
+                throw new KillException(err, KillException.Reason.NO_SUCH_PROCESS);
+            } else {
+                throw new KillException(err, KillException.Reason.UNKNOWN);
+            }
+        }
         waitPidExited(device, pid, timeoutMs);
     }
 
@@ -208,7 +272,8 @@
      * @return whether any processes were killed
      */
     public static boolean killAll(ITestDevice device, String pgrepRegex, long timeoutMs)
-            throws DeviceNotAvailableException, TimeoutException {
+            throws DeviceNotAvailableException, TimeoutException,
+                KillException {
         return killAll(device, pgrepRegex, timeoutMs, true);
     }
 
@@ -224,7 +289,8 @@
      */
     public static boolean killAll(
             ITestDevice device, String pgrepRegex, long timeoutMs, boolean expectExist)
-            throws DeviceNotAvailableException, TimeoutException {
+            throws DeviceNotAvailableException, TimeoutException,
+                KillException {
         Optional<Map<Integer, String>> pids = pidsOf(device, pgrepRegex);
         if (!pids.isPresent()) {
             // no pids to kill
@@ -234,9 +300,18 @@
             }
             return false;
         }
+
         for (int pid : pids.get().keySet()) {
-            killPid(device, pid, timeoutMs);
+            try {
+                killPid(device, pid, timeoutMs);
+            } catch (KillException e) {
+                // ignore pids that do not exist
+                if (e.getReason() != KillException.Reason.NO_SUCH_PROCESS) {
+                    throw e;
+                }
+            }
         }
+
         return true;
     }
 
@@ -244,14 +319,14 @@
      * Kill a process at the beginning and end of a test.
      *
      * @param device the device to use
-     * @param pid the id of the process to kill
+     * @param pgrepRegex the name pattern of the process to kill to give to pgrep
      * @param beforeCloseKill a runnable for any actions that need to cleanup before killing the
      *     process in a normal environment at the end of the test. Can be null.
      * @return An object that will kill the process again when it is closed
      */
     public static AutoCloseable withProcessKill(
             final ITestDevice device, final String pgrepRegex, final Runnable beforeCloseKill)
-            throws DeviceNotAvailableException, TimeoutException {
+            throws DeviceNotAvailableException, TimeoutException, KillException {
         return withProcessKill(device, pgrepRegex, beforeCloseKill, PROCESS_WAIT_TIMEOUT_MS);
     }
 
@@ -259,10 +334,10 @@
      * Kill a process at the beginning and end of a test.
      *
      * @param device the device to use
-     * @param pid the id of the process to kill
-     * @param timeoutMs how long in milliseconds to wait for the process to kill
+     * @param pgrepRegex the name pattern of the process to kill to give to pgrep
      * @param beforeCloseKill a runnable for any actions that need to cleanup before killing the
      *     process in a normal environment at the end of the test. Can be null.
+     * @param timeoutMs how long in milliseconds to wait for the process to kill
      * @return An object that will kill the process again when it is closed
      */
     public static AutoCloseable withProcessKill(
@@ -270,11 +345,16 @@
             final String pgrepRegex,
             final Runnable beforeCloseKill,
             final long timeoutMs)
-            throws DeviceNotAvailableException, TimeoutException {
+            throws DeviceNotAvailableException, TimeoutException, KillException {
         return new AutoCloseable() {
             {
-                if (!killAll(device, pgrepRegex, timeoutMs, /*expectExist*/ false)) {
-                    Log.d(LOG_TAG, String.format("did not kill any processes for %s", pgrepRegex));
+                try {
+                    if (!killAll(device, pgrepRegex, timeoutMs, /*expectExist*/ false)) {
+                        Log.d(LOG_TAG,
+                            String.format("did not kill any processes for %s", pgrepRegex));
+                    }
+                } catch (KillException e) {
+                    Log.d(LOG_TAG, "failed to kill a process");
                 }
             }
 
@@ -283,7 +363,13 @@
                 if (beforeCloseKill != null) {
                     beforeCloseKill.run();
                 }
-                killAll(device, pgrepRegex, timeoutMs, /*expectExist*/ false);
+                try {
+                    killAll(device, pgrepRegex, timeoutMs, /*expectExist*/ false);
+                } catch (KillException e) {
+                    if (e.getReason() != KillException.Reason.NO_SUCH_PROCESS) {
+                        throw e;
+                    }
+                }
             }
         };
     }
@@ -337,4 +423,28 @@
                         .filter((f) -> filePattern.matcher(f).matches())
                         .collect(Collectors.toList()));
     }
+
+    /**
+     * Returns file entry of the first file loaded by the specified process with specified name
+     *
+     * @param device device to be run on
+     * @param process pgrep pattern of process to look for
+     * @param filenameSubstr part of file name/path loaded by the process
+     * @return an Opotional of IFileEntry of the path of the file on the device if exists.
+     */
+    public static Optional<IFileEntry> findFileLoadedByProcess(
+            ITestDevice device, String process, String filenameSubstr)
+            throws DeviceNotAvailableException {
+        Optional<Integer> pid = ProcessUtil.pidOf(device, process);
+        if (pid.isPresent()) {
+            String cmd = "lsof -p " + pid.get().toString() + " | awk '{print $NF}'";
+            String[] openFiles = CommandUtil.runAndCheck(device, cmd).getStdout().split("\n");
+            for (String f : openFiles) {
+                if (f.contains(filenameSubstr)) {
+                    return Optional.of(device.getFileEntry(f.trim()));
+                }
+            }
+        }
+        return Optional.empty();
+    }
 }
diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/RegexUtils.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/RegexUtils.java
index f769c0c..9498790 100644
--- a/libraries/sts-common-util/host-side/src/com/android/sts/common/RegexUtils.java
+++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/RegexUtils.java
@@ -16,13 +16,15 @@
 
 package com.android.sts.common;
 
-import java.util.regex.Pattern;
-import java.util.regex.Matcher;
+import static org.junit.Assert.*;
+
 import com.android.ddmlib.Log.LogLevel;
 import com.android.tradefed.log.LogUtil.CLog;
 
-import static org.junit.Assert.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
+/** Contains wrappers around JUnit assertions with regex matching in strings */
 public class RegexUtils {
     private static final int TIMEOUT_DURATION = 20 * 60_000; // 20 minutes
     private static final int WARNING_THRESHOLD = 1000; // 1 second
diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/RootcanalUtils.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/RootcanalUtils.java
index 8fd995f..c293c91 100644
--- a/libraries/sts-common-util/host-side/src/com/android/sts/common/RootcanalUtils.java
+++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/RootcanalUtils.java
@@ -20,6 +20,8 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assume.assumeNoException;
+import static org.junit.Assume.assumeTrue;
 import static com.android.sts.common.CommandUtil.runAndCheck;
 
 import java.io.ByteArrayOutputStream;
@@ -90,11 +92,19 @@
             device.disableAdbRoot();
             // OverlayFsUtils' finished() will restart the device.
             overlayFsUtils.finished(d);
-            runAndCheck(device, "svc bluetooth enable");
+            device.waitForDeviceAvailable();
+            CommandResult res = device.executeShellV2Command("svc bluetooth enable");
+            if (res.getStatus() != CommandStatus.SUCCESS) {
+                CLog.e("Could not reenable Bluetooth during cleanup!");
+            }
         } catch (DeviceNotAvailableException e) {
             throw new AssertionError("Device unavailable when cleaning up", e);
         } catch (TimeoutException e) {
             CLog.w("Could not kill rootcanal HAL during cleanup");
+        } catch (ProcessUtil.KillException e) {
+            if (e.getReason() != ProcessUtil.KillException.Reason.NO_SUCH_PROCESS) {
+                CLog.w("Could not kill rootcanal HAL during cleanup: " + e.getMessage());
+            }
         }
     }
 
@@ -104,10 +114,13 @@
      * @return an instance of RootcanalController
      */
     public RootcanalController enableRootcanal()
-            throws DeviceNotAvailableException, IOException, InterruptedException,
-                    TimeoutException {
+            throws DeviceNotAvailableException, IOException, InterruptedException {
         ITestDevice device = test.getDevice();
         assertNotNull("Device not set", device);
+        assumeTrue(
+                "Device does not seem to have Bluetooth",
+                device.hasFeature("android.hardware.bluetooth")
+                        || device.hasFeature("android.hardware.bluetooth_le"));
         CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(test.getBuild());
 
         // Check and made sure we're not calling this more than once for a device
@@ -174,34 +187,40 @@
                         + "/vendor/lib/hw/android.hardware.bluetooth@1.1-impl-sim.so "
                         + "/vendor/lib64/hw/android.hardware.bluetooth@1.1-impl-sim.so");
 
-        // Kill currently running BT HAL.
-        if (ProcessUtil.killAll(device, "android\\.hardware\\.bluetooth@.*", 10_000, false)) {
-            CLog.d("Killed existing BT HAL");
-        } else {
-            CLog.w("No existing BT HAL was found running");
+        try {
+            // Kill currently running BT HAL.
+            if (ProcessUtil.killAll(device, "android\\.hardware\\.bluetooth@.*", 10_000, false)) {
+                CLog.d("Killed existing BT HAL");
+            } else {
+                CLog.w("No existing BT HAL was found running");
+            }
+
+            // Kill hwservicemanager, wait for it to come back up on its own, and wait for it
+            // to finish initializing. This is needed to reload the VINTF and HAL rc information.
+            // Note that a userspace reboot would not work here because hwservicemanager starts
+            // before userdata is mounted.
+            device.setProperty("hwservicemanager.ready", "false");
+            ProcessUtil.killAll(device, "hwservicemanager$", 10_000);
+            waitPropertyValue(device, "hwservicemanager.ready", "true", 10_000);
+            TimeUnit.SECONDS.sleep(30);
+
+            // Launch the new HAL
+            List<String> cmd =
+                    List.of(
+                            "adb",
+                            "-s",
+                            device.getSerialNumber(),
+                            "shell",
+                            "/vendor/bin/hw/android.hardware.bluetooth@1.1-service.sim");
+            RunUtil.getDefault().runCmdInBackground(cmd);
+            ProcessUtil.waitProcessRunning(
+                    device, "android\\.hardware\\.bluetooth@1\\.1-service\\.sim", 10_000);
+        } catch (TimeoutException e) {
+            assumeNoException("Could not start virtual BT HAL", e);
+        } catch (ProcessUtil.KillException e) {
+            assumeNoException("Failed to kill process", e);
         }
 
-        // Kill hwservicemanager, wait for it to come back up on its own, and wait for it
-        // to finish initializing. This is needed to reload the VINTF and HAL rc information.
-        // Note that a userspace reboot would not work here because hwservicemanager starts
-        // before userdata is mounted.
-        device.setProperty("hwservicemanager.ready", "false");
-        ProcessUtil.killAll(device, "hwservicemanager$", 10_000);
-        waitPropertyValue(device, "hwservicemanager.ready", "true", 10_000);
-        TimeUnit.SECONDS.sleep(30);
-
-        // Launch the new HAL
-        List<String> cmd =
-                List.of(
-                        "adb",
-                        "-s",
-                        device.getSerialNumber(),
-                        "shell",
-                        "/vendor/bin/hw/android.hardware.bluetooth@1.1-service.sim");
-        RunUtil.getDefault().runCmdInBackground(cmd);
-        ProcessUtil.waitProcessRunning(
-                device, "android\\.hardware\\.bluetooth@1\\.1-service\\.sim", 10_000);
-
         // Reenable Bluetooth and enable RootCanal control channel
         String checkCmd = "netstat -l -t -n -W | grep '0\\.0\\.0\\.0:6111'";
         while (true) {
diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/SystemUtil.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/SystemUtil.java
new file mode 100644
index 0000000..5c2d3ef
--- /dev/null
+++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/SystemUtil.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.sts.common;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.junit.Assume.assumeThat;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.util.CommandResult;
+
+import java.util.Optional;
+
+/** Various system-related helper functions */
+public class SystemUtil {
+    private SystemUtil() {}
+
+    /**
+     * Set the value of a property and set it back to old value upon closing.
+     *
+     * @param device the device to use
+     * @param name the name of the property to set
+     * @param value the value that the property should be set to
+     * @return AutoCloseable that resets the property back to old value upon closing
+     */
+    public static AutoCloseable withProperty(
+            final ITestDevice device, final String name, final String value)
+            throws DeviceNotAvailableException {
+        final String oldValue = device.getProperty(name);
+        assumeTrue("Could not set property: " + name, device.setProperty(name, value));
+        return new AutoCloseable() {
+            @Override
+            public void close() throws Exception {
+                assumeTrue(
+                        "Could not reset property: " + name,
+                        device.setProperty(name, oldValue == null ? "" : oldValue));
+            }
+        };
+    }
+
+    /**
+     * Set the value of a device setting and set it back to old value upon closing.
+     *
+     * @param device the device to use
+     * @param namespace "system", "secure", or "global"
+     * @param key setting key to set
+     * @param value setting value to set to
+     * @return AutoCloseable that resets the setting back to existing value upon closing.
+     */
+    public static AutoCloseable withSetting(
+            final ITestDevice device, final String namespace, final String key, String value)
+            throws DeviceNotAvailableException {
+        String getSettingRes = device.getSetting(namespace, key);
+        final Optional<String> oldSetting = Optional.ofNullable(getSettingRes);
+
+        device.setSetting(namespace, key, value);
+        assumeThat(
+                String.format("Could not set %s:%s to %s", namespace, key, value),
+                device.getSetting(namespace, key),
+                equalTo(value));
+
+        return new AutoCloseable() {
+            @Override
+            public void close() throws Exception {
+                if (!oldSetting.isPresent()) {
+                    String cmd = String.format("settings delete %s %s", namespace, key);
+                    CommandResult res = CommandUtil.runAndCheck(device, cmd);
+                } else {
+                    String oldValue = oldSetting.get();
+                    device.setSetting(namespace, key, oldValue);
+                    String failMsg =
+                            String.format("could not reset '%s' back to '%s'", key, oldValue);
+                    assumeThat(failMsg, device.getSetting(namespace, key), equalTo(oldValue));
+                }
+            }
+        };
+    }
+}
diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/tradefed/testtype/RootSecurityTestCase.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/tradefed/testtype/RootSecurityTestCase.java
index d299e71..2398713 100644
--- a/libraries/sts-common-util/host-side/src/com/android/sts/common/tradefed/testtype/RootSecurityTestCase.java
+++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/tradefed/testtype/RootSecurityTestCase.java
@@ -19,8 +19,6 @@
 import static org.junit.Assume.assumeTrue;
 
 import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.log.LogUtil.CLog;
 
 import org.junit.Before;
 
@@ -37,22 +35,4 @@
     public void setUpRoot() throws DeviceNotAvailableException {
         assumeTrue("Could not enable adb root on device.", getDevice().enableAdbRoot());
     }
-
-    /**
-     * Try to enable adb root on device.
-     *
-     * <p>Use {@link NativeDevice#enableAdbRoot()} internally. The test methods calling this
-     * function should run even if enableAdbRoot fails, which is why the return value is ignored.
-     * However, we may want to act on that data point in the future.
-     */
-    protected static boolean enableAdbRoot(ITestDevice device) throws DeviceNotAvailableException {
-        if (device.enableAdbRoot()) {
-            return true;
-        } else {
-            CLog.e(
-                    "\"enable-root\" set to false! "
-                            + "Root is required to check if device is vulnerable.");
-            return false;
-        }
-    }
 }
diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/tradefed/testtype/SecurityTestCase.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/tradefed/testtype/SecurityTestCase.java
index 3295f09..5280bb7 100644
--- a/libraries/sts-common-util/host-side/src/com/android/sts/common/tradefed/testtype/SecurityTestCase.java
+++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/tradefed/testtype/SecurityTestCase.java
@@ -16,8 +16,9 @@
 
 package com.android.sts.common.tradefed.testtype;
 
+import static com.google.common.truth.Truth.assertWithMessage;
+
 import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeFalse;
 import static org.junit.Assume.assumeTrue;
 
@@ -50,6 +51,11 @@
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+/**
+ * Base test class for all STS tests.
+ *
+ * <p>Use {@link RootSecurityTestCase} or {@link NonRootSecurityTestCase} instead.
+ */
 public class SecurityTestCase extends StsExtraBusinessLogicHostTestBase {
 
     private static final String LOG_TAG = "SecurityTestCase";
@@ -122,10 +128,7 @@
         }
     }
 
-    /**
-     * Makes sure the phone is online, and the ensure the current boottime is within 2 seconds (due
-     * to rounding) of the previous boottime to check if The phone has crashed.
-     */
+    /** Makes sure the phone is online and checks if the device crashed */
     @After
     public void tearDown() throws Exception {
         try {
@@ -136,17 +139,26 @@
             getDevice().waitForDeviceAvailable(30 * 1000);
         }
 
-        if (kernelStartTime != -1) {
-            // only fail when the kernel start time is valid
-            long deviceTime = getDeviceUptime() + kernelStartTime;
-            long hostTime = System.currentTimeMillis() / 1000;
-            assertTrue("Phone has had a hard reset", (hostTime - deviceTime) < 2);
-            kernelStartTime = -1;
-        }
-
         logAndTerminateTestProcesses();
 
-        // TODO(badash@): add ability to catch runtime restart
+        long lastKernelStartTime = kernelStartTime;
+        kernelStartTime = -1;
+        // only test when the kernel start time is valid
+        if (lastKernelStartTime != -1) {
+            long currentKernelStartTime = getKernelStartTime();
+            String bootReason = "(could not get bootreason)";
+            try {
+                bootReason = getDevice().getProperty("ro.boot.bootreason");
+            } catch (DeviceNotAvailableException e) {
+                CLog.e("Could not get ro.boot.bootreason", e);
+            }
+            assertWithMessage(
+                            "The device has unexpectedly rebooted (%s seconds after last recorded"
+                                    + " boot time, bootreason: %s)",
+                            currentKernelStartTime - lastKernelStartTime, bootReason)
+                    .that(currentKernelStartTime)
+                    .isLessThan(lastKernelStartTime + 10);
+        }
     }
 
     public static IBuildInfo getBuildInfo(ITestDevice device) {
@@ -366,10 +378,14 @@
         updateKernelStartTime();
     }
 
+    private long getKernelStartTime() throws DeviceNotAvailableException {
+        long uptime = getDeviceUptime();
+        return (System.currentTimeMillis() / 1000) - uptime;
+    }
+
     /** Allows a test to pass if called after a planned reboot. */
     public void updateKernelStartTime() throws DeviceNotAvailableException {
-        long uptime = getDeviceUptime();
-        kernelStartTime = (System.currentTimeMillis() / 1000) - uptime;
+        kernelStartTime = getKernelStartTime();
     }
 
     /**
@@ -446,13 +462,13 @@
      * <p>Example of skipping a test based on mainline modules:
      *
      * <pre>
-     *  @Test
+     *  {@literal @}Test
      *  public void testPocCVE_1234_5678() throws Exception {
      *      // This will skip the test if MODULE_METADATA mainline module is play managed.
      *      assumeFalse(moduleIsPlayManaged("com.google.android.captiveportallogin"));
      *      // Do testing...
      *  }
-     *  * </pre>
+     * </pre>
      */
     public boolean moduleIsPlayManaged(String modulePackageName) throws Exception {
         return mainlineModuleDetector.getPlayManagedModules().contains(modulePackageName);
@@ -514,6 +530,15 @@
         } while (System.currentTimeMillis() < endTime);
 
         assumeFalse("Wi-Fi could not be enabled on the device; skipping", skipWifiFailure);
+        // enable with one of the following:
+        // '<option name="compatibility-build-provider:build-attribute" key="sts-skip-wifi-failures"
+        // value="true" />'
+        // '--compatibility-build-provider:build-attribute sts-skip-wifi-failures=true'
+        boolean buildAttributeSkipWifiFailure =
+                Boolean.parseBoolean(getBuild().getBuildAttributes().get("sts-skip-wifi-failures"));
+        assumeFalse(
+                "Wi-Fi could not be enabled on the device; skipping",
+                buildAttributeSkipWifiFailure);
         throw new AssertionError(
                 "This test requires a Wi-Fi connection on-device. "
                         + "Please consult the CTS setup guide: "
diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/util/TombstoneParser.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/util/TombstoneParser.java
index a5cffc5..b11c805 100644
--- a/libraries/sts-common-util/host-side/src/com/android/sts/common/util/TombstoneParser.java
+++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/util/TombstoneParser.java
@@ -59,7 +59,8 @@
                     "signal (?<number>\\d+?) \\((?<name>.+?)\\), code (?<code>(?:-)?\\d+?)"
                             + " \\((?<codename>\\S+?)(?: from pid (?<senderpid>\\d+?), uid"
                             + " (?<senderuid>\\d+?))?\\), fault addr"
-                            + " (?:0x)?(?<faultaddress>\\p{XDigit}{1,16}|--------)");
+                            + " (?:0x)?(?<faultaddress>\\p{XDigit}{1,16}|--------)"
+                            + "( (?<register>\\(.+\\)))?");
     private static final Pattern CAUSE_PATTERN = Pattern.compile("Cause: (?<cause>.*?)");
     // Be greedy because some abort messages are multiple lines long
     private static final Pattern ABORT_PATTERN =
diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/util/TombstoneUtils.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/util/TombstoneUtils.java
index 786a366..77c8972 100644
--- a/libraries/sts-common-util/host-side/src/com/android/sts/common/util/TombstoneUtils.java
+++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/util/TombstoneUtils.java
@@ -99,7 +99,8 @@
                                                         try {
                                                             ProcessUtil.waitPidExited(device, pid);
                                                         } catch (TimeoutException
-                                                                | DeviceNotAvailableException e) {
+                                                                | DeviceNotAvailableException
+                                                                | ProcessUtil.KillException e) {
                                                             CLog.w(e);
                                                         }
                                                     });
diff --git a/libraries/sts-common-util/host-side/tests/Android.bp b/libraries/sts-common-util/host-side/tests/Android.bp
index d4e2a0f..7d2c3b1 100644
--- a/libraries/sts-common-util/host-side/tests/Android.bp
+++ b/libraries/sts-common-util/host-side/tests/Android.bp
@@ -26,6 +26,7 @@
 
     static_libs: [
         "sts-host-util",
+        "mockito-host",
     ],
 
     // tag this module as a test artifact
diff --git a/libraries/sts-common-util/host-side/tests/res/logcat.txt b/libraries/sts-common-util/host-side/tests/res/logcat.txt
index a9561ff..e887e0d 100644
--- a/libraries/sts-common-util/host-side/tests/res/logcat.txt
+++ b/libraries/sts-common-util/host-side/tests/res/logcat.txt
@@ -2724,3 +2724,19 @@
 11-08 17:31:00.084 22420 22420 F DEBUG   :       #08 pc 00000000001874f8  /data/data/avrc_poc
 11-08 17:31:00.084 22420 22420 F DEBUG   :       #09 pc 0000000000187178  /data/data/avrc_poc
 11-08 17:31:00.084 22420 22420 F DEBUG   :       #10 pc 0000000000051f9c  /apex/com.android.runtime/lib64/bionic/libc.so (__libc_init+136) (BuildId: 4e9eb0a9a3a51fd38c54515e3d4fdf99)
+01-04 15:28:42.566  7051  7051 F DEBUG   : *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
+01-04 15:28:42.567  7051  7051 F DEBUG   : Build fingerprint: 'Realtek/RealtekThor/RealtekThor:11/RVC/12120ffa7ab:userdebug/release-keys'
+01-04 15:28:42.567  7051  7051 F DEBUG   : Revision: '0'
+01-04 15:28:42.567  7051  7051 F DEBUG   : ABI: 'arm'
+01-04 15:28:42.568  7051  7051 F DEBUG   : Timestamp: 2023-01-04 15:28:42+0800
+01-04 15:28:42.568  7051  7051 F DEBUG   : pid: 7043, tid: 7043, name: CVE-2020-0240  >>> /data/local/tmp/CVE-2020-0240 <<<
+01-04 15:28:42.568  7051  7051 F DEBUG   : uid: 2000
+01-04 15:28:42.568  7051  7051 F DEBUG   : signal 4 (SIGILL), code 1 (ILL_ILLOPC), fault addr 0xa7fc780a (*pc=0xb68defe)
+01-04 15:28:42.568  7051  7051 F DEBUG   :     r0  00000001  r1  410dc023  r2  410dc023  r3  00000000
+01-04 15:28:42.568  7051  7051 F DEBUG   :     r4  a709f1fa  r5  a709c7e8  r6  a7098030  r7  be850978
+01-04 15:28:42.568  7051  7051 F DEBUG   :     r8  be85099c  r9  00000001  r10 be850c48  r11 be850a40
+01-04 15:28:42.568  7051  7051 F DEBUG   :     ip  a8317398  sp  be84e788  lr  a7b8bc0d  pc  a7fc780a
+01-04 15:28:42.710  7051  7051 F DEBUG   : backtrace:
+01-04 15:28:42.710  7051  7051 F DEBUG   :       #00 pc 0050780a  /apex/com.android.art/lib/libpac.so (v8::base::OS::Abort()+14) (BuildId: b1ec1e3ff7cf26e92a2f475a0e47445c)
+01-04 15:28:42.710  7051  7051 F DEBUG   :       #01 pc 00000016  <unknown>
+
diff --git a/libraries/sts-common-util/host-side/tests/res/malloc_debug_logcat.txt b/libraries/sts-common-util/host-side/tests/res/malloc_debug_logcat.txt
new file mode 100644
index 0000000..c6ad1d1
--- /dev/null
+++ b/libraries/sts-common-util/host-side/tests/res/malloc_debug_logcat.txt
@@ -0,0 +1,64 @@
+E/libc    ( 7233): +++ ALLOCATION 0x404b9278 SIZE 10 BYTES MULTIPLY FREED!
+E/libc    ( 7233): +++ ALLOCATION 0x404b9278 SIZE 10 ALLOCATED HERE:
+E/libc    ( 7233): *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
+E/libc    ( 7233):      #00  pc 0000c35a  /system/lib/libc_malloc_debug_leak.so
+E/libc    ( 7233):      #01  pc 0000c658  /system/lib/libc_malloc_debug_leak.so
+E/libc    ( 7233):      #02  pc 00016d80  /system/lib/libc.so
+E/libc    ( 7233):      #03  pc 4009647c  /system/bin/malloctest
+E/libc    ( 7233):      #04  pc 00016f24  /system/lib/libc.so
+E/libc    ( 7233): +++ ALLOCATION 0x404b9278 SIZE 10 FIRST FREED HERE:
+E/libc    ( 7233): *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
+E/libc    ( 7233):      #00  pc 0000c35a  /system/lib/libc_malloc_debug_leak.so
+E/libc    ( 7233):      #01  pc 0000c7d2  /system/lib/libc_malloc_debug_leak.so
+E/libc    ( 7233):      #02  pc 00016d94  /system/lib/libc.so
+E/libc    ( 7233):      #03  pc 40096490  /system/bin/malloctest
+E/libc    ( 7233):      #04  pc 00016f24  /system/lib/libc.so
+E/libc    ( 7233): +++ ALLOCATION 0x404b9278 SIZE 10 NOW BEING FREED HERE:
+E/libc    ( 7233): *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
+E/libc    ( 7233):      #00  pc 0000c35a  /system/lib/libc_malloc_debug_leak.so
+E/libc    ( 7233):      #01  pc 0000c6ac  /system/lib/libc_malloc_debug_leak.so
+E/libc    ( 7233):      #02  pc 00016d94  /system/lib/libc.so
+E/libc    ( 7233):      #03  pc 400964a0  /system/bin/malloctest
+E/libc    ( 7233):      #04  pc 00016f24  /system/lib/libc.soThe following for a heap overrun and underrun:E/libc    ( 7233): +++ REAR GUARD MISMATCH [10, 11)
+E/libc    ( 7233): +++ ALLOCATION 0x404b9198 SIZE 10 HAS A CORRUPTED REAR GUARD
+E/libc    ( 7233): +++ ALLOCATION 0x404b9198 SIZE 10 ALLOCATED HERE:
+E/libc    ( 7233): *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
+E/libc    ( 7233):      #00  pc 0000c35a  /system/lib/libc_malloc_debug_leak.so
+E/libc    ( 7233):      #01  pc 0000c658  /system/lib/libc_malloc_debug_leak.so
+E/libc    ( 7233):      #02  pc 00016d80  /system/lib/libc.so
+E/libc    ( 7233):      #03  pc 40096438  /system/bin/malloctest
+E/libc    ( 7233):      #04  pc 00016f24  /system/lib/libc.so
+E/libc    ( 7233): +++ ALLOCATION 0x404b9198 SIZE 10 FREED HERE:
+E/libc    ( 7233): *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
+E/libc    ( 7233):      #00  pc 0000c35a  /system/lib/libc_malloc_debug_leak.so
+E/libc    ( 7233):      #01  pc 0000c7d2  /system/lib/libc_malloc_debug_leak.so
+E/libc    ( 7233):      #02  pc 00016d94  /system/lib/libc.so
+E/libc    ( 7233):      #03  pc 40096462  /system/bin/malloctest
+E/libc    ( 7233):      #04  pc 00016f24  /system/lib/libc.so
+E/libc    ( 7233): +++ ALLOCATION 0x404b9358 SIZE 10 HAS A CORRUPTED FRONT GUARD
+E/libc    ( 7233): +++ ALLOCATION 0x404b9358 SIZE 10 ALLOCATED HERE:
+E/libc    ( 7233): *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
+E/libc    ( 7233):      #00  pc 0000c35a  /system/lib/libc_malloc_debug_leak.so
+E/libc    ( 7233):      #01  pc 0000c658  /system/lib/libc_malloc_debug_leak.so
+E/libc    ( 7233):      #02  pc 00016d80  /system/lib/libc.so
+E/libc    ( 7233):      #03  pc 400964ba  /system/bin/malloctest
+E/libc    ( 7233):      #04  pc 00016f24  /system/lib/libc.so
+E/libc    ( 7233): +++ ALLOCATION 0x404b9358 SIZE 10 FREED HERE:
+E/libc    ( 7233): *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
+E/libc    ( 7233):      #00  pc 0000c35a  /system/lib/libc_malloc_debug_leak.so
+E/libc    ( 7233):      #01  pc 0000c7d2  /system/lib/libc_malloc_debug_leak.so
+E/libc    ( 7233):      #02  pc 00016d94  /system/lib/libc.so
+E/libc    ( 7233):      #03  pc 400964e4  /system/bin/malloctest
+E/libc    ( 7233):      #04  pc 00016f24  /system/lib/libc.soThe following for a memory leak:E/libc    ( 7233): +++ THERE ARE 1 LEAKED ALLOCATIONS
+E/libc    ( 7233): +++ DELETING 4096 BYTES OF LEAKED MEMORY AT 0x404b95e8 (1 REMAINING)
+E/libc    ( 7233): +++ ALLOCATION 0x404b95e8 SIZE 4096 ALLOCATED HERE:
+E/libc    ( 7233): *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
+E/libc    ( 7233):      #00  pc 0000c35a  /system/lib/libc_malloc_debug_leak.so
+E/libc    ( 7233):      #01  pc 0000c658  /system/lib/libc_malloc_debug_leak.so
+E/libc    ( 7233):      #02  pc 00016d80  /system/lib/libc.so
+E/libc    ( 7233):      #03  pc 0001bc94  /system/lib/libc.so
+E/libc    ( 7233):      #04  pc 0001edf6  /system/lib/libc.so
+E/libc    ( 7233):      #05  pc 0001b80a  /system/lib/libc.so
+E/libc    ( 7233):      #06  pc 0001c086  /system/lib/libc.so
+E/libc    ( 7233):      #07  pc 40096402  /system/bin/malloctest
+E/libc    ( 7233):      #08  pc 00016f24  /system/lib/libc.so
diff --git a/libraries/sts-common-util/host-side/tests/res/nativepoc.res b/libraries/sts-common-util/host-side/tests/res/nativepoc.res
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/libraries/sts-common-util/host-side/tests/res/nativepoc.res
diff --git a/libraries/sts-common-util/host-side/tests/res/tombstones/tombstone-014.pb b/libraries/sts-common-util/host-side/tests/res/tombstones/tombstone-014.pb
index 24dcbca..7917951 100644
--- a/libraries/sts-common-util/host-side/tests/res/tombstones/tombstone-014.pb
+++ b/libraries/sts-common-util/host-side/tests/res/tombstones/tombstone-014.pb
Binary files differ
diff --git a/libraries/sts-common-util/host-side/tests/res/tombstones/tombstone-016.pb b/libraries/sts-common-util/host-side/tests/res/tombstones/tombstone-016.pb
new file mode 100644
index 0000000..b06c5ff
--- /dev/null
+++ b/libraries/sts-common-util/host-side/tests/res/tombstones/tombstone-016.pb
Binary files differ
diff --git a/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/MallocDebugTest.java b/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/MallocDebugTest.java
new file mode 100644
index 0000000..03b1934
--- /dev/null
+++ b/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/MallocDebugTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.sts.common;
+
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/** Unit tests for {@link MallocDebug}. */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class MallocDebugTest extends BaseHostJUnit4Test {
+    private static String logcatWithErrors = null;
+    private static String logcatWithoutErrors = null;
+
+    @BeforeClass
+    public static void setUpClass() throws IOException {
+        try (InputStream is1 =
+                        MallocDebugTest.class
+                                .getClassLoader()
+                                .getResourceAsStream("malloc_debug_logcat.txt");
+                InputStream is2 =
+                        MallocDebugTest.class.getClassLoader().getResourceAsStream("logcat.txt")) {
+            logcatWithErrors = new String(is1.readAllBytes());
+            logcatWithoutErrors = new String(is2.readAllBytes());
+        }
+    }
+
+    @Test(expected = Test.None.class /* no exception expected */)
+    public void testMallocDebugNoErrors() throws Exception {
+        MallocDebug.assertNoMallocDebugErrors(logcatWithoutErrors);
+    }
+
+    @Test(expected = AssertionError.class)
+    public void testMallocDebugWithErrors() throws Exception {
+        MallocDebug.assertNoMallocDebugErrors(logcatWithErrors);
+    }
+}
diff --git a/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/NativePocTest.java b/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/NativePocTest.java
new file mode 100644
index 0000000..9763103
--- /dev/null
+++ b/libraries/sts-common-util/host-side/tests/src/com/android/sts/common/NativePocTest.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.sts.common;
+
+import static org.hamcrest.core.StringContains.containsString;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyLong;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.contains;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.startsWith;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import com.android.tradefed.build.BuildInfo;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.invoker.InvocationContext;
+import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.testtype.Abi;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.IAbi;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.RunUtil;
+
+import com.google.common.collect.ImmutableMap;
+
+import org.junit.After;
+import org.junit.AssumptionViolatedException;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.concurrent.TimeUnit;
+
+/** Unit tests for {@link NativePoc}. */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class NativePocTest extends BaseHostJUnit4Test {
+    private static final IAbi ABI_ARM32 = new Abi("armeabi-v7a", "32");
+    private static final IAbi ABI_ARM64 = new Abi("aarch64", "64");
+    private static final String POC_NAME = "poc_name";
+    private static final String TEST_RESOURCE = "nativepoc.res";
+    private static final String REMOTE_POC_FILE = NativePoc.TMP_PATH + POC_NAME;
+    private static final CommandResult TIMEOUT_RESULT = new CommandResult(CommandStatus.TIMED_OUT);
+    private static final CommandResult SUCCESS_RESULT = new CommandResult(CommandStatus.SUCCESS);
+    private static final CommandResult VULN_RESULT = new CommandResult(CommandStatus.FAILED);
+
+    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+    @Rule public ExpectedException exceptionRule = ExpectedException.none();
+    @Mock private ITestDevice device;
+
+    private BaseHostJUnit4Test testCase;
+    private Path testCasesDir;
+    private BuildInfo buildInfo;
+    private String tmpDir;
+    private File localPocFile32;
+    private File localPocFile64;
+
+    @BeforeClass
+    public static void setupClass() throws Exception {
+        VULN_RESULT.setExitCode(113);
+        VULN_RESULT.setStderr("stderr");
+        VULN_RESULT.setStdout("stdout");
+    }
+
+    @Before
+    public void setup() throws Exception {
+        tmpDir = Files.createTempDirectory("").toFile().getAbsolutePath();
+        testCasesDir = Paths.get(tmpDir, "android-sts-host-util-test", "testcases");
+        Files.createDirectories(testCasesDir);
+        localPocFile32 = new File(testCasesDir.toFile(), POC_NAME + "_sts32");
+        localPocFile32.createNewFile();
+        localPocFile64 = new File(testCasesDir.toFile(), POC_NAME + "_sts64");
+        localPocFile64.createNewFile();
+
+        buildInfo = new BuildInfo("0", "");
+        buildInfo.addBuildAttribute("ROOT_DIR", tmpDir);
+        buildInfo.addBuildAttribute("SUITE_NAME", "sts-host-util-test");
+
+        InvocationContext iContext = new InvocationContext();
+        iContext.addAllocatedDevice("device1", device);
+        iContext.addDeviceBuildInfo("device1", buildInfo);
+
+        testCase = new BaseHostJUnit4Test() {};
+        testCase.setTestInformation(
+                TestInformation.newBuilder().setInvocationContext(iContext).build());
+        testCase.setAbi(ABI_ARM32); // Default to 32bit machine. Re-set this if testing 64
+
+        when(device.executeShellV2Command(startsWith("chmod "))).thenReturn(SUCCESS_RESULT);
+        when(device.executeShellV2Command(startsWith("test "))).thenReturn(SUCCESS_RESULT);
+    }
+
+    @After
+    public void cleanup() throws Exception {
+        RunUtil.getDefault().runTimedCmd(0, "rm", "-r", tmpDir);
+    }
+
+    @Test
+    public void testBasicPocExecution() throws Exception {
+        when(device.executeShellV2Command(anyString(), anyLong(), any(), anyInt()))
+                .thenReturn(SUCCESS_RESULT);
+
+        NativePoc.builder().pocName(POC_NAME).build().run(testCase);
+
+        verifyPocCorrectlyPushed(localPocFile32);
+        verify(device)
+                .executeShellV2Command(
+                        contains(POC_NAME),
+                        eq(NativePoc.DEFAULT_POC_TIMEOUT_SECONDS),
+                        eq(TimeUnit.SECONDS),
+                        eq(0));
+        verify(device).deleteFile(REMOTE_POC_FILE);
+        verifyNoMoreInteractions(device);
+    }
+
+    @Test
+    public void testPocExecutionWithArgsAndEnvVarsAndTimeout() throws Exception {
+        when(device.executeShellV2Command(anyString(), anyLong(), any(), anyInt()))
+                .thenReturn(SUCCESS_RESULT);
+
+        NativePoc.builder()
+                .envVars(ImmutableMap.of("VAR1", "val1", "LD_LIBRARY_PATH", "/val2"))
+                .pocName(POC_NAME)
+                .args("arg1", "arg2")
+                .useDefaultLdLibraryPath(true)
+                .timeoutSeconds(100)
+                .build()
+                .run(testCase);
+
+        verifyPocCorrectlyPushed(localPocFile32);
+        verify(device)
+                .executeShellV2Command(
+                        contains(
+                                "VAR1='val1' LD_LIBRARY_PATH='/val2:/system/lib64:/system/lib' ./"
+                                        + POC_NAME
+                                        + " arg1 arg2"),
+                        eq(100L),
+                        eq(TimeUnit.SECONDS),
+                        eq(0));
+        verify(device).deleteFile(REMOTE_POC_FILE);
+        verifyNoMoreInteractions(device);
+    }
+
+    @Test
+    public void testPocTimeout() throws Exception {
+        when(device.executeShellV2Command(anyString(), anyLong(), any(), anyInt()))
+                .thenReturn(TIMEOUT_RESULT);
+        exceptionRule.expect(AssumptionViolatedException.class);
+        exceptionRule.expectMessage(containsString("PoC timed out"));
+        NativePoc.builder().pocName(POC_NAME).timeoutSeconds(100).build().run(testCase);
+    }
+
+    @Test
+    public void testPocExecutionWithResourceAndAfter() throws Exception {
+        when(device.executeShellV2Command(anyString(), anyLong(), any(), anyInt()))
+                .thenReturn(SUCCESS_RESULT);
+
+        NativePoc.builder()
+                .pocName(POC_NAME)
+                .resources(TEST_RESOURCE)
+                .resourcePushLocation("/tmp")
+                .after(res -> testCase.getDevice().executeShellV2Command("echo EXTRA AFTER CMD"))
+                .build()
+                .run(testCase);
+
+        verify(device).pushFile(any(), eq("/tmp/" + TEST_RESOURCE));
+        verifyPocCorrectlyPushed(localPocFile32);
+        verify(device)
+                .executeShellV2Command(
+                        contains(POC_NAME),
+                        eq(NativePoc.DEFAULT_POC_TIMEOUT_SECONDS),
+                        eq(TimeUnit.SECONDS),
+                        eq(0));
+        verify(device).executeShellV2Command("echo EXTRA AFTER CMD");
+        verify(device).deleteFile(REMOTE_POC_FILE);
+        verify(device).deleteFile("/tmp/" + TEST_RESOURCE);
+        verifyNoMoreInteractions(device);
+    }
+
+    @Test
+    public void test64bit() throws Exception {
+        testCase.setAbi(ABI_ARM64);
+        when(device.executeShellV2Command(anyString(), anyLong(), any(), anyInt()))
+                .thenReturn(SUCCESS_RESULT);
+
+        NativePoc.builder().pocName(POC_NAME).build().run(testCase);
+        verifyPocCorrectlyPushed(localPocFile64);
+    }
+
+    @Test
+    public void testBadBitness() throws Exception {
+        when(device.executeShellV2Command(anyString(), anyLong(), any(), anyInt()))
+                .thenReturn(SUCCESS_RESULT);
+
+        exceptionRule.expect(AssumptionViolatedException.class);
+        NativePoc.builder().pocName(POC_NAME).only64().build().run(testCase);
+    }
+
+    @Test
+    public void testAsserter() throws Exception {
+        when(device.executeShellV2Command(anyString(), anyLong(), any(), anyInt()))
+                .thenReturn(VULN_RESULT);
+
+        exceptionRule.expect(AssertionError.class);
+        exceptionRule.expectMessage(containsString("113"));
+
+        NativePoc.builder()
+                .pocName(POC_NAME)
+                .asserter(NativePocStatusAsserter.assertNotVulnerableExitCode())
+                .build()
+                .run(testCase);
+
+        verifyPocCorrectlyPushed(localPocFile32);
+        verify(device)
+                .executeShellV2Command(
+                        contains(POC_NAME),
+                        eq(NativePoc.DEFAULT_POC_TIMEOUT_SECONDS),
+                        eq(TimeUnit.SECONDS),
+                        eq(0));
+        verify(device).deleteFile(REMOTE_POC_FILE);
+        verifyNoMoreInteractions(device);
+    }
+
+    private void verifyPocCorrectlyPushed(File poc) throws Exception {
+        verify(device).pushFile(poc, REMOTE_POC_FILE);
+        verify(device).executeShellV2Command("chmod 777 '/data/local/tmp/" + POC_NAME + "'");
+        verify(device).executeShellV2Command("test -r '/data/local/tmp/" + POC_NAME + "'");
+        verify(device).executeShellV2Command("test -w '/data/local/tmp/" + POC_NAME + "'");
+        verify(device).executeShellV2Command("test -x '/data/local/tmp/" + POC_NAME + "'");
+    }
+}
diff --git a/libraries/sts-common-util/sts-sdk/Android.mk b/libraries/sts-common-util/sts-sdk/Android.mk
index f4b45e8..245748b 100644
--- a/libraries/sts-common-util/sts-sdk/Android.mk
+++ b/libraries/sts-common-util/sts-sdk/Android.mk
@@ -22,6 +22,8 @@
 		sed -i 's~{{PLATFORM_SDK_VERSION}}~$(PLATFORM_SDK_VERSION)~g' $${tmplfile}; \
 		mv $${tmplfile} $${tmplfile/.template/}; \
 	done
-	$(SOONG_ZIP) -o $@ -C $(STS_SDK_TMP_DIR) -D $(STS_SDK_TMP_DIR)
+	# Build system can't cleanly handle hidden files
+	mv $(STS_SDK_TMP_DIR)/dotidea $(STS_SDK_TMP_DIR)/.idea
+	$(SOONG_ZIP) -o $@ -C $(STS_SDK_TMP_DIR) -D $(STS_SDK_TMP_DIR) -D $(STS_SDK_TMP_DIR)/.idea
 
 sts_sdk_sample_files :=
diff --git a/libraries/sts-common-util/sts-sdk/package/README.md b/libraries/sts-common-util/sts-sdk/package/README.md
new file mode 100644
index 0000000..663994a
--- /dev/null
+++ b/libraries/sts-common-util/sts-sdk/package/README.md
@@ -0,0 +1,2 @@
+See https://source.android.com/docs/security/test/sts-sdK for instructions and
+documentation.
diff --git a/libraries/sts-common-util/sts-sdk/package/build.gradle b/libraries/sts-common-util/sts-sdk/package/build.gradle
index ee00121..a7ef692 100644
--- a/libraries/sts-common-util/sts-sdk/package/build.gradle
+++ b/libraries/sts-common-util/sts-sdk/package/build.gradle
@@ -5,36 +5,92 @@
 
 task clean(type: Delete) {
     delete layout.buildDirectory
+    if (findProject('native-poc') != null) {
+        delete project('native-poc').layout.projectDirectory.dir('.cxx')
+    }
 }
 
-ext {
-    testAppApkName = 'CVE_2020_0215.apk'
+ext.copyArtifacts = { nativeDir ->
+    copy {
+        from project('sts-test').layout.buildDirectory.file('testcases')
+
+        if (findProject('native-poc') != null) {
+            from project('native-poc').layout.buildDirectory.file(nativeDir)
+        }
+
+        into layout.buildDirectory.dir('android-sts/testcases')
+    }
+
+    // TODO: figure out variants
+    if (findProject('test-app') != null) {
+        copy {
+            from project('test-app').layout.buildDirectory.file('outputs/apk/debug')
+            rename '(.*).apk', 'sts_test_app_package.apk'
+            include '**/*.apk'
+            into layout.buildDirectory.dir('android-sts/testcases')
+        }
+    }
+
+    // To add another Android apk to the test, copy the block above and rename
+    // the project name to your submodule as well as the APK output filename.
+    // Remember to use that APK file name in your `sts-test`.
+
+    copy {
+        from project('sts-test').layout.projectDirectory.file('libs')
+        into layout.buildDirectory.dir('android-sts/tools')
+    }
 }
 
 task assembleStsARM {
     dependsOn ':sts-test:copyHostSideTest'
-    dependsOn ':native-poc:copyArm32'
-    dependsOn ':native-poc:copyArm64'
-    dependsOn ':test-app:assemble'
+
+    if (findProject('native-poc') != null) {
+        dependsOn ':native-poc:copyarmeabi-v7a'
+        dependsOn ':native-poc:copyarm64-v8a'
+    }
+
+    if (findProject('test-app') != null) {
+        dependsOn ':test-app:assemble'
+    }
+
+    // To add another Android apk to the test, copy the block above and rename
+    // the project name to your new submodule
 
     doLast {
-        copy {
-            from project('sts-test').layout.buildDirectory.file('testcases')
-            from project('native-poc').layout.buildDirectory.file('testcases')
-            from project('test-app').layout.buildDirectory.file('testcases')
-            into layout.buildDirectory.dir('android-sts/testcases')
-        }
-
-        copy {
-            from project('test-app').layout.buildDirectory.file('outputs/apk/debug')
-            rename '(.*).apk', "${testAppApkName}"
-            include '**/*.apk'
-            into layout.buildDirectory.dir('android-sts/testcases')
-        }
-
-        copy {
-            from project('sts-test').layout.projectDirectory.file('libs')
-            into layout.buildDirectory.dir('android-sts/tools')
-        }
+        copyArtifacts('testcases_arm')
     }
 }
+
+task assembleStsx86 {
+    dependsOn ':sts-test:copyHostSideTest'
+
+    if (findProject('native-poc') != null) {
+        dependsOn ':native-poc:copyx86'
+        dependsOn ':native-poc:copyx86_64'
+    }
+
+    if (findProject('test-app') != null) {
+        dependsOn ':test-app:assemble'
+    }
+
+    // To add another Android apk to the test, copy the block above and rename
+    // the project name to your new submodule
+
+    doLast {
+        copyArtifacts('testcases_x86')
+    }
+}
+
+task zipForSubmission(type: Zip) {
+    from('.') {
+        exclude "**/build"
+        exclude '.gradle'
+        exclude 'test-app/libs'
+        exclude 'sts-test/libs'
+        exclude 'sts-test/utils'
+        exclude "**/.cxx"
+    }
+    from project('sts-test').layout.projectDirectory.file('libs/version.txt')
+    archiveFileName.set("codesubmission.zip")
+    destinationDirectory.set(layout.buildDirectory)
+}
diff --git a/libraries/sts-common-util/sts-sdk/package/dotidea/runConfigurations/assemble_STS_arm.xml b/libraries/sts-common-util/sts-sdk/package/dotidea/runConfigurations/assemble_STS_arm.xml
new file mode 100644
index 0000000..c79a3b0
--- /dev/null
+++ b/libraries/sts-common-util/sts-sdk/package/dotidea/runConfigurations/assemble_STS_arm.xml
@@ -0,0 +1,23 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="assembleStsArm" type="GradleRunConfiguration" factoryName="Gradle">
+    <ExternalSystemSettings>
+      <option name="executionName" />
+      <option name="externalProjectPath" value="$PROJECT_DIR$" />
+      <option name="externalSystemIdString" value="GRADLE" />
+      <option name="scriptParameters" value="" />
+      <option name="taskDescriptions">
+        <list />
+      </option>
+      <option name="taskNames">
+        <list>
+          <option value="assembleStsARM" />
+        </list>
+      </option>
+      <option name="vmOptions" />
+    </ExternalSystemSettings>
+    <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
+    <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
+    <DebugAllEnabled>false</DebugAllEnabled>
+    <method v="2" />
+  </configuration>
+</component>
diff --git a/libraries/sts-common-util/sts-sdk/package/dotidea/runConfigurations/assemble_STS_x86.xml b/libraries/sts-common-util/sts-sdk/package/dotidea/runConfigurations/assemble_STS_x86.xml
new file mode 100644
index 0000000..790e427
--- /dev/null
+++ b/libraries/sts-common-util/sts-sdk/package/dotidea/runConfigurations/assemble_STS_x86.xml
@@ -0,0 +1,23 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="assembleStsx86" type="GradleRunConfiguration" factoryName="Gradle">
+    <ExternalSystemSettings>
+      <option name="executionName" />
+      <option name="externalProjectPath" value="$PROJECT_DIR$" />
+      <option name="externalSystemIdString" value="GRADLE" />
+      <option name="scriptParameters" value="" />
+      <option name="taskDescriptions">
+        <list />
+      </option>
+      <option name="taskNames">
+        <list>
+          <option value="assembleStsx86" />
+        </list>
+      </option>
+      <option name="vmOptions" />
+    </ExternalSystemSettings>
+    <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
+    <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
+    <DebugAllEnabled>false</DebugAllEnabled>
+    <method v="2" />
+  </configuration>
+</component>
diff --git a/libraries/sts-common-util/sts-sdk/package/dotidea/runConfigurations/clean.xml b/libraries/sts-common-util/sts-sdk/package/dotidea/runConfigurations/clean.xml
new file mode 100644
index 0000000..9480f95
--- /dev/null
+++ b/libraries/sts-common-util/sts-sdk/package/dotidea/runConfigurations/clean.xml
@@ -0,0 +1,23 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="clean" type="GradleRunConfiguration" factoryName="Gradle">
+    <ExternalSystemSettings>
+      <option name="executionName" />
+      <option name="externalProjectPath" value="$PROJECT_DIR$" />
+      <option name="externalSystemIdString" value="GRADLE" />
+      <option name="scriptParameters" value="" />
+      <option name="taskDescriptions">
+        <list />
+      </option>
+      <option name="taskNames">
+        <list>
+          <option value="clean" />
+        </list>
+      </option>
+      <option name="vmOptions" />
+    </ExternalSystemSettings>
+    <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
+    <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
+    <DebugAllEnabled>false</DebugAllEnabled>
+    <method v="2" />
+  </configuration>
+</component>
diff --git a/libraries/sts-common-util/sts-sdk/package/dotidea/runConfigurations/create_zip.xml b/libraries/sts-common-util/sts-sdk/package/dotidea/runConfigurations/create_zip.xml
new file mode 100644
index 0000000..50d26e2
--- /dev/null
+++ b/libraries/sts-common-util/sts-sdk/package/dotidea/runConfigurations/create_zip.xml
@@ -0,0 +1,24 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="zipForSubmission" type="GradleRunConfiguration" factoryName="Gradle">
+    <ExternalSystemSettings>
+      <option name="executionName" />
+      <option name="externalProjectPath" value="$PROJECT_DIR$" />
+      <option name="externalSystemIdString" value="GRADLE" />
+      <option name="scriptParameters" value="" />
+      <option name="taskDescriptions">
+        <list />
+      </option>
+      <option name="taskNames">
+        <list>
+          <option value="zipForSubmission" />
+        </list>
+      </option>
+      <option name="vmOptions" />
+    </ExternalSystemSettings>
+    <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
+    <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
+    <DebugAllEnabled>false</DebugAllEnabled>
+    <method v="2" />
+  </configuration>
+</component>
+
diff --git a/libraries/sts-common-util/sts-sdk/package/dotidea/runConfigurations/run_STS_nonroot.xml b/libraries/sts-common-util/sts-sdk/package/dotidea/runConfigurations/run_STS_nonroot.xml
new file mode 100644
index 0000000..a8e0ca6
--- /dev/null
+++ b/libraries/sts-common-util/sts-sdk/package/dotidea/runConfigurations/run_STS_nonroot.xml
@@ -0,0 +1,12 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="run STS on device without root" type="ShConfigurationType">
+    <option name="SCRIPT_TEXT" value="./sts-tradefed run commandAndExit sts-sdk-nonroot --log-level debug --log-level-display debug -m hostsidetest" />
+    <option name="INDEPENDENT_SCRIPT_PATH" value="true" />
+    <option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
+    <option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$/build/android-sts/tools" />
+    <option name="EXECUTE_IN_TERMINAL" value="false" />
+    <option name="EXECUTE_SCRIPT_FILE" value="false" />
+    <envs />
+    <method v="2" />
+  </configuration>
+</component>
diff --git a/libraries/sts-common-util/sts-sdk/package/dotidea/runConfigurations/run_STS_root.xml b/libraries/sts-common-util/sts-sdk/package/dotidea/runConfigurations/run_STS_root.xml
new file mode 100644
index 0000000..053744b
--- /dev/null
+++ b/libraries/sts-common-util/sts-sdk/package/dotidea/runConfigurations/run_STS_root.xml
@@ -0,0 +1,12 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="run STS on device with root" type="ShConfigurationType">
+    <option name="SCRIPT_TEXT" value="./sts-tradefed run commandAndExit sts-sdk-root --log-level debug --log-level-display debug -m hostsidetest" />
+    <option name="INDEPENDENT_SCRIPT_PATH" value="true" />
+    <option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
+    <option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$/build/android-sts/tools" />
+    <option name="EXECUTE_IN_TERMINAL" value="false" />
+    <option name="EXECUTE_SCRIPT_FILE" value="false" />
+    <envs />
+    <method v="2" />
+  </configuration>
+</component>
diff --git a/libraries/sts-common-util/sts-sdk/package/native-poc/build.gradle.template b/libraries/sts-common-util/sts-sdk/package/native-poc/build.gradle.template
index f10e314..08b4333 100644
--- a/libraries/sts-common-util/sts-sdk/package/native-poc/build.gradle.template
+++ b/libraries/sts-common-util/sts-sdk/package/native-poc/build.gradle.template
@@ -16,16 +16,17 @@
     }
 }
 
-task copyArm32(type: Copy) {
-    dependsOn 'externalNativeBuildDebug'
-    from layout.buildDirectory.file('intermediates/cmake/debug/obj/armeabi-v7a/nativepoc')
-    rename ('nativepoc', "${project.name.replaceFirst(/-native/, '')}_sts32")
-    into layout.buildDirectory.dir('testcases')
+ext.copyArtifact = { arch, suffix, outputDir ->
+    tasks.register("copy${arch}", Copy) {
+        dependsOn 'externalNativeBuildDebug'
+        from layout.buildDirectory.file("intermediates/cmake/debug/obj/${arch}/nativepoc")
+        rename ('nativepoc', "${project.name.replaceFirst(/-native/, '')}${suffix}")
+        into layout.buildDirectory.dir(outputDir)
+    }
 }
 
-task copyArm64(type: Copy) {
-    dependsOn 'externalNativeBuildDebug'
-    from layout.buildDirectory.file('intermediates/cmake/debug/obj/arm64-v8a/nativepoc')
-    rename ('nativepoc', "${project.name.replaceFirst(/-native/, '')}_sts64")
-    into layout.buildDirectory.dir('testcases')
-}
+copyArtifact('armeabi-v7a', '_sts32', 'testcases_arm')
+copyArtifact('arm64-v8a', '_sts64', 'testcases_arm')
+copyArtifact('x86', '_sts32', 'testcases_x86')
+copyArtifact('x86_64', '_sts64', 'testcases_x86')
+
diff --git a/libraries/sts-common-util/sts-sdk/package/native-poc/src/native-sample.cpp b/libraries/sts-common-util/sts-sdk/package/native-poc/src/native-sample.cpp
index 621af17..1d6fd59 100644
--- a/libraries/sts-common-util/sts-sdk/package/native-poc/src/native-sample.cpp
+++ b/libraries/sts-common-util/sts-sdk/package/native-poc/src/native-sample.cpp
@@ -1,7 +1,19 @@
 #include <iostream>
-#include <stdlib.h>
+#include <fstream>
 
-int main () {
-    std::cout << "Hello Android!" << std::endl;
-    return 0;
-}
\ No newline at end of file
+#define EXIT_SUCCESS 0
+#define EXIT_FAILURE 1
+#define EXIT_VULNERABLE 113
+
+int main(int argc, char *argv[]) {
+    if (argc != 3) {
+        return EXIT_FAILURE;
+    }
+    std::ifstream f(argv[1]);
+    if (f.is_open()) {
+        std::cout << "Hello " << f.rdbuf() << "! " << argv[2] << std::endl;
+        return EXIT_SUCCESS;
+    } else {
+        return EXIT_VULNERABLE;
+    }
+}
diff --git a/libraries/sts-common-util/sts-sdk/package/settings.gradle b/libraries/sts-common-util/sts-sdk/package/settings.gradle
index b2bf784..c73df1c 100644
--- a/libraries/sts-common-util/sts-sdk/package/settings.gradle
+++ b/libraries/sts-common-util/sts-sdk/package/settings.gradle
@@ -15,6 +15,13 @@
 }
 
 rootProject.name = 'STS test'
-include ':native-poc'
-include ':test-app'
+
+if (new File('native-poc').isDirectory()) {
+    include ':native-poc'
+}
+
+if (new File('test-app').isDirectory()) {
+    include ':test-app'
+}
+
 include ':sts-test'
diff --git a/libraries/sts-common-util/sts-sdk/package/sts-test/src/main/java/android/security/sts/CVE_2020_0215.java b/libraries/sts-common-util/sts-sdk/package/sts-test/src/main/java/android/security/sts/CVE_2020_0215.java
deleted file mode 100644
index 7588fc8..0000000
--- a/libraries/sts-common-util/sts-sdk/package/sts-test/src/main/java/android/security/sts/CVE_2020_0215.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES 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.security.sts;
-
-import static com.android.sts.common.CommandUtil.runAndCheck;
-
-import android.platform.test.annotations.AsbSecurityTest;
-
-import com.android.sts.common.tradefed.testtype.StsExtraBusinessLogicHostTestBase;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
-
-import org.junit.runner.RunWith;
-import org.junit.Test;
-
-@RunWith(DeviceJUnit4ClassRunner.class)
-public class CVE_2020_0215 extends StsExtraBusinessLogicHostTestBase {
-
-    static final String TEST_APP = "CVE_2020_0215.apk";
-    static final String TEST_PKG = "android.security.sts.CVE_2020_0215";
-    static final String TEST_CLASS = TEST_PKG + "." + "DeviceTest";
-
-    /** b/140417248 */
-    @AsbSecurityTest(cveBugId = 140417248)
-    @Test
-    public void testPocCVE_2020_0215() throws Exception {
-        ITestDevice device = getDevice();
-        device.enableAdbRoot();
-        uninstallPackage(device, TEST_PKG);
-
-        runAndCheck(device, "input keyevent KEYCODE_WAKEUP");
-        runAndCheck(device, "input keyevent KEYCODE_MENU");
-        runAndCheck(device, "input keyevent KEYCODE_HOME");
-
-        installPackage(TEST_APP);
-        runDeviceTests(TEST_PKG, TEST_CLASS, "testCVE_2020_0215");
-    }
-}
diff --git a/libraries/sts-common-util/sts-sdk/package/sts-test/src/main/java/android/security/sts/StsHostSideTestCase.java b/libraries/sts-common-util/sts-sdk/package/sts-test/src/main/java/android/security/sts/StsHostSideTestCase.java
new file mode 100644
index 0000000..2e27305
--- /dev/null
+++ b/libraries/sts-common-util/sts-sdk/package/sts-test/src/main/java/android/security/sts/StsHostSideTestCase.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.security.sts;
+
+import static com.android.sts.common.CommandUtil.runAndCheck;
+
+import com.android.sts.common.NativePoc;
+import com.android.sts.common.NativePocStatusAsserter;
+import com.android.sts.common.tradefed.testtype.NonRootSecurityTestCase;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class StsHostSideTestCase extends NonRootSecurityTestCase {
+
+    static final String TEST_APP = "sts_test_app_package.apk";
+    static final String TEST_PKG = "android.security.sts.sts_test_app_package";
+    static final String TEST_CLASS = TEST_PKG + "." + "DeviceTest";
+
+    @Test
+    public void testWithApp() throws Exception {
+        // Note: this test is for CVE-2020-0215
+        ITestDevice device = getDevice();
+        device.enableAdbRoot();
+        uninstallPackage(device, TEST_PKG);
+
+        runAndCheck(device, "input keyevent KEYCODE_WAKEUP");
+        runAndCheck(device, "input keyevent KEYCODE_MENU");
+        runAndCheck(device, "input keyevent KEYCODE_HOME");
+
+        installPackage(TEST_APP);
+        runDeviceTests(TEST_PKG, TEST_CLASS, "testDeviceSideMethod");
+    }
+
+    @Test
+    public void testWithNativePoc() throws Exception {
+        NativePoc.builder()
+                .pocName("native-poc")
+                .resources("res.txt")
+                .args("res.txt", "arg2")
+                .useDefaultLdLibraryPath(true)
+                .assumePocExitSuccess(true)
+                .after(r -> getDevice().executeShellV2Command("ls -l /"))
+                .asserter(NativePocStatusAsserter.assertNotVulnerableExitCode()) // not 113
+                .build()
+                .run(this);
+    }
+}
diff --git a/libraries/sts-common-util/sts-sdk/package/sts-test/src/main/resources/res.txt b/libraries/sts-common-util/sts-sdk/package/sts-test/src/main/resources/res.txt
new file mode 100644
index 0000000..330ce8c
--- /dev/null
+++ b/libraries/sts-common-util/sts-sdk/package/sts-test/src/main/resources/res.txt
@@ -0,0 +1 @@
+Android Security
diff --git a/libraries/sts-common-util/sts-sdk/package/test-app/build.gradle b/libraries/sts-common-util/sts-sdk/package/test-app/build.gradle
index 4327486..17881cc 100644
--- a/libraries/sts-common-util/sts-sdk/package/test-app/build.gradle
+++ b/libraries/sts-common-util/sts-sdk/package/test-app/build.gradle
@@ -6,7 +6,7 @@
     compileSdk 31
 
     defaultConfig {
-        applicationId "android.security.sts.CVE_2020_0215"
+        applicationId "android.security.sts.sts_test_app_package"
         minSdk 29
         targetSdk 31
         versionCode 1
diff --git a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/AndroidManifest.xml b/libraries/sts-common-util/sts-sdk/package/test-app/src/main/AndroidManifest.xml
index 884fdf5..b7f8ac8 100644
--- a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/AndroidManifest.xml
+++ b/libraries/sts-common-util/sts-sdk/package/test-app/src/main/AndroidManifest.xml
@@ -16,12 +16,12 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.security.sts.CVE_2020_0215"
+    package="android.security.sts.sts_test_app_package"
     android:versionCode="1"
     android:versionName="1.0">
     <instrumentation
         android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="android.security.sts.CVE_2020_0215" />
+        android:targetPackage="android.security.sts.sts_test_app_package" />
     <application
         android:supportsRtl="true">
         <activity
diff --git a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/CVE_2020_0215/DeviceTest.java b/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/DeviceTest.java
similarity index 96%
rename from libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/CVE_2020_0215/DeviceTest.java
rename to libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/DeviceTest.java
index bd2e8a7..da1f7bf 100644
--- a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/CVE_2020_0215/DeviceTest.java
+++ b/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/DeviceTest.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package android.security.sts.CVE_2020_0215;
+package android.security.sts.sts_test_app_package;
 
 import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
@@ -46,7 +46,7 @@
     }
 
     @Test
-    public void testCVE_2020_0215() {
+    public void testDeviceSideMethod() {
         try {
             mAppContext = getApplicationContext();
             UiDevice device = UiDevice.getInstance(getInstrumentation());
diff --git a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/CVE_2020_0215/PocActivity.java b/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/PocActivity.java
similarity index 94%
rename from libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/CVE_2020_0215/PocActivity.java
rename to libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/PocActivity.java
index 5825043..27d682d 100644
--- a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/CVE_2020_0215/PocActivity.java
+++ b/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/PocActivity.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package android.security.sts.CVE_2020_0215;
+package android.security.sts.sts_test_app_package;
 
 import android.app.Activity;
 import android.os.Bundle;
diff --git a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/CVE_2020_0215/PocReceiver.java b/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/PocReceiver.java
similarity index 96%
rename from libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/CVE_2020_0215/PocReceiver.java
rename to libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/PocReceiver.java
index a7d3762..ac87925 100644
--- a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/CVE_2020_0215/PocReceiver.java
+++ b/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/sts_test_app_package/PocReceiver.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package android.security.sts.CVE_2020_0215;
+package android.security.sts.sts_test_app_package;
 
 import android.content.BroadcastReceiver;
 import android.content.Context;
diff --git a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/res/values/strings.xml b/libraries/sts-common-util/sts-sdk/package/test-app/src/main/res/values/strings.xml
index 85a302d..286e6fd 100644
--- a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/res/values/strings.xml
+++ b/libraries/sts-common-util/sts-sdk/package/test-app/src/main/res/values/strings.xml
@@ -17,5 +17,5 @@
 
 <resources>
     <string name="RESULT_KEY">result</string>
-    <string name="SHARED_PREFERENCE">CVE_2020_0215</string>
+    <string name="SHARED_PREFERENCE">sts_test_app_failure</string>
 </resources>
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 ad9b413..79a838a 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
@@ -16,6 +16,8 @@
 
 package android.system.helpers;
 
+import static junit.framework.Assert.assertTrue;
+
 import android.app.KeyguardManager;
 import android.content.Context;
 import android.graphics.Point;
@@ -418,7 +420,7 @@
     }
 
     public void waitLockscreenVisible() {
-        mDevice.wait(Until.hasObject(SCREEN_LOCK), MAX_SCREEN_LOCK_WAIT_TIME_MS);
+        assertTrue(mDevice.wait(Until.hasObject(SCREEN_LOCK), MAX_SCREEN_LOCK_WAIT_TIME_MS));
     }
 
     /* Returns screen coordinates for each pattern dot
diff --git a/libraries/system-helpers/sysui-helper/src/android/system/helpers/NavigationControlHelper.java b/libraries/system-helpers/sysui-helper/src/android/system/helpers/NavigationControlHelper.java
index 0abf187..1f5b0eb 100644
--- a/libraries/system-helpers/sysui-helper/src/android/system/helpers/NavigationControlHelper.java
+++ b/libraries/system-helpers/sysui-helper/src/android/system/helpers/NavigationControlHelper.java
@@ -20,7 +20,7 @@
 import androidx.test.uiautomator.By;
 import androidx.test.uiautomator.BySelector;
 import androidx.test.uiautomator.UiDevice;
-import androidx.test.uiautomator.UiObject2;
+import androidx.test.uiautomator.Until;
 
 public class NavigationControlHelper {
 
@@ -31,8 +31,8 @@
             UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
 
     public static void assertNavigationBarNotVisible() {
-        final UiObject2 navBarObject = sDevice.findObject(NAVIGATION_BAR_VIEW);
-        if (navBarObject != null) {
+        final boolean navBarInvisible = sDevice.wait(Until.gone(NAVIGATION_BAR_VIEW), 2_000);
+        if (!navBarInvisible) {
             throw new AssertionError("Navigation bar is visible, expected: invisible");
         }
     }
diff --git a/libraries/systemui-helper/Android.bp b/libraries/systemui-helper/Android.bp
new file mode 100644
index 0000000..e49f8f5
--- /dev/null
+++ b/libraries/systemui-helper/Android.bp
@@ -0,0 +1,41 @@
+//
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+    name: "systemui-helper",
+    libs: [
+        "ub-uiautomator",
+        "app-helpers-handheld-interfaces",
+        "androidx.test.runner",
+        "androidx.test.rules",
+        "system-helpers",
+        "health-testing-utils",
+        "platform-test-rules",
+        "uiautomator-helpers",
+        "tapl-common",
+    ],
+    static_libs: [
+        "truth-prebuilt",
+    ],
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
+}
diff --git a/libraries/systemui-helper/OWNERS b/libraries/systemui-helper/OWNERS
new file mode 100644
index 0000000..a2af24c
--- /dev/null
+++ b/libraries/systemui-helper/OWNERS
@@ -0,0 +1,3 @@
+include platform/frameworks/base:/packages/SystemUI/OWNERS
+nicomazz@google.com
+vadimt@google.com
diff --git a/libraries/systemui-helper/TEST_MAPPING b/libraries/systemui-helper/TEST_MAPPING
new file mode 100644
index 0000000..be870e3
--- /dev/null
+++ b/libraries/systemui-helper/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "imports": [
+    {
+      "path": "vendor/google_testing/integration/tests/scenarios/src/android/platform/test/scenario/sysui"
+    }
+  ]
+}
diff --git a/libraries/systemui-helper/src/android/platform/helpers/ColorUtils.kt b/libraries/systemui-helper/src/android/platform/helpers/ColorUtils.kt
new file mode 100644
index 0000000..9aba628
--- /dev/null
+++ b/libraries/systemui-helper/src/android/platform/helpers/ColorUtils.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.graphics.Bitmap
+import android.graphics.Color
+import androidx.test.runner.screenshot.Screenshot
+import androidx.test.uiautomator.UiDevice
+
+fun UiDevice.getScreenBorderColors(): ScreenBorderColors {
+    val screenshot: Bitmap = Screenshot.capture().bitmap
+
+    val firstColumn = screenshot.columnColor(x = 0)
+    val lastColumn = screenshot.columnColor(x = screenshot.width - 1)
+    return ScreenBorderColors(leftColumn = firstColumn, rightColumn = lastColumn)
+}
+
+/** Represents colors of the first and last screen column. */
+data class ScreenBorderColors(val leftColumn: List<Color>, val rightColumn: List<Color>) {
+    infix fun darkerThan(other: ScreenBorderColors): Boolean =
+        leftColumn darkerThan other.leftColumn && rightColumn darkerThan other.rightColumn
+}
+
+/** Returns a list of colors of the column at [x]. */
+fun Bitmap.columnColor(x: Int): List<Color> = (0 until height).map { y -> getColor(x, y) }
+
+/** Returns middle element of a list. Used for debugging purposes only. */
+fun List<Color>.middle(): Color? = getOrNull(size / 2)
+
+/** Returns whether i-th colors in this list have a luminance lower than the i-th one in [other] */
+infix fun List<Color>.darkerThan(other: List<Color>): Boolean =
+    zip(other).all { (thisColor, otherColor) -> thisColor darkerThan otherColor }
+
+/** Returns whether this color is darker than [other] based on [Color.luminance]. */
+infix fun Color.darkerThan(other: Color): Boolean = luminance() < other.luminance()
+
+/** Returns [true] if the entire device screen is completely black. */
+// TODO(b/262588714): Add tracing once this is moved to uiautomator_utils.
+//  (androidx.tracing is not available here.)
+fun UiDevice.hasBlackScreen(): Boolean = Screenshot.capture().bitmap.isBlack()
+
+private fun Bitmap.isBlack(): Boolean {
+    for (i in 0 until width) {
+        for (j in 0 until height) {
+            if (getColor(i, j).toArgb() != Color.BLACK) {
+                return false
+            }
+        }
+    }
+    return true
+}
diff --git a/libraries/systemui-helper/src/android/platform/helpers/CommonUtils.java b/libraries/systemui-helper/src/android/platform/helpers/CommonUtils.java
new file mode 100644
index 0000000..ecacf80
--- /dev/null
+++ b/libraries/systemui-helper/src/android/platform/helpers/CommonUtils.java
@@ -0,0 +1,398 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.platform.helpers.ui.UiAutomatorUtils.getInstrumentation;
+import static android.platform.helpers.ui.UiAutomatorUtils.getUiDevice;
+import static android.platform.helpers.ui.UiSearch.search;
+import static android.platform.uiautomator_helpers.DeviceHelpers.getContext;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static java.lang.String.format;
+
+import android.app.Instrumentation;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.platform.helpers.features.common.HomeLockscreenPage;
+import android.platform.helpers.ui.UiSearch2;
+import android.platform.test.util.HealthTestingUtils;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiObject;
+import android.support.test.uiautomator.UiObjectNotFoundException;
+import android.util.Log;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Helper class for writing System UI ui tests. It consists of common utils required while writing
+ * UI tests.
+ */
+public class CommonUtils {
+
+    private static final int LARGE_SCREEN_DP_THRESHOLD = 600;
+    private static final String TAG = "CommonUtils";
+    private static final int SWIPE_STEPS = 100;
+    private static final int DEFAULT_MARGIN = 5;
+    private static final String LIST_ALL_USERS_COMMAND = "cmd user list -v --all";
+
+    private CommonUtils() {
+    }
+
+    /**
+     * Prints a message to standard output during an instrumentation test.
+     *
+     * Message will be printed to terminal if test is run using {@code am instrument}. This is
+     * useful for debugging.
+     */
+    public static void println(String msg) {
+        final Bundle streamResult = new Bundle();
+        streamResult.putString(Instrumentation.REPORT_KEY_STREAMRESULT, msg + "\n");
+        InstrumentationRegistry.getInstrumentation().sendStatus(0, streamResult);
+    }
+
+    /**
+     * This method help you execute you shell command.
+     * Example: adb shell pm list packages -f
+     * Here you just need to provide executeShellCommand("pm list packages -f")
+     *
+     * @param command command need to executed.
+     * @return output in String format.
+     */
+    public static String executeShellCommand(String command) {
+        Log.d(TAG, format("Executing Shell Command: %s", command));
+        try {
+            String out = getUiDevice().executeShellCommand(command);
+            return out;
+        } catch (IOException e) {
+            Log.d(TAG, format("IOException Occurred: %s", e));
+            throw new RuntimeException(e);
+        }
+    }
+
+    /** Returns PIDs of all System UI processes */
+    private static String[] getSystemUiPids() {
+        String output = executeShellCommand("pidof com.android.systemui");
+        if (output.isEmpty()) {
+            // explicit check empty string, and return 0-length array.
+            // "".split("\\s") returns 1-length array [""], which invalidates
+            // allSysUiProcessesRestarted check.
+            return new String[0];
+        }
+        return output.split("\\s");
+    }
+
+    private static boolean allSysUiProcessesRestarted(List<String> initialPidsList) {
+        final String[] currentPids = getSystemUiPids();
+        Log.d(TAG, "restartSystemUI: Current PIDs=" + Arrays.toString(currentPids));
+        if (currentPids.length < initialPidsList.size()) {
+            return false; // No all processes restarted.
+        }
+        for (String pid : currentPids) {
+            if (initialPidsList.contains(pid)) {
+                return false; // Old process still running.
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Restart System UI by running {@code am crash com.android.systemui}.
+     *
+     * <p>This is sometimes necessary after changing flags, configs, or settings ensure that
+     * systemui is properly initialized with the new changes. This method will wait until the home
+     * screen is visible, then it will optionally dismiss the home screen via swipe.
+     *
+     * @param swipeUp whether to call {@link HomeLockscreenPage#swipeUp()} after restarting System
+     *     UI
+     * @deprecated Use {@link SysuiRestarter} instead. It has been moved out from here to use
+     *     androidx uiautomator version (this class depends on the old version, and there are many
+     *     deps that don't allow to easily switch to the new androidx one)
+     */
+    @Deprecated
+    public static void restartSystemUI(boolean swipeUp) {
+        SysuiRestarter.restartSystemUI(swipeUp);
+    }
+
+    /** Asserts that the screen is on. */
+    public static void assertScreenOn(String errorMessage) {
+        try {
+            assertWithMessage(errorMessage)
+                    .that(getUiDevice().isScreenOn())
+                    .isTrue();
+        } catch (RemoteException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * Helper method to swipe the given object.
+     *
+     * @param gestureType direction which to swipe.
+     * @param obj         object which needs to be swiped.
+     */
+    public static void swipe(GestureType gestureType, UiObject obj) {
+        Log.d(TAG, format("Swiping Object[%s] %s", obj.getSelector(), gestureType));
+        try {
+            Rect boundary = obj.getBounds();
+            final int displayHeight = getUiDevice().getDisplayHeight() - DEFAULT_MARGIN;
+            final int displayWidth = getUiDevice().getDisplayWidth() - DEFAULT_MARGIN;
+            final int objHeight = boundary.height();
+            final int objWidth = boundary.width();
+            final int marginHeight = (Math.abs(displayHeight - objHeight)) / 2;
+            final int marginWidth = (Math.abs(displayWidth - objWidth)) / 2;
+            switch (gestureType) {
+                case DOWN:
+                    getUiDevice().swipe(
+                            marginWidth + (objWidth / 2),
+                            marginHeight,
+                            marginWidth + (objWidth / 2),
+                            displayHeight,
+                            SWIPE_STEPS
+                    );
+                    break;
+                case UP:
+                    getUiDevice().swipe(
+                            marginWidth + (objWidth / 2),
+                            displayHeight,
+                            marginWidth + (objWidth / 2),
+                            marginHeight,
+                            SWIPE_STEPS
+                    );
+                    break;
+                case RIGHT:
+                    getUiDevice().swipe(
+                            marginWidth,
+                            marginHeight + (objHeight / 2),
+                            displayWidth,
+                            marginHeight + (objHeight / 2),
+                            SWIPE_STEPS
+                    );
+                    break;
+                case LEFT:
+                    getUiDevice().swipe(
+                            displayWidth,
+                            marginHeight + (objHeight / 2),
+                            marginWidth,
+                            marginHeight + (objHeight / 2),
+                            SWIPE_STEPS
+                    );
+                    break;
+            }
+        } catch (UiObjectNotFoundException e) {
+            Log.e(TAG,
+                    format("Given object was not found. Hence failed to swipe. Exception %s", e));
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * Launching an app with different ways.
+     *
+     * @param launchAppWith                  options used to launching an app.
+     * @param packageActivityOrComponentName required package or activity or component name to
+     *                                       launch the given app
+     * @param appName                        name of the app
+     */
+    public static void launchApp(LaunchAppWith launchAppWith, String packageActivityOrComponentName,
+            String appName) {
+        Log.d(TAG, String.format("Opening app %s using their %s [%s]",
+                appName, launchAppWith, packageActivityOrComponentName));
+        Intent appIntent = null;
+        switch (launchAppWith) {
+            case PACKAGE_NAME:
+                PackageManager packageManager = getContext().getPackageManager();
+                appIntent = packageManager.getLaunchIntentForPackage(
+                        packageActivityOrComponentName);
+                appIntent.addCategory(Intent.CATEGORY_LAUNCHER);
+                break;
+            case ACTIVITY:
+                appIntent = new Intent(packageActivityOrComponentName);
+                break;
+            case COMPONENT_NAME:
+                ComponentName componentName = ComponentName.unflattenFromString(
+                        packageActivityOrComponentName);
+                appIntent = new Intent();
+                appIntent.setComponent(componentName);
+                break;
+            default:
+                throw new AssertionError("Non-supported Launch App with: " + launchAppWith);
+        }
+        // Ensure the app is completely restarted so that none of the test app's state
+        // leaks between tests.
+        appIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+        getContext().startActivity(appIntent);
+    }
+
+    /**
+     * Asserts that a given page is visible.
+     *
+     * @param pageSelector      selector helped to verify the page
+     * @param pageName          name of the page to be verified
+     * @param maxTimeoutSeconds max time in seconds to verify the page
+     */
+    public static void assertPageVisible(BySelector pageSelector, String pageName,
+            int maxTimeoutSeconds) {
+        assertWithMessage(format("Page[%s] not visible; selector: %s", pageName, pageSelector))
+                .that(search(null, pageSelector, format("Page[%s]", pageName), maxTimeoutSeconds))
+                .isTrue();
+    }
+
+    /**
+     * Asserts that a given page is visible.
+     *
+     * @param pageSelector      selector helped to verify the page
+     * @param pageName          name of the page to be verified
+     * @param maxTimeoutSeconds max time in seconds to verify the page
+     */
+    public static void assertPageVisible(androidx.test.uiautomator.BySelector pageSelector,
+            String pageName, int maxTimeoutSeconds) {
+        assertThat(UiSearch2.search(null, pageSelector, format("Page[%s]", pageName),
+                maxTimeoutSeconds)).isTrue();
+    }
+
+    /**
+     * Asserts that a given page is not visible.
+     *
+     * @param pageSelector selector helped to verify the page
+     * @param pageName     name of the page to be verified
+     */
+    public static void assertPageNotVisible(BySelector pageSelector, String pageName) {
+        HealthTestingUtils.waitForCondition(
+                () -> "Page is still visible",
+                () -> !search(null, pageSelector, format("Page[%s]", pageName), 0));
+    }
+
+    /**
+     * Asserts that a given page is visible.
+     *
+     * @param pageSelector      selector helped to verify the page
+     * @param pageName          name of the page to be verified
+     * @param maxTimeoutSeconds max time in seconds to verify the page
+     */
+    public static void assertPageNotVisible(androidx.test.uiautomator.BySelector pageSelector,
+            String pageName, int maxTimeoutSeconds) {
+        assertThat(UiSearch2.search(null, pageSelector, format("Page[%s]", pageName),
+                maxTimeoutSeconds)).isFalse();
+    }
+
+    /**
+     * Execute the given shell command and get the detailed output
+     *
+     * @param shellCommand shell command to be executed
+     * @return the detailed output as an arraylist.
+     */
+    public static ArrayList<String> executeShellCommandWithDetailedOutput(String shellCommand) {
+        try {
+            ParcelFileDescriptor fileDescriptor =
+                    getInstrumentation().getUiAutomation().executeShellCommand(shellCommand);
+            byte[] buf = new byte[512];
+            int bytesRead;
+            FileInputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(
+                    fileDescriptor);
+            ArrayList<String> output = new ArrayList<>();
+            while ((bytesRead = inputStream.read(buf)) != -1) {
+                output.add(new String(buf, 0, bytesRead));
+            }
+            inputStream.close();
+            return output;
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * Returns the current user user ID. NOTE: UserID = 0 is for Owner
+     *
+     * @return a current user ID
+     */
+    public static int getCurrentUserId() {
+        Log.d(TAG, "Getting the Current User ID");
+
+        // Example terminal output of the list all users command:
+        //
+        //  $ adb shell cmd user list -v --all
+        // 2 users:
+        //
+        // 0: id=0, name=Owner, type=full.SYSTEM, flags=FULL|INITIALIZED|PRIMARY|SYSTEM (running)
+        // 1: id=10, name=Guest, type=full.GUEST, flags=FULL|GUEST|INITIALIZED (running) (current)
+        ArrayList<String> output = executeShellCommandWithDetailedOutput(LIST_ALL_USERS_COMMAND);
+        String getCurrentUser = null;
+        for (String line : output) {
+            if (line.contains("(current)")) {
+                getCurrentUser = line;
+                break;
+            }
+        }
+        Pattern userRegex = Pattern.compile("[\\d]+:.*id=([\\d]+).*\\(current\\)");
+        Matcher matcher = userRegex.matcher(getCurrentUser);
+        while (matcher.find()) {
+            return Integer.parseInt(matcher.group(1));
+        }
+
+        Log.d(TAG, "Failed to find current user ID. dumpsys activity follows:");
+        for (String line : output) {
+            Log.d(TAG, line);
+        }
+        throw new RuntimeException("Failed to find current user ID.");
+    }
+
+    public static boolean isSplitShade() {
+        int orientation = getContext().getResources().getConfiguration().orientation;
+        return isLargeScreen() && orientation == Configuration.ORIENTATION_LANDSCAPE;
+    }
+
+    public static boolean isLargeScreen() {
+        Point sizeDp = getUiDevice().getDisplaySizeDp();
+        return sizeDp.x >= LARGE_SCREEN_DP_THRESHOLD && sizeDp.y >= LARGE_SCREEN_DP_THRESHOLD;
+    }
+
+    /**
+     * Gesture for swipe
+     */
+    public enum GestureType {
+        RIGHT,
+        LEFT,
+        UP,
+        DOWN
+    }
+
+    /**
+     * Different options used for launching an app.
+     */
+    public enum LaunchAppWith {
+        PACKAGE_NAME,
+        ACTIVITY,
+        COMPONENT_NAME
+    }
+}
diff --git a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/CVE_2020_0215/PocActivity.java b/libraries/systemui-helper/src/android/platform/helpers/Constants.java
similarity index 65%
copy from libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/CVE_2020_0215/PocActivity.java
copy to libraries/systemui-helper/src/android/platform/helpers/Constants.java
index 5825043..a910820 100644
--- a/libraries/sts-common-util/sts-sdk/package/test-app/src/main/java/android/security/sts/CVE_2020_0215/PocActivity.java
+++ b/libraries/systemui-helper/src/android/platform/helpers/Constants.java
@@ -14,16 +14,11 @@
  * limitations under the License.
  */
 
-package android.security.sts.CVE_2020_0215;
+package android.platform.helpers;
 
-import android.app.Activity;
-import android.os.Bundle;
-
-public class PocActivity extends Activity {
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        setContentView(R.layout.activity_main);
-    }
+public class Constants {
+    public static String UI_PACKAGE_NAME_SYSUI = "com.android.systemui";
+    public static final int MAX_VERIFICATION_TIME_IN_SECONDS = 30;
+    public static final int MEDIUM_VERIFICATION_TIME_IN_SECONDS = 8;
+    public static final int SHORT_WAIT_TIME_IN_SECONDS = 2;
 }
diff --git a/libraries/systemui-helper/src/android/platform/helpers/LaunchAppUtils.kt b/libraries/systemui-helper/src/android/platform/helpers/LaunchAppUtils.kt
new file mode 100644
index 0000000..ce6d4ff
--- /dev/null
+++ b/libraries/systemui-helper/src/android/platform/helpers/LaunchAppUtils.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.content.Context
+import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK
+import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.Until
+import java.time.Duration
+
+/** Utilities to launch an [App]. */
+object LaunchAppUtils {
+
+    /** Launches an [App]. */
+    @JvmStatic
+    fun Context.launchApp(app: App) {
+        val appIntent =
+            packageManager.getLaunchIntentForPackage(app.packageName)?.apply {
+                flags = FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK
+            }
+                ?: error("Package ${app.packageName} not available")
+
+        startActivity(appIntent)
+        assertAppInForeground(app)
+    }
+
+    /** Asserts that a given app is in the foreground. */
+    @JvmStatic
+    fun assertAppInForeground(app: App) {
+        check(
+            device.wait(Until.hasObject(By.pkg(app.packageName).depth(0)), MAX_TIMEOUT.toMillis())
+        ) { "$app not in the foreground after ${MAX_TIMEOUT.toSeconds()} seconds" }
+    }
+
+    private val device: UiDevice
+        get() = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+}
+
+/** Describes an app that can be launched with [LaunchAppUtils]. */
+enum class App(internal val packageName: String) {
+    CALCULATOR("com.google.android.calculator")
+}
+
+private val MAX_TIMEOUT = Duration.ofSeconds(10)
diff --git a/libraries/systemui-helper/src/android/platform/helpers/LockscreenUtils.java b/libraries/systemui-helper/src/android/platform/helpers/LockscreenUtils.java
new file mode 100644
index 0000000..63d88d4
--- /dev/null
+++ b/libraries/systemui-helper/src/android/platform/helpers/LockscreenUtils.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.content.Context.KEYGUARD_SERVICE;
+import static android.os.SystemClock.sleep;
+import static android.platform.helpers.CommonUtils.executeShellCommand;
+import static android.platform.helpers.Constants.SHORT_WAIT_TIME_IN_SECONDS;
+import static android.platform.helpers.ui.UiAutomatorUtils.getUiDevice;
+import static android.platform.uiautomator_helpers.DeviceHelpers.getContext;
+import static android.view.KeyEvent.KEYCODE_ENTER;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertEquals;
+
+import static java.lang.String.format;
+import static java.lang.System.currentTimeMillis;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import android.app.KeyguardManager;
+import android.content.ContentResolver;
+import android.os.RemoteException;
+import android.platform.helpers.features.common.HomeLockscreenPage;
+import android.platform.test.util.HealthTestingUtils;
+import android.provider.Settings;
+import android.util.Log;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+/**
+ * All required util for Lockscreen.
+ * @deprecated use classes from the "systemui-tapl" library instead
+ */
+@Deprecated
+public class LockscreenUtils {
+    private static final String TAG = "LockscreenUtils";
+    private static final String RESET_LOCKSCREEN_SHELL_COMMAND = "locksettings clear --old";
+    private static final String INPUT_KEYEVENT_COMMAND = "input keyevent";
+    private static final String INPUT_TEXT_COMMAND = "input keyboard text";
+    private static final String SET_PASSWORD_COMMAND = "locksettings set-password";
+    private static final String SET_PIN_COMMAND = "locksettings set-pin";
+    private static final String SET_PATTERN_COMMAND = "locksettings set-pattern";
+    private static final String SET_SWIPE_COMMAND = "locksettings set-disabled false";
+    private static final String SET_LOCK_AS_NONE_COMMAND = "locksettings set-disabled true";
+    private static final int MAX_LOCKSCREEN_TIMEOUT_IN_SEC = 10;
+
+    public static int sPreviousAodSetting;
+
+    private LockscreenUtils() {
+    }
+
+    /**
+     * To get an instance of class that can be used to lock and unlock the keygaurd.
+     *
+     * @return an instance of class that can be used to lock and unlock the screen.
+     */
+    public static final KeyguardManager getKeyguardManager() {
+        return (KeyguardManager) getContext().getSystemService(KEYGUARD_SERVICE);
+    }
+
+    /**
+     * Different way to set the Lockscreen for Android device. Currently we only support PIN,
+     * PATTERN and PASSWORD
+     *
+     * @param lockscreenType it enum with list of supported lockscreen type
+     * @param lockscreenCode code[PIN or PATTERN or PASSWORD] which needs to be set.
+     * @param expectedResult expected result after setting the lockscreen because for lock type
+     *                       Swipe and None Keygaurd#isKeyguardSecure remain unlocked i.e. false.
+     */
+    public static void setLockscreen(LockscreenType lockscreenType, String lockscreenCode,
+            boolean expectedResult) {
+        Log.d(TAG, format("Setting Lockscreen [%s(%s)]", lockscreenType, lockscreenCode));
+        switch (lockscreenType) {
+            case PIN:
+                executeShellCommand(format("%s %s", SET_PIN_COMMAND, lockscreenCode));
+                break;
+            case PASSWORD:
+                executeShellCommand(format("%s %s", SET_PASSWORD_COMMAND, lockscreenCode));
+                break;
+            case PATTERN:
+                executeShellCommand(format("%s %s", SET_PATTERN_COMMAND, lockscreenCode));
+                break;
+            case SWIPE:
+                executeShellCommand(SET_SWIPE_COMMAND);
+                break;
+            case NONE:
+                executeShellCommand(SET_LOCK_AS_NONE_COMMAND);
+                break;
+            default:
+                throw new AssertionError("Non-supported Lockscreen Type: " + lockscreenType);
+        }
+        assertKeyguardSecure(expectedResult);
+    }
+
+    private static void assertKeyguardSecure(boolean expectedSecure) {
+        HealthTestingUtils.waitForCondition(
+                () -> String.format("Assert that keyguard %s secure, but failed.",
+                        expectedSecure ? "is" : "isn't"),
+                () -> getKeyguardManager().isKeyguardSecure() == expectedSecure);
+    }
+
+    /**
+     * Resets the give lockscreen.
+     *
+     * @param lockscreenCode old code which is currently set.
+     */
+    public static void resetLockscreen(String lockscreenCode) {
+        Log.d(TAG, String.format("Re-Setting Lockscreen %s", lockscreenCode));
+        executeShellCommand(
+                format("%s %s", RESET_LOCKSCREEN_SHELL_COMMAND, lockscreenCode));
+        assertKeyguardSecure(/* expectedSecure= */ false);
+    }
+
+    /**
+     * Entering the given code on the lockscreen
+     *
+     * @param lockscreenType type of lockscreen set.
+     * @param lockscreenCode valid lockscreen code.
+     */
+    public static void enterCodeOnLockscreen(LockscreenType lockscreenType,
+            String lockscreenCode) {
+        Log.d(TAG,
+                format("Entering Lockscreen code: %s(%s)", lockscreenType, lockscreenCode));
+        assertEquals("Lockscreen was not set", true,
+                getKeyguardManager().isKeyguardSecure());
+        switch (lockscreenType) {
+            case PIN:
+            case PASSWORD:
+                // Entering the lockscreen code in text box.
+                executeShellCommand(format("%s %s", INPUT_TEXT_COMMAND, lockscreenCode));
+                // Pressing the ENTER button after entering the code.
+                executeShellCommand(format("%s %s", INPUT_KEYEVENT_COMMAND, KEYCODE_ENTER));
+                break;
+            default:
+                throw new AssertionError("Non-supported Lockscreen Type: " + lockscreenType);
+        }
+    }
+
+    /**
+     * Check if the device is locked as per the user expectation.
+     *
+     * @param expectedLockStatus expected device lock status.
+     */
+    public static void checkDeviceLock(boolean expectedLockStatus) {
+        Log.d(TAG, format("Checking device lock status: %s", expectedLockStatus));
+        long endTime = currentTimeMillis() + SECONDS.toMillis(MAX_LOCKSCREEN_TIMEOUT_IN_SEC);
+        while (currentTimeMillis() <= endTime) {
+            if (getKeyguardManager().isDeviceLocked() == expectedLockStatus) {
+                break;
+            }
+            try {
+                Thread.sleep(100);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+        }
+        assertThat(getKeyguardManager().isDeviceLocked()).isEqualTo(expectedLockStatus);
+    }
+
+    /**
+     * Goes to the Locked screen page
+     */
+    public static void goToLockScreen() {
+        try {
+            getUiDevice().sleep();
+            sleep(SHORT_WAIT_TIME_IN_SECONDS * 1000);
+            getUiDevice().wakeUp();
+        } catch (RemoteException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /** Ensures that the lockscreen is visible. */
+    public static void ensureLockscreen() {
+        HomeLockscreenPage page = new HomeLockscreenPage();
+        HealthTestingUtils.waitForCondition(() -> "Lock screen is not visible", page::isVisible);
+    }
+
+    /**
+     * Dismisses the lock screen, by swiping up, if it's visible.
+     * The device shouldn't have a password set.
+     */
+    public static void dismissLockScreen() {
+        checkDeviceLock(false /* expectedLockStatus */);
+
+        HomeLockscreenPage page = new HomeLockscreenPage();
+        if (page.isVisible()) {
+            page.swipeUp();
+        }
+    }
+
+    public static void ensureAoD(boolean enabled) {
+        final ContentResolver contentResolver =
+                InstrumentationRegistry.getInstrumentation().getContext().getContentResolver();
+        sPreviousAodSetting = Settings.Secure.getInt(
+                contentResolver, Settings.Secure.DOZE_ALWAYS_ON, 0);
+        final boolean isAodEnabled = sPreviousAodSetting != 0;
+        if (isAodEnabled != enabled) {
+            Settings.Secure.putInt(
+                    contentResolver, Settings.Secure.DOZE_ALWAYS_ON, enabled ? 1 : 0);
+        }
+    }
+
+    public static void recoverAoD() {
+        final ContentResolver contentResolver =
+                InstrumentationRegistry.getInstrumentation().getContext().getContentResolver();
+        Settings.Secure.putInt(
+                contentResolver, Settings.Secure.DOZE_ALWAYS_ON, sPreviousAodSetting);
+    }
+
+    /**
+     * Enum for different types of Lockscreen, PIN, PATTERN and PASSWORD.
+     */
+    public enum LockscreenType {
+        PIN,
+        PASSWORD,
+        PATTERN,
+        SWIPE,
+        NONE
+    }
+}
diff --git a/libraries/systemui-helper/src/android/platform/helpers/ProcessUtil.kt b/libraries/systemui-helper/src/android/platform/helpers/ProcessUtil.kt
new file mode 100644
index 0000000..44cf821
--- /dev/null
+++ b/libraries/systemui-helper/src/android/platform/helpers/ProcessUtil.kt
@@ -0,0 +1,39 @@
+package android.platform.helpers
+
+import android.platform.uiautomator_helpers.DeviceHelpers.shell
+import android.platform.uiautomator_helpers.DeviceHelpers.uiDevice
+import android.platform.uiautomator_helpers.WaitUtils.ensureThat
+import android.util.Log
+
+/** Allows to execute operations such as restart on a process identififed by [packageName]. */
+class ProcessUtil(private val packageName: String) {
+
+    /** Restart [packageName] running `am crash <package-name>`. */
+    fun restart() {
+        val initialPids = pids
+        // make sure the lock screen is enable.
+        Log.d(TAG, "Old $packageName PIDs=$initialPids)")
+        initialPids
+            .map { pid -> "kill $pid" }
+            .forEach { killCmd ->
+                val result = uiDevice.shell(killCmd)
+                Log.d(TAG, "Result of \"$killCmd\": \"$result\"")
+            }
+        ensureThat("All sysui process restarted") { allProcessesRestarted(initialPids) }
+    }
+
+    private val pids: List<String>
+        get() {
+            val pidofResult = uiDevice.shell("pidof $packageName")
+            return if (pidofResult.isEmpty()) {
+                emptyList()
+            } else pidofResult.split("\\s".toRegex())
+        }
+
+    private fun allProcessesRestarted(initialPidsList: List<String>): Boolean =
+        (pids intersect initialPidsList).isEmpty()
+
+    private companion object {
+        const val TAG = "ProcessUtils"
+    }
+}
diff --git a/libraries/systemui-helper/src/android/platform/helpers/SysuiRestarter.kt b/libraries/systemui-helper/src/android/platform/helpers/SysuiRestarter.kt
new file mode 100644
index 0000000..557f357
--- /dev/null
+++ b/libraries/systemui-helper/src/android/platform/helpers/SysuiRestarter.kt
@@ -0,0 +1,61 @@
+package android.platform.helpers
+
+import android.platform.helpers.CommonUtils.assertScreenOn
+import android.platform.helpers.Constants.UI_PACKAGE_NAME_SYSUI
+import android.platform.helpers.LockscreenUtils.LockscreenType
+import android.platform.helpers.features.common.HomeLockscreenPage
+import android.platform.uiautomator_helpers.DeviceHelpers.assertVisibility
+import android.platform.uiautomator_helpers.DeviceHelpers.uiDevice
+import androidx.test.uiautomator.By
+import java.util.regex.Pattern
+
+/** Restarts system ui. */
+object SysuiRestarter {
+
+    private val sysuiProcessUtils = ProcessUtil(UI_PACKAGE_NAME_SYSUI)
+
+    private val PAGE_TITLE_SELECTOR_PATTERN =
+        Pattern.compile(
+            String.format(
+                "com.android.systemui:id/(%s|%s)",
+                "lockscreen_clock_view",
+                "lockscreen_clock_view_large"
+            )
+        )
+    private val PAGE_TITLE_SELECTOR = By.res(PAGE_TITLE_SELECTOR_PATTERN)
+
+    /**
+     * Restart System UI by running `am crash com.android.systemui`.
+     *
+     * This is sometimes necessary after changing flags, configs, or settings ensure that systemui
+     * is properly initialized with the new changes. This method will wait until the home screen is
+     * visible, then it will optionally dismiss the home screen via swipe.
+     *
+     * @param swipeUp whether to call [HomeLockscreenPage.swipeUp] after restarting System UI
+     */
+    @JvmStatic
+    fun restartSystemUI(swipeUp: Boolean) {
+        // This method assumes the screen is on.
+        assertScreenOn("restartSystemUI needs the screen to be on.")
+        // make sure the lock screen is enable.
+        LockscreenUtils.setLockscreen(
+            LockscreenType.SWIPE,
+            /* lockscreenCode= */ null,
+            /* expectedResult= */ false
+        )
+        sysuiProcessUtils.restart()
+        assertLockscreenVisibility(true) { "Lockscreen not visible after restart" }
+        if (swipeUp) {
+            HomeLockscreenPage().swipeUp()
+            assertLockscreenVisibility(false) { "Lockscreen still visible after swiping up." }
+        }
+    }
+
+    private fun assertLockscreenVisibility(visible: Boolean, errorMessageProvider: () -> String) {
+        uiDevice.assertVisibility(
+            PAGE_TITLE_SELECTOR,
+            visible,
+            customMessageProvider = errorMessageProvider
+        )
+    }
+}
diff --git a/libraries/systemui-helper/src/android/platform/helpers/features/Page.java b/libraries/systemui-helper/src/android/platform/helpers/features/Page.java
new file mode 100644
index 0000000..46fca0a
--- /dev/null
+++ b/libraries/systemui-helper/src/android/platform/helpers/features/Page.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.features;
+
+import android.support.test.uiautomator.BySelector;
+
+/**
+ * An interface which all the page should implement.
+ */
+public interface Page {
+
+    /**
+     * To get page selector used for determining the given page
+     *
+     * @return an instance of given page selector identifier.
+     */
+    BySelector getPageTitleSelector();
+
+    /**
+     * To get the name of the given page.
+     *
+     * @return the name of the given page
+     */
+    default String getPageName() {
+        return getClass().getSimpleName();
+    }
+
+    /**
+     * Action required to open the app or page otherwise it will remain empty.
+     */
+    default void open() {}
+}
diff --git a/libraries/systemui-helper/src/android/platform/helpers/features/Page2.java b/libraries/systemui-helper/src/android/platform/helpers/features/Page2.java
new file mode 100644
index 0000000..5082a3f
--- /dev/null
+++ b/libraries/systemui-helper/src/android/platform/helpers/features/Page2.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.features;
+
+import androidx.test.uiautomator.BySelector;
+
+/**
+ * An interface which all the page should implement. AndroidX version of {@link Page}
+ */
+public interface Page2 {
+
+    /**
+     * To get page selector used for determining the given page
+     *
+     * @return an instance of given page selector identifier.
+     */
+    BySelector getPageTitleSelector();
+
+    /**
+     * To get the name of the given page.
+     *
+     * @return the name of the given page
+     */
+    default String getPageName() {
+        return getClass().getSimpleName();
+    }
+
+    /**
+     * Action required to open the app or page otherwise it will remain empty.
+     */
+    default void open() {}
+}
diff --git a/libraries/systemui-helper/src/android/platform/helpers/features/common/HomeLockscreenPage.java b/libraries/systemui-helper/src/android/platform/helpers/features/common/HomeLockscreenPage.java
new file mode 100644
index 0000000..2d0fd88
--- /dev/null
+++ b/libraries/systemui-helper/src/android/platform/helpers/features/common/HomeLockscreenPage.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.features.common;
+
+import static android.platform.helpers.ui.UiAutomatorUtils.getUiDevice;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import android.os.RemoteException;
+import android.platform.helpers.features.Page;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.util.Log;
+
+import java.util.regex.Pattern;
+
+/**
+ * Helper class for Lock screen Home page. This contains the all the possible helper methods for the
+ * page.
+ *
+ * HSV:
+ *  - Android 11: http://go/hsv/4836673386971136
+ *  - Android 12: http://go/hsv/5398171133935616
+ *  - Android 12 (big clock): http://go/hsv/4759092784529408
+ * @deprecated use classes from the "systemui-tapl" library instead
+ */
+@Deprecated
+public class HomeLockscreenPage implements Page {
+
+    // https://hsv.googleplex.com/4836673386971136?node=68
+    public static final BySelector SWIPEABLE_AREA =
+            By.res("com.android.systemui:id/notification_panel");
+    // https://hsv.googleplex.com/5130837462876160?node=121
+    public static final Pattern PAGE_TITLE_SELECTOR_PATTERN =
+            Pattern.compile(
+                    String.format(
+                            "com.android.systemui:id/(%s|%s)",
+                            "lockscreen_clock_view", "lockscreen_clock_view_large"));
+    private static final BySelector PAGE_TITLE_SELECTOR =
+            By.res(PAGE_TITLE_SELECTOR_PATTERN);
+    private static final int SHORT_SLEEP_IN_SECONDS = 2;
+    private static final int WAIT_TIME_MILLIS = 5000;
+
+    @Override
+    public void open() {
+        try {
+            // Turning off the screen for lockscreen to enable
+            getUiDevice().sleep();
+            // Immediately waking up the device after sleep acts weird.
+            SECONDS.sleep(SHORT_SLEEP_IN_SECONDS);
+            // Waking up the device.
+            getUiDevice().wakeUp();
+        } catch (RemoteException | InterruptedException e) {
+            Log.e(getPageName(), String.format("Exception Occurred: %s", e));
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * To get page selector used for determining the given page
+     *
+     * @return an instance of given page selector identifier.
+     */
+    @Override
+    public BySelector getPageTitleSelector() {
+        return PAGE_TITLE_SELECTOR;
+    }
+
+    /** If we're currently on the lock screen. */
+    public boolean isVisible() {
+        return getUiDevice().findObject(PAGE_TITLE_SELECTOR) != null;
+    }
+
+    /**
+     * To swipe the keyguard ui element up.
+     * HSV: https://hsv.googleplex.com/4836673386971136?node=68
+     */
+    public void swipeUp() {
+        UiObject2 swipeableArea = getUiDevice().wait(Until.findObject(SWIPEABLE_AREA),
+                WAIT_TIME_MILLIS);
+        assertWithMessage("Swipeable area not found").that(swipeableArea).isNotNull();
+        //shift swipe gesture over to left so we don't begin the gesture on the lock icon
+        //   this can be removed if b/229696938 gets resolved to allow for swiping on the icon
+        swipeableArea.setGestureMargins(
+                /* left= */ 0,
+                /* top= */ 0,
+                swipeableArea.getVisibleCenter().x,
+                /* bottom= */ 0
+        );
+        swipeableArea.swipe(Direction.UP, /* percent= */ 0.7f , /* speed= */ 1000 );
+    }
+}
diff --git a/libraries/systemui-helper/src/android/platform/helpers/features/common/SettingsPage.java b/libraries/systemui-helper/src/android/platform/helpers/features/common/SettingsPage.java
new file mode 100644
index 0000000..704e23f
--- /dev/null
+++ b/libraries/systemui-helper/src/android/platform/helpers/features/common/SettingsPage.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.features.common;
+
+import static android.platform.helpers.CommonUtils.launchApp;
+import static android.provider.Settings.ACTION_SETTINGS;
+
+import android.platform.helpers.CommonUtils;
+import android.platform.helpers.features.Page;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+
+/**
+ * Helper class for Settings Home page. This contains the all the possible helper methods for the
+ * page.
+ * HSV: https://hsv.googleplex.com/5014201858785280
+ */
+public class SettingsPage implements Page {
+    private static final String SETTINGS_PAGE_IDENTIFIER = "com.android.settings:id/settings_homepage_container";
+    private static final String SETTINGS_PAGE_NAME = "Settings";
+
+    /**
+     * To get page selector used for determining the given page
+     *
+     * @return an instance of given page selector identifier.
+     */
+    @Override
+    public BySelector getPageTitleSelector() {
+        return By.res(SETTINGS_PAGE_IDENTIFIER);
+    }
+
+    /**
+     * To get the name of the given page.
+     *
+     * @return the name of the given page
+     */
+    @Override
+    public String getPageName() {
+        return SETTINGS_PAGE_NAME;
+    }
+
+    /**
+     * Action required to open the app or page otherwise it will remain empty.
+     */
+    @Override
+    public void open() {
+        launchApp(CommonUtils.LaunchAppWith.ACTIVITY, ACTION_SETTINGS, SETTINGS_PAGE_NAME);
+    }
+}
diff --git a/libraries/systemui-helper/src/android/platform/helpers/features/common/SystemPowerMenu.java b/libraries/systemui-helper/src/android/platform/helpers/features/common/SystemPowerMenu.java
new file mode 100644
index 0000000..8e65582
--- /dev/null
+++ b/libraries/systemui-helper/src/android/platform/helpers/features/common/SystemPowerMenu.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.features.common;
+
+import static android.view.KeyEvent.KEYCODE_POWER;
+
+import static android.platform.helpers.CommonUtils.executeShellCommand;
+
+import static java.util.regex.Pattern.CASE_INSENSITIVE;
+import static java.util.regex.Pattern.compile;
+
+
+import android.platform.helpers.features.Page2;
+
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.BySelector;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.Until;
+
+/**
+ * Helper class for System power menu. This contains the all the possible helper methods for
+ * menu.
+ * HSV: https://hsv.googleplex.com/6244730548518912 for power menu button in
+ * new quick settings footer design.
+ */
+public class SystemPowerMenu implements Page2 {
+    private static final String POWER_COMMAND = String.format("input keyevent --longpress  %s",
+            KEYCODE_POWER);
+
+    // https://hsv.googleplex.com/6244730548518912?node=13
+    private BySelector mPageTitleSelector = By.text(compile("Power off", CASE_INSENSITIVE));
+
+    private static final long OPEN_MENU_TIMEOUT_MSEC = 5000;
+
+    private UiDevice mUiDevice;
+
+    public SystemPowerMenu(UiDevice uiDevice) {
+        mUiDevice = uiDevice;
+    }
+
+    /**
+     * To get page selector used for determining the given page.
+     * https://hsv.googleplex.com/6504676126097408?node=13
+     *
+     * @return an instance of given page selector identifier.
+     */
+    @Override
+    public BySelector getPageTitleSelector() {
+        return mPageTitleSelector;
+    }
+
+    /**
+     * Action required to open the app or page otherwise it will remain empty.
+     */
+    @Override
+    public void open() {
+        executeShellCommand(POWER_COMMAND);
+        mUiDevice.waitForIdle();
+
+        if (!mUiDevice.wait(Until.hasObject(mPageTitleSelector), OPEN_MENU_TIMEOUT_MSEC)) {
+            throw new AssertionError("Timed out trying to open system power menu");
+        }
+    }
+}
diff --git a/libraries/systemui-helper/src/android/platform/helpers/features/quicksettings/EditCurrentUserDialog.java b/libraries/systemui-helper/src/android/platform/helpers/features/quicksettings/EditCurrentUserDialog.java
new file mode 100644
index 0000000..a2570f0
--- /dev/null
+++ b/libraries/systemui-helper/src/android/platform/helpers/features/quicksettings/EditCurrentUserDialog.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.features.quicksettings;
+
+import static android.platform.helpers.CommonUtils.assertPageVisible;
+import static android.platform.helpers.Constants.MAX_VERIFICATION_TIME_IN_SECONDS;
+import static android.platform.helpers.Constants.SHORT_WAIT_TIME_IN_SECONDS;
+import static android.platform.helpers.ui.UiAutomatorUtils.getUiDevice;
+import static android.platform.helpers.ui.UiSearch.search;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.helpers.features.Page;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+
+import java.util.regex.Pattern;
+
+/**
+ * Helper class required for setting the current user's name and user icon
+ * https://hsv.googleplex.com/4709486012923904
+ */
+public final class EditCurrentUserDialog implements Page {
+
+    // https://hsv.googleplex.com/4709486012923904?node=7
+    private static final BySelector TITLE_SELECTOR = By.res("android:id/alertTitle")
+            .text(Pattern.compile("Add user", Pattern.CASE_INSENSITIVE));
+    // https://hsv.googleplex.com/4709486012923904?node=12
+    private static final BySelector EDIT_TEXT_SELECTOR =
+            By.res("com.android.systemui:id/user_name");
+    // https://hsv.googleplex.com/4709486012923904?node=16
+    private static final BySelector OK_SELECTOR = By.res("android:id/button1")
+            .text(Pattern.compile("OK", Pattern.CASE_INSENSITIVE));
+    // https://hsv.googleplex.com/4709486012923904?node=15
+    private static final BySelector CANCEL_SELECTOR = By.res("android:id/button2")
+            .text(Pattern.compile("Cancel", Pattern.CASE_INSENSITIVE));
+
+    @Override
+    public BySelector getPageTitleSelector() {
+        return TITLE_SELECTOR;
+    }
+
+    /**
+     * Change the user's name using the text field.
+     * https://hsv.googleplex.com/4709486012923904?node=12
+     */
+    public void setUserNameText(String userName) {
+        assertPageVisible(getPageTitleSelector(), getPageName(), MAX_VERIFICATION_TIME_IN_SECONDS);
+        assertThat(search(null, EDIT_TEXT_SELECTOR, "EditText[CurrentUserName]",
+                MAX_VERIFICATION_TIME_IN_SECONDS)).isTrue();
+        UiObject2 inputTextField = getUiDevice().findObject(EDIT_TEXT_SELECTOR);
+        inputTextField.clickAndWait(Until.newWindow(), SHORT_WAIT_TIME_IN_SECONDS * 1000);
+        inputTextField.setText(userName);
+    }
+
+    /**
+     * Click the OK button. https://hsv.googleplex.com/4709486012923904?node=16
+     */
+    public void clickOK() {
+        assertPageVisible(getPageTitleSelector(), getPageName(), MAX_VERIFICATION_TIME_IN_SECONDS);
+        assertThat(
+                search(null, OK_SELECTOR, "Button[OK]", MAX_VERIFICATION_TIME_IN_SECONDS)).isTrue();
+        getUiDevice().findObject(OK_SELECTOR).click();
+    }
+
+    /**
+     * Click the cancel button. https://hsv.googleplex.com/4709486012923904?node=15
+     */
+    public void clickCancel() {
+        assertPageVisible(getPageTitleSelector(), getPageName(), MAX_VERIFICATION_TIME_IN_SECONDS);
+        assertThat(search(null, CANCEL_SELECTOR, "Button[Cancel]",
+                MAX_VERIFICATION_TIME_IN_SECONDS)).isTrue();
+        getUiDevice().findObject(CANCEL_SELECTOR).click();
+    }
+}
diff --git a/libraries/systemui-helper/src/android/platform/helpers/features/quicksettings/GuestUserExitConfirmationDialog.java b/libraries/systemui-helper/src/android/platform/helpers/features/quicksettings/GuestUserExitConfirmationDialog.java
new file mode 100644
index 0000000..f20f5d7
--- /dev/null
+++ b/libraries/systemui-helper/src/android/platform/helpers/features/quicksettings/GuestUserExitConfirmationDialog.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.features.quicksettings;
+
+import static android.platform.helpers.CommonUtils.assertPageVisible;
+import static android.platform.helpers.Constants.MAX_VERIFICATION_TIME_IN_SECONDS;
+import static android.platform.helpers.ui.UiAutomatorUtils.getUiDevice;
+
+import android.platform.helpers.features.Page;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+
+import java.util.regex.Pattern;
+
+/**
+ * Helper class for reset confirmation dialog when either switching from an ephemeral guest user,
+ * or pressing "Exit guest" when device is configured with config_guestUserAutoCreated.
+ *
+ * https://hsv.googleplex.com/5025959268843520
+ * @deprecated use classes from the "systemui-tapl" library instead
+ */
+@Deprecated
+public final class GuestUserExitConfirmationDialog implements Page {
+
+    // https://hsv.googleplex.com/6067368607350784?node=10
+    private static final BySelector sTitleSelector = By.res("android:id/alertTitle")
+            .text(Pattern.compile("Exit guest mode\\?", Pattern.CASE_INSENSITIVE));
+    // https://hsv.googleplex.com/6067368607350784?node=19
+    private static final BySelector sExitSelector = By.res("android:id/button1")
+            .text(Pattern.compile("Exit", Pattern.CASE_INSENSITIVE));
+    // https://hsv.googleplex.com/6067368607350784?node=18
+    private static final BySelector sCancelSelector = By.res("android:id/button3")
+            .text(Pattern.compile("Cancel", Pattern.CASE_INSENSITIVE));
+
+    @Override
+    public BySelector getPageTitleSelector() {
+        return sTitleSelector;
+    }
+
+    /**
+     * Click the exit button. https://hsv.googleplex.com/5025959268843520?node=16
+     */
+    public void confirmExitGuest() {
+        assertPageVisible(getPageTitleSelector(), getPageName(), MAX_VERIFICATION_TIME_IN_SECONDS);
+        getUiDevice().findObject(sExitSelector).click();
+    }
+
+    /**
+     * Click the cancel button. https://hsv.googleplex.com/5025959268843520?node=15
+     */
+    public void cancelExitGuest() {
+        assertPageVisible(getPageTitleSelector(), getPageName(), MAX_VERIFICATION_TIME_IN_SECONDS);
+        getUiDevice().findObject(sCancelSelector).click();
+    }
+}
diff --git a/libraries/systemui-helper/src/android/platform/helpers/features/quicksettings/NewUserSetupPage.java b/libraries/systemui-helper/src/android/platform/helpers/features/quicksettings/NewUserSetupPage.java
new file mode 100644
index 0000000..45b42b4
--- /dev/null
+++ b/libraries/systemui-helper/src/android/platform/helpers/features/quicksettings/NewUserSetupPage.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.features.quicksettings;
+
+import static android.platform.helpers.CommonUtils.assertPageVisible;
+import static android.platform.helpers.Constants.MAX_VERIFICATION_TIME_IN_SECONDS;
+import static android.platform.helpers.ui.UiAutomatorUtils.getUiDevice;
+import static android.platform.helpers.ui.UiSearch.search;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.helpers.features.Page;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+
+import java.util.regex.Pattern;
+
+/**
+ * Helper class for setting up a new secondary (non-guest) user
+ * https://hsv.googleplex.com/5357563224784896
+ */
+public final class NewUserSetupPage implements Page {
+
+    // https://hsv.googleplex.com/5357563224784896?node=14
+    private static final BySelector TITLE_SELECTOR =
+            By.res("com.google.android.setupwizard:id/suc_layout_title")
+                    .text(Pattern.compile("Set up new user", Pattern.CASE_INSENSITIVE));
+    // https://hsv.googleplex.com/5357563224784896?node=33
+    private static final BySelector CONTINUE_SELECTOR =
+            By.text(Pattern.compile("Continue", Pattern.CASE_INSENSITIVE));
+    // https://hsv.googleplex.com/5357563224784896?node=31
+    private static final BySelector CANCEL_SELECTOR =
+            By.text(Pattern.compile("Cancel", Pattern.CASE_INSENSITIVE));
+
+    @Override
+    public BySelector getPageTitleSelector() {
+        return TITLE_SELECTOR;
+    }
+
+    /**
+     * Click the continue button. https://hsv.googleplex.com/5357563224784896?node=33
+     */
+    public void clickContinue() {
+        assertPageVisible(getPageTitleSelector(), getPageName(), MAX_VERIFICATION_TIME_IN_SECONDS);
+        assertThat(search(null, CONTINUE_SELECTOR, "Button[Continue]",
+                MAX_VERIFICATION_TIME_IN_SECONDS)).isTrue();
+        getUiDevice().findObject(CONTINUE_SELECTOR).click();
+    }
+
+    /**
+     * Clicks the cancel button. https://hsv.googleplex.com/5357563224784896?node=31
+     */
+    public void clickCancel() {
+        assertPageVisible(getPageTitleSelector(), getPageName(), MAX_VERIFICATION_TIME_IN_SECONDS);
+        assertThat(search(null, CANCEL_SELECTOR, "Button[Cancel]",
+                MAX_VERIFICATION_TIME_IN_SECONDS)).isTrue();
+        getUiDevice().findObject(CANCEL_SELECTOR).click();
+    }
+}
diff --git a/libraries/systemui-helper/src/android/platform/helpers/features/quicksettings/RemoveUserDialog.java b/libraries/systemui-helper/src/android/platform/helpers/features/quicksettings/RemoveUserDialog.java
new file mode 100644
index 0000000..6880541
--- /dev/null
+++ b/libraries/systemui-helper/src/android/platform/helpers/features/quicksettings/RemoveUserDialog.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.features.quicksettings;
+
+import static android.platform.helpers.CommonUtils.assertPageVisible;
+import static android.platform.helpers.Constants.MAX_VERIFICATION_TIME_IN_SECONDS;
+import static android.platform.helpers.ui.UiAutomatorUtils.getUiDevice;
+import static android.platform.helpers.ui.UiSearch.search;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.helpers.features.Page;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+
+import java.util.regex.Pattern;
+
+/**
+ * Helper class for removing a user
+ * https://hsv.googleplex.com/4700004604182528
+ */
+public final class RemoveUserDialog implements Page {
+
+    // https://hsv.googleplex.com/4700004604182528?node=14
+    private static final BySelector TITLE_SELECTOR = By.res("android:id/message")
+            .text(Pattern.compile("Remove user\\?", Pattern.CASE_INSENSITIVE));
+    // https://hsv.googleplex.com/4700004604182528?node=23
+    private static final BySelector REMOVE_USER_SELECTOR = By.res("android:id/button1")
+            .text(Pattern.compile("Remove user", Pattern.CASE_INSENSITIVE));
+    // https://hsv.googleplex.com/4700004604182528?node=22
+    private static final BySelector KEEP_USER_SELECTOR = By.res("android:id/button2")
+            .text(Pattern.compile("Keep user", Pattern.CASE_INSENSITIVE));
+
+    @Override
+    public BySelector getPageTitleSelector() {
+        return TITLE_SELECTOR;
+    }
+
+    /**
+     * Click remove user button. https://hsv.googleplex.com/4700004604182528?node=23
+     */
+    public void removeUser() {
+        assertPageVisible(getPageTitleSelector(), getPageName(), MAX_VERIFICATION_TIME_IN_SECONDS);
+        assertThat(search(null, REMOVE_USER_SELECTOR, "Button[RemoveUser]",
+                MAX_VERIFICATION_TIME_IN_SECONDS)).isTrue();
+        getUiDevice().findObject(REMOVE_USER_SELECTOR).click();
+    }
+
+    /**
+     * Click keep user button. https://hsv.googleplex.com/4700004604182528?node=22
+     */
+    public void keepUser() {
+        assertPageVisible(getPageTitleSelector(), getPageName(), MAX_VERIFICATION_TIME_IN_SECONDS);
+        assertThat(search(null, KEEP_USER_SELECTOR, "Button[KeepUser]",
+                MAX_VERIFICATION_TIME_IN_SECONDS)).isTrue();
+        getUiDevice().findObject(KEEP_USER_SELECTOR).click();
+    }
+}
diff --git a/libraries/systemui-helper/src/android/platform/helpers/features/quicksettings/UserSelection.java b/libraries/systemui-helper/src/android/platform/helpers/features/quicksettings/UserSelection.java
new file mode 100644
index 0000000..02d65e3
--- /dev/null
+++ b/libraries/systemui-helper/src/android/platform/helpers/features/quicksettings/UserSelection.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.features.quicksettings;
+
+import static android.platform.helpers.CommonUtils.assertPageVisible;
+import static android.platform.helpers.Constants.MAX_VERIFICATION_TIME_IN_SECONDS;
+import static android.platform.helpers.ui.UiAutomatorUtils.getInstrumentation;
+import static android.platform.helpers.ui.UiAutomatorUtils.getUiDevice;
+import static android.platform.helpers.ui.UiSearch.search;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static java.lang.String.format;
+
+import android.app.Instrumentation;
+import android.graphics.Rect;
+import android.platform.helpers.features.Page;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiObject2;
+import android.system.helpers.QuickSettingsHelper;
+
+import androidx.test.uiautomator.UiDevice;
+
+import java.util.regex.Pattern;
+
+/**
+ * Helper class to select user for switching the user. This class will contain all the required
+ * methods to switch the user.
+ *
+ * http://go/hsv/5905004487507968
+ * @deprecated use classes from the "systemui-tapl" library instead
+ */
+@Deprecated
+public class UserSelection implements Page {
+
+    // http://go/hsv/5905004487507968?node=9
+    private static final BySelector MULTI_USER_TITLE_SELECTOR =
+            By.res("android:id/alertTitle")
+                    .text(Pattern.compile("Select user", Pattern.CASE_INSENSITIVE));
+    // http://go/hsv/5905004487507968?node=17
+    private static final String USER_NAME_RES_ID = "com.android.systemui:id/user_name";
+    // http://go/hsv/5905004487507968?node=28
+    private static final BySelector SETTINGS_SELECTOR = By.res("android:id/button3")
+            .text(Pattern.compile("User settings", Pattern.CASE_INSENSITIVE));
+    // http://go/hsv/5905004487507968?node=29
+    private static final BySelector CLOSE_SELECTOR = By.res("android:id/button1")
+            .text(Pattern.compile("Close|Done", Pattern.CASE_INSENSITIVE));
+    // http://go/hsv/6460527419064320?node=14
+    private static final BySelector BRIGHTNESS_SLIDER = By.res(
+            "com.android.systemui:id/brightness_slider");
+    // http://go/hsv/5465641194618880?node=84
+    private static final BySelector MULTI_USER_SWITCH_SELECTOR = By.res(
+            "com.android.systemui:id/multi_user_switch");
+
+    /**
+     * To get page selector used for determining the given page
+     *
+     * @return an instance of given page selector identifier.
+     */
+    @Override
+    public BySelector getPageTitleSelector() {
+        return MULTI_USER_TITLE_SELECTOR;
+    }
+
+    /**
+     * Action required to open the app or page otherwise it will remain empty.
+     */
+    @Override
+    public void open() {
+        final Instrumentation inst = getInstrumentation();
+        final QuickSettingsHelper qsHelper = new QuickSettingsHelper(
+                UiDevice.getInstance(inst), inst);
+        qsHelper.launchQuickSetting();
+        assertPageVisible(MULTI_USER_SWITCH_SELECTOR, getPageName(),
+                MAX_VERIFICATION_TIME_IN_SECONDS);
+        clickMultiUserSwitch();
+    }
+
+    /**
+     * Click on the given user item. http://go/hsv/5008296081620992?node=10#
+     *
+     * @param userName name of the user to be selected.
+     */
+    public void selectUserItem(String userName) {
+        BySelector userSelector = By.res(USER_NAME_RES_ID).text(
+                Pattern.compile(userName, Pattern.CASE_INSENSITIVE));
+        assertThat(search(null, userSelector, format("User[%s]", userName),
+                MAX_VERIFICATION_TIME_IN_SECONDS)).isTrue();
+        getUiDevice().findObject(userSelector).click();
+    }
+
+    /**
+     * Click the user settings button. http://go/hsv/5008296081620992?node=19
+     */
+    public void openUserSettings() {
+        assertThat(search(null, SETTINGS_SELECTOR,
+                "Button[UserSettings]",
+                MAX_VERIFICATION_TIME_IN_SECONDS)).isTrue();
+        // Clicking the "center" of the footer button does not work. Instead, as a workaround,
+        // click the top-left corner.
+        clickTopLeftCorner(getUiDevice().findObject(SETTINGS_SELECTOR));
+    }
+
+    /**
+     * Click the close button. http://go/hsv/5008296081620992?node=20
+     */
+    public void close() {
+        assertThat(search(null, CLOSE_SELECTOR, "Button[Close]",
+                MAX_VERIFICATION_TIME_IN_SECONDS)).isTrue();
+        // Clicking the "center" of the footer button does not work. Instead, as a workaround,
+        // click the top-left corner.
+        clickTopLeftCorner(getUiDevice().findObject(CLOSE_SELECTOR));
+    }
+
+    /** This is needed as new version of UiAutomator don't support click(Point). */
+    private void clickTopLeftCorner(UiObject2 obj) {
+        Rect r = obj.getVisibleBounds();
+        getUiDevice().click(r.left, r.top);
+    }
+
+    /**
+     * Click on the multi user switch icon. http://go/hsv/5465641194618880?node=84
+     */
+    public void clickMultiUserSwitch() {
+        assertThat(search(null, MULTI_USER_SWITCH_SELECTOR,
+                format("Multi User Switch: %s", MULTI_USER_SWITCH_SELECTOR),
+                MAX_VERIFICATION_TIME_IN_SECONDS)).isTrue();
+        getUiDevice().findObject(MULTI_USER_SWITCH_SELECTOR).click();
+    }
+}
diff --git a/libraries/systemui-helper/src/android/platform/helpers/foldable/FoldableDeviceController.kt b/libraries/systemui-helper/src/android/platform/helpers/foldable/FoldableDeviceController.kt
new file mode 100644
index 0000000..dce2d89
--- /dev/null
+++ b/libraries/systemui-helper/src/android/platform/helpers/foldable/FoldableDeviceController.kt
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.foldable
+
+import android.hardware.Sensor
+import android.hardware.devicestate.DeviceStateManager
+import android.hardware.devicestate.DeviceStateRequest
+import android.platform.test.rule.isLargeScreen
+import android.platform.uiautomator_helpers.DeviceHelpers.isScreenOnSettled
+import android.platform.uiautomator_helpers.DeviceHelpers.printInstrumentationStatus
+import android.platform.uiautomator_helpers.DeviceHelpers.uiDevice
+import android.platform.uiautomator_helpers.WaitUtils.ensureThat
+import android.util.Log
+import androidx.annotation.FloatRange
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.internal.R
+import java.time.Duration
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import kotlin.properties.Delegates.notNull
+import org.junit.Assume.assumeTrue
+
+/** Helper to set the folded state to a device. */
+internal class FoldableDeviceController {
+
+    private val context = InstrumentationRegistry.getInstrumentation().context
+
+    private val resources = context.resources
+    private val deviceStateManager = context.getSystemService(DeviceStateManager::class.java)!!
+    private val hingeAngleSensor = SensorInjectionController(Sensor.TYPE_HINGE_ANGLE)
+
+    private var foldedState by notNull<Int>()
+    private var unfoldedState by notNull<Int>()
+    // [currentState] is meant to be not null only when there is an override active.
+    private var currentState: Int? = null
+
+    private var deviceStateLatch = CountDownLatch(1)
+    private var pendingRequest: DeviceStateRequest? = null
+
+    /** Sets device state to folded. */
+    fun fold() {
+        printInstrumentationStatus(TAG, "Folding")
+        setDeviceState(foldedState)
+    }
+
+    /** Sets device state to an unfolded state. */
+    fun unfold() {
+        printInstrumentationStatus(TAG, "Unfolding")
+        setDeviceState(unfoldedState)
+    }
+
+    /** Removes the override on the device state. */
+    private fun resetDeviceState() {
+        printInstrumentationStatus(TAG, "resetDeviceState")
+        deviceStateManager.cancelBaseStateOverride()
+        // This might cause the screen to turn off if the default state is folded.
+        if (!uiDevice.isScreenOnSettled) {
+            uiDevice.wakeUp()
+            ensureThat("screen is on after cancelling base state override.") { uiDevice.isScreenOn }
+        }
+    }
+
+    /** Fetches folded and unfolded state identifier from the device. */
+    fun init() {
+        findFoldedUnfoldedStates()
+        currentState = if (isLargeScreen()) unfoldedState else foldedState
+        Log.d(TAG, "Initial state. Folded=$isFolded")
+        hingeAngleSensor.init()
+    }
+
+    fun uninit() {
+        resetDeviceState()
+        hingeAngleSensor.uninit()
+    }
+
+    val isFolded: Boolean
+        get() {
+            check(currentState != null) {
+                "Trying to get the current state while there is no state override set."
+            }
+            return currentState == foldedState
+        }
+
+    fun setHingeAngle(@FloatRange(from = 0.0, to = 180.0) angle: Float) {
+        hingeAngleSensor.setValue(angle)
+    }
+
+    private fun findFoldedUnfoldedStates() {
+        val foldedStates = resources.getIntArray(R.array.config_foldedDeviceStates)
+        assumeTrue("Skipping on non-foldable devices", foldedStates.isNotEmpty())
+        foldedState = foldedStates[0]
+        unfoldedState =
+            deviceStateManager.supportedStates.firstOrNull { it != foldedState }
+                ?: throw IllegalStateException("Unfolded state not found.")
+    }
+
+    private fun setDeviceState(state: Int) {
+        if (currentState == state) {
+            Log.e(TAG, "setting device state to the same state already set.")
+            return
+        }
+        deviceStateLatch = CountDownLatch(1)
+        val request = DeviceStateRequest.newBuilder(state).build()
+        pendingRequest = request
+        Log.d(TAG, "Requesting base state override to ${state.desc()}")
+        deviceStateManager.requestBaseStateOverride(
+            request,
+            context.mainExecutor,
+            deviceStateRequestCallback
+        )
+        deviceStateLatch.await { "Device state didn't change within the timeout" }
+        ensureStateSet(state)
+        Log.d(TAG, "Device state set to ${state.desc()}")
+    }
+
+    private fun ensureStateSet(state: Int) {
+        when (state) {
+            foldedState -> ensureThat("Device folded") { !isLargeScreen() }
+            unfoldedState -> ensureThat("Device unfolded") { isLargeScreen() }
+            else -> TODO("Implement a way to know if states other than un/folded are set.")
+        }
+    }
+
+    private fun Int.desc() =
+        when (this) {
+            foldedState -> "Folded"
+            unfoldedState -> "Unfolded"
+            else -> "unknown"
+        }
+
+    private fun CountDownLatch.await(error: () -> String) {
+        check(this.await(DEVICE_STATE_MAX_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS), error)
+    }
+
+    private val deviceStateRequestCallback =
+        object : DeviceStateRequest.Callback {
+            override fun onRequestActivated(request: DeviceStateRequest) {
+                Log.d(TAG, "Request activated: ${request.state.desc()}")
+                if (request == pendingRequest) {
+                    deviceStateLatch.countDown()
+                }
+                currentState = request.state
+            }
+
+            override fun onRequestCanceled(request: DeviceStateRequest) {
+                Log.d(TAG, "Request cancelled: ${request.state.desc()}")
+                if (currentState == request.state) {
+                    currentState = null
+                }
+            }
+
+            override fun onRequestSuspended(request: DeviceStateRequest) {
+                Log.d(TAG, "Request suspended: ${request.state.desc()}")
+                if (currentState == request.state) {
+                    currentState = null
+                }
+            }
+        }
+
+    private companion object {
+        const val TAG = "FoldableController"
+        val DEVICE_STATE_MAX_TIMEOUT = Duration.ofSeconds(10)
+    }
+}
diff --git a/libraries/systemui-helper/src/android/platform/helpers/foldable/FoldableRule.kt b/libraries/systemui-helper/src/android/platform/helpers/foldable/FoldableRule.kt
new file mode 100644
index 0000000..c0161b1
--- /dev/null
+++ b/libraries/systemui-helper/src/android/platform/helpers/foldable/FoldableRule.kt
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.foldable
+
+import android.os.SystemClock
+import android.platform.test.rule.TestWatcher
+import android.platform.uiautomator_helpers.WaitUtils.ensureThat
+import androidx.annotation.FloatRange
+import java.util.concurrent.TimeUnit
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+
+/**
+ * Provides an interface to use foldable specific features.
+ *
+ * Should be used as [ClassRule]. To start a test folded or unfolded, use [foldBeforeTestRule] and
+ * [unfoldBeforeTestRule].
+ *
+ * Example:
+ *
+ * ```
+ *  companion object {
+ *      @get:ClassRule val foldable = FoldableRule()
+ *  }
+ *  @get:Rule val foldRule = foldable.foldBeforeTestRule
+ * ```
+ */
+class FoldableRule(private val ensureScreenOn: Boolean = false) : TestWatcher() {
+
+    private val controller = FoldableDeviceController()
+    private var initialized = false
+
+    override fun starting(description: Description?) {
+        controller.init()
+        initialized = true
+    }
+
+    override fun finished(description: Description?) {
+        if (initialized) {
+            controller.uninit()
+            initialized = false
+        }
+    }
+
+    fun fold() {
+        check(!controller.isFolded) { "Trying to fold when already folded" }
+        if (ensureScreenOn) {
+            ensureThat("screen is on before folding") { screenOn }
+        }
+        val initialScreenSurface = displaySurface
+
+        controller.fold()
+        SystemClock.sleep(ANIMATION_TIMEOUT) // Let's wait for the unfold animation to finish.
+
+        ensureThat("screen is off after folding") { !screenOn }
+        ensureThat("screen surface decreases after folding") {
+            displaySurface < initialScreenSurface
+        }
+    }
+
+    fun unfold() {
+        check(controller.isFolded) { "Trying to unfold when already unfolded" }
+        if (ensureScreenOn) {
+            ensureThat("screen is on before unfolding") { screenOn }
+        }
+        val initialScreenSurface = displaySurface
+
+        controller.unfold()
+        SystemClock.sleep(ANIMATION_TIMEOUT) // Let's wait for the unfold animation to finish.
+
+        ensureThat("screen is on after unfolding") { screenOn }
+        ensureThat("screen surface increases after unfolding") {
+            displaySurface > initialScreenSurface
+        }
+    }
+
+    fun setHingeAngle(@FloatRange(from = 0.0, to = 180.0) angle: Float) {
+        controller.setHingeAngle(angle)
+    }
+
+    val foldBeforeTestRule: TestRule = FoldControlRule(FoldableState.FOLDED)
+    val unfoldBeforeTestRule: TestRule = FoldControlRule(FoldableState.UNFOLDED)
+
+    private inner class FoldControlRule(private val startState: FoldableState) : TestWatcher() {
+        override fun starting(description: Description?) {
+            check(initialized) { "Initialize of FoldableRule needed before this." }
+            if (currentState == startState) {
+                return
+            }
+            when (startState) {
+                FoldableState.FOLDED -> fold()
+                FoldableState.UNFOLDED -> unfold()
+                FoldableState.HALF_FOLDED -> TODO("Not yet supported.")
+            }
+        }
+    }
+
+    private val currentState: FoldableState
+        get() =
+            when (controller.isFolded) {
+                true -> FoldableState.FOLDED
+                false -> FoldableState.UNFOLDED
+            }
+
+    private val screenOn: Boolean
+        get() = uiDevice.isScreenOn
+
+    private val displaySurface: Int
+        get() = uiDevice.displayWidth * uiDevice.displayHeight
+}
+
+private val ANIMATION_TIMEOUT = TimeUnit.SECONDS.toMillis(3)
+
+private enum class FoldableState {
+    FOLDED,
+    UNFOLDED,
+    HALF_FOLDED
+}
diff --git a/libraries/systemui-helper/src/android/platform/helpers/foldable/SensorInjectionController.kt b/libraries/systemui-helper/src/android/platform/helpers/foldable/SensorInjectionController.kt
new file mode 100644
index 0000000..ce2e0fc
--- /dev/null
+++ b/libraries/systemui-helper/src/android/platform/helpers/foldable/SensorInjectionController.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.foldable
+
+import android.hardware.SensorManager
+import android.platform.test.rule.TestWatcher
+import kotlin.properties.Delegates.notNull
+import org.junit.Assume
+
+/**
+ * Allows to inject values to a sensor. Assumes that sensor injection is supported. Note that
+ * currently injection is only supported on virtual devices.
+ */
+class SensorInjectionController(sensorType: Int) : TestWatcher() {
+
+    private val sensorManager = context.getSystemService(SensorManager::class.java)!!
+    private val sensor = sensorManager.getDefaultSensor(sensorType)
+    private var initialized = false
+
+    var injectionSupported by notNull<Boolean>()
+        private set
+
+    fun init() {
+        executeShellCommand(SENSOR_SERVICE_ENABLE)
+        executeShellCommand(SENSOR_SERVICE_DATA_INJECTION + context.packageName)
+        injectionSupported = sensorManager.initDataInjection(true)
+        initialized = true
+    }
+
+    fun uninit() {
+        if (initialized && injectionSupported) {
+            sensorManager.initDataInjection(false)
+        }
+        initialized = false
+    }
+
+    fun setValue(value: Float) {
+        check(initialized) { "Trying to set sensor value before initialization" }
+        Assume.assumeTrue("Skipping as data injection is not supported", injectionSupported)
+        check(
+            sensorManager.injectSensorData(
+                sensor,
+                floatArrayOf(value),
+                SensorManager.SENSOR_STATUS_ACCURACY_HIGH,
+                System.currentTimeMillis()
+            )
+        ) { "Error while injecting sensor data." }
+    }
+
+    companion object {
+        private const val SENSOR_SERVICE_ENABLE = "dumpsys sensorservice enable"
+        private const val SENSOR_SERVICE_DATA_INJECTION = "dumpsys sensorservice data_injection "
+    }
+}
diff --git a/libraries/systemui-helper/src/android/platform/helpers/media/MediaController.java b/libraries/systemui-helper/src/android/platform/helpers/media/MediaController.java
new file mode 100644
index 0000000..be2f19e
--- /dev/null
+++ b/libraries/systemui-helper/src/android/platform/helpers/media/MediaController.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.media;
+
+import android.app.Instrumentation;
+import android.graphics.Rect;
+import android.media.MediaMetadata;
+import android.media.session.PlaybackState;
+import android.platform.test.scenario.tapl_common.Gestures;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.BySelector;
+import androidx.test.uiautomator.Direction;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.UiObject2;
+import androidx.test.uiautomator.Until;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public class MediaController {
+
+    private static final String PKG = "com.android.systemui";
+    private static final String HIDE_BTN_RES = "dismiss";
+    private static final BySelector PLAY_BTN_SELECTOR =
+        By.res(PKG, "actionPlayPause").descContains("Play");
+    private static final BySelector PAUSE_BTN_SELECTOR =
+        By.res(PKG, "actionPlayPause").descContains("Pause");
+    private static final BySelector SKIP_NEXT_BTN_SELECTOR =
+        By.res(PKG, "actionNext").descContains("Next");
+    private static final BySelector SKIP_PREV_BTN_SELECTOR =
+        By.res(PKG, "actionPrev").descContains("Previous");
+    private static final int WAIT_TIME_MILLIS = 10_000;
+    private static final int LONG_PRESS_TIME_MILLIS = 1_000;
+    private static final long UI_WAIT_TIMEOUT = 3_000;
+
+    private final UiObject2 mUiObject;
+    private final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation();
+    private final UiDevice mDevice = UiDevice.getInstance(mInstrumentation);
+    private final List<Integer> mStateChanges;
+    private Runnable mStateListener;
+
+    MediaController(MediaInstrumentation media, UiObject2 uiObject) {
+        media.addMediaSessionStateChangedListeners(this::onMediaSessionStageChanged);
+        mUiObject = uiObject;
+        mStateChanges = new ArrayList<>();
+    }
+
+    public void play() {
+        runToNextState(
+            () -> mUiObject
+                .wait(Until.findObject(PLAY_BTN_SELECTOR), WAIT_TIME_MILLIS)
+                .click(),
+            PlaybackState.STATE_PLAYING);
+    }
+
+    public void pause() {
+        runToNextState(
+                () -> Gestures.click(
+                        mUiObject.wait(Until.findObject(PAUSE_BTN_SELECTOR), WAIT_TIME_MILLIS),
+                        "Pause button"),
+                PlaybackState.STATE_PAUSED);
+    }
+
+    public void skipToNext() {
+        runToNextState(
+                () -> Gestures.click(
+                        mUiObject.wait(Until.findObject(SKIP_NEXT_BTN_SELECTOR), WAIT_TIME_MILLIS),
+                        "Next button"),
+                PlaybackState.STATE_SKIPPING_TO_NEXT);
+    }
+
+    public void skipToPrev() {
+        runToNextState(
+                () -> Gestures.click(
+                        mUiObject.wait(Until.findObject(SKIP_PREV_BTN_SELECTOR), WAIT_TIME_MILLIS),
+                        "Previous button"),
+                PlaybackState.STATE_SKIPPING_TO_PREVIOUS);
+    }
+
+    private void runToNextState(Runnable runnable, int state) {
+        mStateChanges.clear();
+        CountDownLatch latch = new CountDownLatch(1);
+        mStateListener = latch::countDown;
+        runnable.run();
+        try {
+            if (!latch.await(WAIT_TIME_MILLIS, TimeUnit.MILLISECONDS)) {
+                throw new RuntimeException("PlaybackState didn't change and timeout.");
+            }
+        } catch (InterruptedException e) {
+            throw new RuntimeException();
+        }
+        if (!mStateChanges.contains(state)) {
+            throw new RuntimeException(String.format("Fail to run to next state(%d).", state));
+        }
+    }
+
+    private void onMediaSessionStageChanged(int state) {
+        mStateChanges.add(state);
+        if (mStateListener != null) {
+            mStateListener.run();
+            mStateListener = null;
+        }
+    }
+
+    public String title() {
+        UiObject2 header =
+            mUiObject.wait(Until.findObject(By.res(PKG, "header_title")), WAIT_TIME_MILLIS);
+        if (header == null) {
+            return "";
+        }
+        return header.getText();
+    }
+
+    /**
+     * Long press for {@link #LONG_PRESS_TIME_MILLIS} ms on UMO then clik the hide button.
+     */
+    public void longPressAndHide() {
+        if (mUiObject == null) {
+            throw new RuntimeException("UMO should exist to do long press.");
+        }
+
+        mUiObject.click(LONG_PRESS_TIME_MILLIS);
+        UiObject2 hideBtn = mUiObject.wait(
+            Until.findObject(By.res(PKG, HIDE_BTN_RES)), WAIT_TIME_MILLIS);
+        if (hideBtn == null) {
+            throw new RuntimeException("Hide button should exist after long press on UMO.");
+        }
+        hideBtn.clickAndWait(Until.newWindow(), UI_WAIT_TIMEOUT);
+    }
+
+    /**
+     * Checks if the current media session is using the given MediaMetadata.
+     *
+     * @param meta MediaMetadata to get media title and artist.
+     * @return boolean
+     */
+    public boolean hasMetadata(MediaMetadata meta) {
+        final BySelector mediaTitleSelector =
+            By.res(PKG, "header_title").text(meta.getString(MediaMetadata.METADATA_KEY_TITLE));
+        final BySelector mediaArtistSelector =
+            By.res(PKG, "header_artist")
+                .text(meta.getString(MediaMetadata.METADATA_KEY_ARTIST));
+        return mUiObject.hasObject(mediaTitleSelector) && mUiObject.hasObject(mediaArtistSelector);
+    }
+
+    public boolean swipe(Direction direction) {
+        Rect bound = mUiObject.getVisibleBounds();
+        final int startX;
+        final int endX;
+        switch (direction) {
+            case LEFT:
+                startX = (bound.right + bound.centerX()) / 2;
+                endX = bound.left;
+                break;
+            case RIGHT:
+                startX = (bound.left + bound.centerX()) / 2;
+                endX = bound.right;
+                break;
+            default:
+                throw new RuntimeException(
+                    String.format("swipe to %s on UMO isn't supported.", direction));
+        }
+        return mDevice.swipe(startX, bound.centerY(), endX, bound.centerY(), 10);
+    }
+
+    public Rect getVisibleBound() {
+        return mUiObject.getVisibleBounds();
+    }
+}
diff --git a/libraries/systemui-helper/src/android/platform/helpers/media/MediaInstrumentation.java b/libraries/systemui-helper/src/android/platform/helpers/media/MediaInstrumentation.java
new file mode 100644
index 0000000..8c4c46a
--- /dev/null
+++ b/libraries/systemui-helper/src/android/platform/helpers/media/MediaInstrumentation.java
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.media;
+
+import static org.junit.Assert.assertNotNull;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.graphics.Rect;
+import android.media.MediaMetadata;
+import android.media.session.MediaSession;
+import android.media.session.PlaybackState;
+import android.os.Handler;
+import android.os.Looper;
+import android.platform.test.util.HealthTestingUtils;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.BySelector;
+import androidx.test.uiautomator.Direction;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.UiObject2;
+import androidx.test.uiautomator.Until;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+public final class MediaInstrumentation {
+
+    private static final int WAIT_TIME_MILLIS = 5000;
+    private static final String PKG = "com.android.systemui";
+    private static final String MEDIA_CONTROLLER_RES_ID = "qs_media_controls";
+    private static int notificationID = 0;
+
+    private final UiDevice mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+
+    private final String mChannelId;
+    private final NotificationManager mManager;
+    private final MediaSession mMediaSession;
+    private final Handler mHandler;
+    private final MediaSessionCallback mCallback;
+    private final Context mContext;
+    // TODO(bennolin): support legacy version media controller. Please refer
+    //  go/media-t-app-changes for more details.
+    private final boolean mUseLegacyVersion;
+    private final List<Consumer<Integer>> mMediaSessionStateChangedListeners;
+    private final int mNotificationId;
+    private final MockMediaPlayer mPlayer;
+    private int mCurrentMediaState;
+
+    // the idx of mMediaSources which represents current media source.
+    private int mCurrentMediaSource;
+    private final List<MediaMetadata> mMediaSources;
+
+    private MediaInstrumentation(
+            Context context, MediaSession mediaSession,
+            List<MediaMetadata> mediaSources,
+            String channelId, boolean useLegacyVersion
+    ) {
+        mHandler = new Handler(Looper.getMainLooper());
+        mContext = context;
+        mMediaSession = mediaSession;
+        mChannelId = channelId;
+        mUseLegacyVersion = useLegacyVersion;
+        mManager = context.getSystemService(NotificationManager.class);
+        mCurrentMediaState = PlaybackState.STATE_NONE;
+        mPlayer = new MockMediaPlayer();
+        mCallback = new MediaSessionCallback(mPlayer);
+        mMediaSources = mediaSources;
+        mCurrentMediaSource = 0;
+        mNotificationId = ++notificationID;
+        mMediaSessionStateChangedListeners = new ArrayList<>();
+        initialize();
+    }
+
+    private void initialize() {
+        mHandler.post(() -> mMediaSession.setCallback(mCallback));
+        mCallback.addOnMediaStateChangedListener(this::onMediaSessionStateChanged);
+        mCallback.addOnMediaStateChangedListener(this::onMediaSessionSkipTo);
+        MediaMetadata source = mMediaSources.stream().findFirst().orElse(null);
+        mMediaSession.setMetadata(source);
+        mMediaSession.setActive(true);
+        mPlayer.setDataSource(source);
+        mPlayer.setOnCompletionListener(() -> setCurrentMediaState(PlaybackState.STATE_STOPPED));
+        setCurrentMediaState(
+                source == null ? PlaybackState.STATE_NONE : PlaybackState.STATE_STOPPED);
+    }
+
+    Notification.Builder buildNotification() {
+        return new Notification.Builder(mContext, mChannelId)
+                .setContentTitle("MediaInstrumentation")
+                .setContentText("media")
+                .setSmallIcon(android.R.drawable.stat_sys_headset)
+                .setStyle(new Notification.MediaStyle()
+                        .setMediaSession(mMediaSession.getSessionToken()));
+    }
+
+    public void createNotification() {
+        mManager.notify(mNotificationId, buildNotification().build());
+    }
+
+    UiObject2 scrollToMediaNotification(MediaMetadata meta) {
+        final BySelector qsScrollViewSelector = By.res(PKG, "expanded_qs_scroll_view");
+        final BySelector mediaTitleSelector = By.res(PKG, "header_title")
+                .text(meta.getString(MediaMetadata.METADATA_KEY_TITLE));
+        final BySelector umoSelector = By.res(PKG, MEDIA_CONTROLLER_RES_ID)
+                .hasDescendant(mediaTitleSelector);
+        UiObject2 notification = mDevice.wait(Until.findObject(umoSelector), WAIT_TIME_MILLIS);
+        if (notification == null) {
+            // Try to scroll down the QS container to make UMO visible.
+            UiObject2 qsScrollView = mDevice.wait(Until.findObject(qsScrollViewSelector),
+                    WAIT_TIME_MILLIS);
+            assertNotNull("Unable to scroll the QS container.", qsScrollView);
+            qsScrollView.scroll(Direction.DOWN, 1.0f, 100);
+            notification = mDevice.wait(Until.findObject(umoSelector), WAIT_TIME_MILLIS);
+        }
+        assertNotNull("Unable to find UMO.", notification);
+        // The UMO may still not be fully visible, double check it's visibility.
+        notification = ensureUMOFullyVisible(notification);
+        assertNotNull("UMO isn't fully visible.", notification);
+        mDevice.waitForIdle();
+        HealthTestingUtils.waitForValueToSettle(
+                () -> "UMO isn't settle after timeout.", notification::getVisibleBounds);
+        return notification;
+    }
+
+    private UiObject2 ensureUMOFullyVisible(UiObject2 umo) {
+        final BySelector footerSelector = By.res(PKG, "qs_footer_actions");
+        UiObject2 footer = mDevice.wait(Until.findObject(footerSelector), WAIT_TIME_MILLIS);
+        assertNotNull("Can't find QS actions footer.", footer);
+        Rect umoBound = umo.getVisibleBounds();
+        Rect footerBound = footer.getVisibleBounds();
+        int distance = umoBound.bottom - footerBound.top;
+        if (distance <= 0) {
+            return umo;
+        }
+        distance += footerBound.height();
+        UiObject2 scrollable = mDevice.wait(Until.findObject(By.scrollable(true)), WAIT_TIME_MILLIS);
+        scrollable.scroll(
+                Direction.DOWN, (float)distance / scrollable.getVisibleBounds().height(), 100);
+        return mDevice.wait(Until.findObject(By.res(umo.getResourceName())), WAIT_TIME_MILLIS);
+    }
+
+    /**
+     * Find the UMO that belongs to the current MediaInstrumentation (Media Session).
+     * If the UMO can't be found, the function will raise an assertion error.
+     *
+     * @return MediaController
+     */
+    public MediaController getMediaNotification() {
+        MediaMetadata source = mMediaSources.stream().findFirst().orElseThrow();
+        UiObject2 notification = scrollToMediaNotification(source);
+        return new MediaController(this, notification);
+    }
+
+    /**
+     * Find the UMO in current view. This method will only check UMO in current view page different
+     * than {@link #getMediaNotification()} to seek UMO in quick setting view.
+     *
+     * @return MediaController
+     * @throws AssertionError if the UMO can't be found in current view.
+     */
+    public MediaController getMediaNotificationInCurrentView() {
+        MediaMetadata source = mMediaSources.stream().findFirst().orElseThrow();
+        final BySelector mediaTitleSelector = By.res(PKG, "header_title")
+                .text(source.getString(MediaMetadata.METADATA_KEY_TITLE));
+        final BySelector umoSelector = By.res(PKG, MEDIA_CONTROLLER_RES_ID)
+                .hasDescendant(mediaTitleSelector);
+        UiObject2 notification = mDevice.wait(Until.findObject(umoSelector), WAIT_TIME_MILLIS);
+        assertNotNull("Unable to find UMO.", notification);
+        mDevice.waitForIdle();
+        HealthTestingUtils.waitForValueToSettle(
+                () -> "UMO isn't settle after timeout.", notification::getVisibleBounds);
+        return new MediaController(this, notification);
+    }
+
+    public boolean isMediaNotificationVisible() {
+        return mDevice.hasObject(By.res(PKG, MEDIA_CONTROLLER_RES_ID));
+    }
+
+    public void addMediaSessionStateChangedListeners(Consumer<Integer> listener) {
+        mMediaSessionStateChangedListeners.add(listener);
+    }
+
+    public void clearMediaSessionStateChangedListeners() {
+        mMediaSessionStateChangedListeners.clear();
+    }
+
+    private void onMediaSessionStateChanged(int state) {
+        setCurrentMediaState(state);
+        for (Consumer<Integer> listener : mMediaSessionStateChangedListeners) {
+            listener.accept(state);
+        }
+    }
+
+    private void onMediaSessionSkipTo(int state) {
+        final int sources = mMediaSources.size();
+        if (sources <= 0) { // no media sources to skip to
+            return;
+        }
+        switch (state) {
+            case PlaybackState.STATE_SKIPPING_TO_NEXT:
+                mCurrentMediaSource = (mCurrentMediaSource + 1) % sources;
+                break;
+            case PlaybackState.STATE_SKIPPING_TO_PREVIOUS:
+                mCurrentMediaSource = (mCurrentMediaSource - 1) % sources;
+                break;
+            default: // the state changing isn't related to skip.
+                return;
+        }
+        mMediaSession.setMetadata(mMediaSources.get(mCurrentMediaSource));
+        mPlayer.setDataSource(mMediaSources.get(mCurrentMediaSource));
+        mPlayer.reset();
+        mPlayer.start();
+        setCurrentMediaState(PlaybackState.STATE_PLAYING);
+        createNotification();
+    }
+
+    private void updatePlaybackState() {
+        if (mUseLegacyVersion) {
+            // TODO(bennolin): add legacy version, be aware of `setState`  and  `ACTION_SEEK_TO`
+            //  are still relevant to legacy version controller.
+            return;
+        }
+        mMediaSession.setPlaybackState(new PlaybackState.Builder()
+                .setActions(getAvailableActions(mCurrentMediaState))
+                .setState(mCurrentMediaState, mPlayer.getCurrentPosition(), 1.0f)
+                .build());
+    }
+
+    private void setCurrentMediaState(int state) {
+        mCurrentMediaState = state;
+        updatePlaybackState();
+    }
+
+    private Long getAvailableActions(int state) {
+        switch (state) {
+            case PlaybackState.STATE_PLAYING:
+                return PlaybackState.ACTION_PAUSE
+                        | PlaybackState.ACTION_SEEK_TO
+                        | PlaybackState.ACTION_SKIP_TO_NEXT
+                        | PlaybackState.ACTION_SKIP_TO_PREVIOUS;
+            case PlaybackState.STATE_PAUSED:
+                return PlaybackState.ACTION_PLAY
+                        | PlaybackState.ACTION_STOP
+                        | PlaybackState.ACTION_SKIP_TO_NEXT
+                        | PlaybackState.ACTION_SKIP_TO_PREVIOUS;
+            case PlaybackState.STATE_STOPPED:
+                return PlaybackState.ACTION_PLAY
+                        | PlaybackState.ACTION_PAUSE
+                        | PlaybackState.ACTION_SKIP_TO_NEXT
+                        | PlaybackState.ACTION_SKIP_TO_PREVIOUS;
+            default:
+                return PlaybackState.ACTION_PLAY | PlaybackState.ACTION_PLAY_PAUSE
+                        | PlaybackState.ACTION_STOP | PlaybackState.ACTION_SEEK_TO;
+        }
+    }
+
+    public static class Builder {
+
+        private final boolean mUseLegacyVersion;
+        private final Context mContext;
+        private final MediaSession mSession;
+        private String mChannelId;
+        private final List<MediaMetadata> mDataSources;
+
+        public Builder(Context context, MediaSession session) {
+            mUseLegacyVersion = false;
+            mContext = context;
+            mChannelId = "";
+            mSession = session;
+            mDataSources = new ArrayList<>();
+        }
+
+        public Builder setChannelId(String id) {
+            mChannelId = id;
+            return this;
+        }
+
+        public Builder addDataSource(MediaMetadata source) {
+            mDataSources.add(source);
+            return this;
+        }
+
+        public MediaInstrumentation build() {
+            if (mChannelId.isEmpty()) {
+                NotificationManager manager = mContext.getSystemService(NotificationManager.class);
+                mChannelId = MediaInstrumentation.class.getCanonicalName();
+                NotificationChannel channel = new NotificationChannel(
+                        mChannelId, "Default", NotificationManager.IMPORTANCE_DEFAULT);
+                manager.createNotificationChannel(channel);
+            }
+            return new MediaInstrumentation(
+                    mContext, mSession, mDataSources, mChannelId, mUseLegacyVersion);
+        }
+    }
+}
diff --git a/libraries/systemui-helper/src/android/platform/helpers/media/MediaSessionCallback.java b/libraries/systemui-helper/src/android/platform/helpers/media/MediaSessionCallback.java
new file mode 100644
index 0000000..fe82888
--- /dev/null
+++ b/libraries/systemui-helper/src/android/platform/helpers/media/MediaSessionCallback.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.media;
+
+import android.media.session.MediaSession;
+import android.media.session.PlaybackState;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+final class MediaSessionCallback extends MediaSession.Callback {
+    private final List<Consumer<Integer>> mOnMediaStateChangedListeners;
+    private final MockMediaPlayer mPlayer;
+
+    public MediaSessionCallback(MockMediaPlayer player) {
+        mPlayer = player;
+        mOnMediaStateChangedListeners = new ArrayList<>();
+    }
+
+    @Override
+    public void onPlay() {
+        mPlayer.start();
+        setCurrentMediaState(PlaybackState.STATE_PLAYING);
+    }
+
+    @Override
+    public void onPause() {
+        mPlayer.pause();
+        setCurrentMediaState(PlaybackState.STATE_PAUSED);
+    }
+
+    @Override
+    public void onSkipToNext() {
+        mPlayer.pause();
+        setCurrentMediaState(PlaybackState.STATE_SKIPPING_TO_NEXT);
+    }
+
+    @Override
+    public void onSkipToPrevious() {
+        mPlayer.pause();
+        setCurrentMediaState(PlaybackState.STATE_SKIPPING_TO_PREVIOUS);
+    }
+
+    public void addOnMediaStateChangedListener(Consumer<Integer> listener) {
+        mOnMediaStateChangedListeners.add(listener);
+    }
+
+    private void setCurrentMediaState(int state) {
+        for (Consumer<Integer> listener : mOnMediaStateChangedListeners) {
+            listener.accept(state);
+        }
+    }
+}
diff --git a/libraries/systemui-helper/src/android/platform/helpers/media/MockMediaPlayer.java b/libraries/systemui-helper/src/android/platform/helpers/media/MockMediaPlayer.java
new file mode 100644
index 0000000..02876c0
--- /dev/null
+++ b/libraries/systemui-helper/src/android/platform/helpers/media/MockMediaPlayer.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.media;
+
+import android.media.MediaMetadata;
+
+import androidx.annotation.Nullable;
+
+import java.time.Duration;
+import java.util.Timer;
+import java.util.TimerTask;
+
+final class MockMediaPlayer {
+
+    private final static int PERIOD = 1000; // milliseconds
+
+    private long mCurrentPosition; // current position in milliseconds.
+    private Timer mTimer;
+    @Nullable
+    private MediaMetadata mCurrentSource;
+    private Runnable mOnCompletionListener;
+
+    public MockMediaPlayer() {
+        mCurrentPosition = 0;
+    }
+
+    public void start() {
+        mTimer = new Timer();
+        mTimer.scheduleAtFixedRate(new TimerTask() {
+            @Override
+            public void run() {
+                mCurrentPosition += PERIOD;
+                if (mCurrentPosition >= getDuration()) {
+                    onCompletion();
+                }
+            }
+        }, 0, PERIOD);
+    }
+
+    private void onCompletion() {
+        reset();
+        if (mOnCompletionListener != null) {
+            mOnCompletionListener.run();
+        }
+    }
+
+    public void setOnCompletionListener(@Nullable Runnable listener) {
+        mOnCompletionListener = listener;
+    }
+
+    public void reset() {
+        pause();
+        mCurrentPosition = 0;
+    }
+
+    public void pause() {
+        if (mTimer != null) {
+            mTimer.cancel();
+        }
+        mTimer = null;
+    }
+
+    public void stop() {
+        reset();
+    }
+
+    public long getCurrentPosition() {
+        return mCurrentPosition;
+    }
+
+    public void setDataSource(MediaMetadata source) {
+        mCurrentSource = source;
+    }
+
+    private long getDuration() {
+        return mCurrentSource.getLong(MediaMetadata.METADATA_KEY_DURATION);
+    }
+}
diff --git a/libraries/systemui-helper/src/android/platform/helpers/rules/LockscreenRule.kt b/libraries/systemui-helper/src/android/platform/helpers/rules/LockscreenRule.kt
new file mode 100644
index 0000000..9efd9f4
--- /dev/null
+++ b/libraries/systemui-helper/src/android/platform/helpers/rules/LockscreenRule.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.rules
+
+import android.platform.helpers.LockscreenUtils
+import android.platform.helpers.LockscreenUtils.LockscreenType
+import android.platform.helpers.LockscreenUtils.LockscreenType.*
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
+
+/**
+ * Sets [type] lockscreen before the test and resets it to [SWIPE] afterwards.
+ *
+ * Note that if [NONE] type is set, it is needed to unlock the current one before not having it in
+ * the future.
+ *
+ * This default to [SWIPE] in [finished] as it is the default after a factory reset.
+ */
+class LockscreenRule(private val type: LockscreenType) : TestWatcher() {
+
+    override fun starting(description: Description?) {
+        when (type) {
+            PIN -> setLockscreen(type = PIN, code = VALID_PIN)
+            NONE -> setLockscreen(type = NONE)
+            PASSWORD,
+            PATTERN,
+            SWIPE -> TODO("Not yet supported.")
+        }
+    }
+
+    override fun finished(description: Description?) {
+        if (type == PIN) {
+            LockscreenUtils.resetLockscreen(VALID_PIN)
+        }
+        setLockscreen(SWIPE)
+    }
+}
+
+private const val VALID_PIN = "1234"
+
+/** Wrapper for java method to make above code less verbose and error prone. */
+private fun setLockscreen(
+    type: LockscreenType,
+    code: String? = null,
+    expectedLocked: Boolean = code != null
+): Unit = LockscreenUtils.setLockscreen(type, code, expectedLocked)
diff --git a/libraries/systemui-helper/src/android/platform/helpers/rules/SidedBouncerRule.kt b/libraries/systemui-helper/src/android/platform/helpers/rules/SidedBouncerRule.kt
new file mode 100644
index 0000000..79d4c3a
--- /dev/null
+++ b/libraries/systemui-helper/src/android/platform/helpers/rules/SidedBouncerRule.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.rules
+
+import android.platform.helpers.ui.UiAutomatorUtils.getUiDevice
+import android.provider.Settings.Global.ONE_HANDED_KEYGUARD_SIDE
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
+
+/**
+ * Resets the bouncer side to default after each test.
+ *
+ * This is needed to make the initial bouncer in a predictable location, not based on past
+ * interactions.
+ */
+class SidedBouncerRule : TestWatcher() {
+    override fun finished(description: Description?) {
+        getUiDevice().executeShellCommand("settings delete global $ONE_HANDED_KEYGUARD_SIDE")
+    }
+}
diff --git a/libraries/systemui-helper/src/android/platform/helpers/rules/TemporaryMediaNotification.kt b/libraries/systemui-helper/src/android/platform/helpers/rules/TemporaryMediaNotification.kt
new file mode 100644
index 0000000..76e0d84
--- /dev/null
+++ b/libraries/systemui-helper/src/android/platform/helpers/rules/TemporaryMediaNotification.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.rules
+
+import android.R
+import android.app.Notification
+import android.app.NotificationManager
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Color
+import android.media.MediaMetadata
+import android.media.session.MediaSession
+import android.media.session.PlaybackState
+import androidx.test.InstrumentationRegistry
+import java.time.Duration
+import org.junit.rules.ExternalResource
+
+/** Posts a temporary media notification, and deletes it when the test finishes. */
+class TemporaryMediaNotification(private val id: String) : ExternalResource() {
+
+    companion object {
+        // Pieces of the media session.
+        private const val SESSION_KEY = "Session"
+        private const val SESSION_TITLE = "Title"
+        private const val SESSION_ARTIST = "Artist"
+        private const val PLAYBACK_SPEED = 1f
+        private val SESSION_DURATION = Duration.ofMinutes(60)
+        private val SESSION_POSITION = Duration.ofMinutes(6)
+
+        // Pieces of the notification.
+        private const val NOTIFICATION_ID = 1
+        private const val TITLE = "Media-style Notification"
+        private const val TEXT = "Notification for a test media session"
+    }
+
+    private lateinit var notificationManager: NotificationManager
+    private lateinit var mediaSession: MediaSession
+
+    override fun before() {
+        val context = InstrumentationRegistry.getTargetContext()
+        notificationManager = context.getSystemService(NotificationManager::class.java)!!
+        mediaSession = MediaSession(context, SESSION_KEY)
+
+        // create a solid color bitmap to use as album art in media metadata
+        val bitmap: Bitmap = Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)
+        Canvas(bitmap).drawColor(Color.YELLOW)
+
+        mediaSession.setMetadata(
+            MediaMetadata.Builder()
+                .putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST)
+                .putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE)
+                .putLong(MediaMetadata.METADATA_KEY_DURATION, SESSION_DURATION.toMillis())
+                .putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, bitmap)
+                .build()
+        )
+        mediaSession.setPlaybackState(
+            PlaybackState.Builder()
+                .setState(PlaybackState.STATE_PAUSED, SESSION_POSITION.toMillis(), PLAYBACK_SPEED)
+                .setActions(
+                    PlaybackState.ACTION_SEEK_TO or
+                        PlaybackState.ACTION_PLAY or
+                        PlaybackState.ACTION_PAUSE or
+                        PlaybackState.ACTION_SKIP_TO_PREVIOUS or
+                        PlaybackState.ACTION_SKIP_TO_NEXT
+                )
+                .addCustomAction("action.rew", "Rewind", R.drawable.ic_media_rew)
+                .addCustomAction("action.ff", "Fast Forward", R.drawable.ic_media_ff)
+                .build()
+        )
+
+        val notificationBuilder =
+            Notification.Builder(context, id)
+                .setContentTitle(TITLE)
+                .setContentText(TEXT)
+                .setSmallIcon(R.drawable.ic_media_pause)
+                .setStyle(
+                    Notification.MediaStyle()
+                        .setShowActionsInCompactView(1, 2, 3)
+                        .setMediaSession(mediaSession.sessionToken)
+                )
+                .setColor(Color.BLUE)
+                .setColorized(true)
+                .addAction(R.drawable.ic_media_rew, "rewind", null)
+                .addAction(R.drawable.ic_media_previous, "previous track", null)
+                .addAction(R.drawable.ic_media_pause, "pause", null)
+                .addAction(R.drawable.ic_media_next, "next track", null)
+                .addAction(R.drawable.ic_media_ff, "fast forward", null)
+
+        mediaSession.isActive = true
+        notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build())
+    }
+
+    override fun after() {
+        notificationManager.cancel(NOTIFICATION_ID)
+        mediaSession.release()
+    }
+}
diff --git a/libraries/systemui-helper/src/android/platform/helpers/rules/TemporaryNotificationChannel.java b/libraries/systemui-helper/src/android/platform/helpers/rules/TemporaryNotificationChannel.java
new file mode 100644
index 0000000..4408390
--- /dev/null
+++ b/libraries/systemui-helper/src/android/platform/helpers/rules/TemporaryNotificationChannel.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.rules;
+
+import static android.app.NotificationManager.IMPORTANCE_LOW;
+
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import androidx.test.InstrumentationRegistry;
+
+import org.junit.rules.ExternalResource;
+
+/**
+ * The TemporaryNotificationChannel Rule allows creation of a notification channel that will be
+ * deleted when the test finishes.
+ */
+public class TemporaryNotificationChannel extends ExternalResource {
+
+    private static final String DEFAULT_ID = "temporary_channel_id";
+    private static final String DEFAULT_NAME = "Temporary Channel";
+    private static final int DEFAULT_IMPORTANCE = IMPORTANCE_LOW;
+
+    private final String mId;
+    private final String mName;
+    private final int mImportance;
+
+    public TemporaryNotificationChannel() {
+        this(DEFAULT_ID, DEFAULT_NAME, DEFAULT_IMPORTANCE);
+    }
+
+    public TemporaryNotificationChannel(String id, String name, int importance) {
+        mId = id;
+        mName = name;
+        mImportance = importance;
+    }
+
+    public String getId() {
+        return mId;
+    }
+
+    @Override
+    protected void before() {
+        NotificationChannel channel = new NotificationChannel(mId, mName, mImportance);
+        getNotificationManager().createNotificationChannel(channel);
+    }
+
+    @Override
+    protected void after() {
+        getNotificationManager().deleteNotificationChannel(mId);
+    }
+
+    private NotificationManager getNotificationManager() {
+        return InstrumentationRegistry
+                .getTargetContext()
+                .getSystemService(NotificationManager.class);
+    }
+}
diff --git a/libraries/systemui-helper/src/android/platform/helpers/ui/UiAutomatorUtils.java b/libraries/systemui-helper/src/android/platform/helpers/ui/UiAutomatorUtils.java
new file mode 100644
index 0000000..009e0a9
--- /dev/null
+++ b/libraries/systemui-helper/src/android/platform/helpers/ui/UiAutomatorUtils.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.ui;
+
+import android.app.Instrumentation;
+import android.support.test.uiautomator.UiDevice;
+import android.util.Log;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+/**
+ * Most commonly used UiAutomator CommonUtils required for writing UI tests.
+ */
+public class UiAutomatorUtils {
+
+    private static final String TAG = "UiAutomatorUtils";
+
+    private UiAutomatorUtils() {
+    }
+
+    /**
+     * To get the instance of UiDevice which provides access to state information about the device.
+     *
+     * @return an instance of UiDevice.
+     */
+    public static UiDevice getUiDevice() {
+        return UiDevice.getInstance(getInstrumentation());
+    }
+
+    /**
+     * To get base class for implementing application instrumentation code.
+     *
+     * @return an instance of Instrumentation
+     */
+    public static Instrumentation getInstrumentation() {
+        return InstrumentationRegistry.getInstrumentation();
+    }
+
+    /**
+     * Dumps the current view hierarchy
+     */
+    public static void dumpViewHierarchy() {
+        final ByteArrayOutputStream stream = new ByteArrayOutputStream();
+        try {
+            UiAutomatorUtils.getUiDevice().dumpWindowHierarchy(stream);
+            stream.flush();
+            stream.close();
+            for (String line : stream.toString().split("\\r?\\n")) {
+                Log.i(TAG, line.trim());
+            }
+        } catch (IOException e) {
+            Log.e(TAG, "error dumping XML to logcat", e);
+        }
+    }
+}
diff --git a/libraries/systemui-helper/src/android/platform/helpers/ui/UiSearch.java b/libraries/systemui-helper/src/android/platform/helpers/ui/UiSearch.java
new file mode 100644
index 0000000..3c7c854
--- /dev/null
+++ b/libraries/systemui-helper/src/android/platform/helpers/ui/UiSearch.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.ui;
+
+import static android.platform.helpers.ui.UiAutomatorUtils.getUiDevice;
+import static android.support.test.uiautomator.Until.hasObject;
+
+import static java.lang.String.format;
+import static java.lang.System.currentTimeMillis;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiObjectNotFoundException;
+import android.support.test.uiautomator.UiScrollable;
+import android.support.test.uiautomator.UiSelector;
+import android.util.Log;
+
+/**
+ * Helper class for searching Ui Element.
+ */
+public class UiSearch {
+
+    private static final String TAG = "UiSearch";
+    private static final int SHORT_WAIT_IN_SECONDS = 2;
+    private static final int MAX_SWIPE_STEPS = 50;
+
+    private UiSearch() {
+    }
+
+    /**
+     * Searches the given UiSelector in the given page by scrolling through the page.
+     *
+     * @param scroller            lookup scroller.
+     * @param selector            selector to be searched.
+     * @param name                name of the selector.
+     * @param maxTimeoutInSeconds number time to retry. By default it will run 1 time if retry given
+     *                            is 0.
+     * @return an UiSelector instance of the searched selector if found otherwise null.
+     */
+    public static boolean search(UiScrollable scroller, UiSelector selector, String name,
+            int maxTimeoutInSeconds) {
+        if (scroller != null) {
+            Log.d(TAG,
+                    format("Looking for %s[%s] in the List[%s] within %s seconds", name, selector,
+                            scroller.getSelector(), maxTimeoutInSeconds));
+        } else {
+            Log.d(TAG,
+                    format("Looking for %s[%s] within %s seconds", name, selector,
+                            maxTimeoutInSeconds));
+        }
+
+        try {
+            long endTime = currentTimeMillis() + SECONDS.toMillis(maxTimeoutInSeconds);
+            //Checks if the given selector is null
+            if (selector == null) {
+                Log.w(TAG, format("Selector[%s] is null", selector));
+                return false;
+            }
+            // Checks if given selector is present on the current screen.
+            if (search(selector, SHORT_WAIT_IN_SECONDS)) {
+                return true;
+            }
+            // If given scroller is null and search for the selector on the current page.
+            if (scroller == null) {
+                Log.w(TAG, format("Scroller is null. So looking for %s[%s] in in %s seconds", name,
+                        selector, maxTimeoutInSeconds));
+                return search(selector, maxTimeoutInSeconds);
+            }
+            // Checks if given scroller exist or not.
+            if (!search(scroller.getSelector(), maxTimeoutInSeconds)) {
+                Log.w(TAG, format("Given Scroller[%s] was not found in %s seconds",
+                        scroller.getSelector(), maxTimeoutInSeconds));
+                return false;
+            }
+            // Looking for given selector.
+            do {
+                // Checks if given searchable selector isElementVisible or not.
+                if (scroller.scrollIntoView(selector)) {
+                    return true;
+                }
+            } while (currentTimeMillis() <= endTime);
+        } catch (UiObjectNotFoundException e) {
+            throw new RuntimeException(e);
+        }
+        return false;
+    }
+
+    /**
+     * Searches the given BySelector in the given page by scrolling through the page.
+     *
+     * @param scroller            lookup scroller.
+     * @param selector            selector to be searched.
+     * @param name                name of the selector.
+     * @param maxTimeoutInSeconds number time to retry. By default it will run 1 time if retry given
+     *                            is 0.
+     * @return true if found otherwise false.
+     */
+    public static boolean search(UiScrollable scroller, BySelector selector, String name,
+            int maxTimeoutInSeconds) {
+        if (scroller != null) {
+            Log.d(TAG,
+                    format("Looking for %s[%s] in the List[%s] within %s seconds", name, selector,
+                            scroller.getSelector(), maxTimeoutInSeconds));
+        } else {
+            Log.d(TAG,
+                    format("Looking for %s[%s] within %s seconds", name, selector,
+                            maxTimeoutInSeconds));
+        }
+        try {
+            long endTime = currentTimeMillis() + SECONDS.toMillis(maxTimeoutInSeconds);
+            // Checks is searchable selector is null
+            if (selector == null) {
+                Log.w(TAG, format("Selector[%s] is null", selector));
+                return false;
+            }
+            // Checks if given selector is present on the current screen.
+            if (getUiDevice().wait(hasObject(selector),
+                    SECONDS.toMillis(SHORT_WAIT_IN_SECONDS))) {
+                return true;
+            }
+            // If given scroller is null and search the selector on the current page.
+            if (scroller == null) {
+                Log.w(TAG, format("Scroller is null. So looking for %s[%s] in in %s seconds", name,
+                        selector, maxTimeoutInSeconds));
+                return getUiDevice().wait(hasObject(selector),
+                        SECONDS.toMillis(maxTimeoutInSeconds));
+            }
+            // Checks if given scroller exist or not.
+            if (!search(scroller.getSelector(), maxTimeoutInSeconds)) {
+                Log.w(TAG, format("Given Scroller[%s] was not found in %s seconds",
+                        scroller.getSelector(), maxTimeoutInSeconds));
+                return false;
+            }
+            // Looking for given selector in the given list.
+            do {
+                scroller.scrollToBeginning(MAX_SWIPE_STEPS);
+                // Max page scroll to lookup item in list 5 Swipe through the screen to search given
+                // selector
+                int maxSwipe = 5;
+                while (maxSwipe >= 0) {
+                    if (getUiDevice().hasObject(selector)) {
+                        return true;
+                    }
+                    scroller.swipeUp(MAX_SWIPE_STEPS);
+                    maxSwipe--;
+                }
+            } while (currentTimeMillis() <= endTime);
+        } catch (UiObjectNotFoundException e) {
+            throw new RuntimeException(e);
+        }
+        return false;
+    }
+
+    /**
+     * Looks for the given UiSelector with given max timeout.
+     *
+     * @param selector            selector to be searched.
+     * @param maxTimeoutInSeconds max time to look for the selector.
+     * @return if found return true otherwise false.
+     */
+    public static boolean search(UiSelector selector, int maxTimeoutInSeconds) {
+        Log.d(TAG, format("Looking for Selector[%s] within %s seconds", selector,
+                maxTimeoutInSeconds));
+        // Checks is searchable selector is null
+        if (selector == null) {
+            Log.w(TAG, format("Selector[%s] is null", selector));
+            return false;
+        }
+
+        // Looking for the given selector.
+        long endTime = currentTimeMillis() + SECONDS.toMillis(maxTimeoutInSeconds);
+        while (currentTimeMillis() <= endTime) {
+            if (getUiDevice().findObject(selector).exists()) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
diff --git a/libraries/systemui-helper/src/android/platform/helpers/ui/UiSearch2.java b/libraries/systemui-helper/src/android/platform/helpers/ui/UiSearch2.java
new file mode 100644
index 0000000..dafdf01
--- /dev/null
+++ b/libraries/systemui-helper/src/android/platform/helpers/ui/UiSearch2.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.ui;
+
+import static androidx.test.uiautomator.Until.hasObject;
+
+import static java.lang.String.format;
+import static java.lang.System.currentTimeMillis;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import android.util.Log;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.uiautomator.BySelector;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.UiObjectNotFoundException;
+import androidx.test.uiautomator.UiScrollable;
+import androidx.test.uiautomator.UiSelector;
+
+/**
+ * Helper class for searching Ui Element. Androidx test version of {@link UiSearch}
+ */
+public class UiSearch2 {
+
+    private static final String TAG = "UiSearch2";
+    private static final int SHORT_WAIT_IN_SECONDS = 2;
+    private static final int MAX_SWIPE_STEPS = 50;
+
+    private UiSearch2() {
+    }
+
+    private static UiDevice getUiDevice() {
+        return UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+    }
+
+    /**
+     * Searches the given UiSelector in the given page by scrolling through the page.
+     *
+     * @param scroller            lookup scroller.
+     * @param selector            selector to be searched.
+     * @param name                name of the selector.
+     * @param maxTimeoutInSeconds number time to retry. By default it will run 1 time if retry given
+     *                            is 0.
+     * @return an UiSelector instance of the searched selector if found otherwise null.
+     */
+    public static boolean search(UiScrollable scroller, UiSelector selector, String name,
+            int maxTimeoutInSeconds) {
+        if (scroller != null) {
+            Log.d(TAG,
+                    format("Looking for %s[%s] in the List[%s] within %s seconds", name, selector,
+                            scroller.getSelector(), maxTimeoutInSeconds));
+        } else {
+            Log.d(TAG,
+                    format("Looking for %s[%s] within %s seconds", name, selector,
+                            maxTimeoutInSeconds));
+        }
+
+        try {
+            long endTime = currentTimeMillis() + SECONDS.toMillis(maxTimeoutInSeconds);
+            //Checks if the given selector is null
+            if (selector == null) {
+                Log.w(TAG, format("Selector[%s] is null", selector));
+                return false;
+            }
+            // Checks if given selector is present on the current screen.
+            if (search(selector, SHORT_WAIT_IN_SECONDS)) {
+                return true;
+            }
+            // If given scroller is null and search for the selector on the current page.
+            if (scroller == null) {
+                Log.w(TAG, format("Scroller is null. So looking for %s[%s] in in %s seconds", name,
+                        selector, maxTimeoutInSeconds));
+                return search(selector, maxTimeoutInSeconds);
+            }
+            // Checks if given scroller exist or not.
+            if (!search(scroller.getSelector(), maxTimeoutInSeconds)) {
+                Log.w(TAG, format("Given Scroller[%s] was not found in %s seconds",
+                        scroller.getSelector(), maxTimeoutInSeconds));
+                return false;
+            }
+            // Looking for given selector.
+            do {
+                // Checks if given searchable selector isElementVisible or not.
+                if (scroller.scrollIntoView(selector)) {
+                    return true;
+                }
+            } while (currentTimeMillis() <= endTime);
+        } catch (UiObjectNotFoundException e) {
+            throw new RuntimeException(e);
+        }
+        return false;
+    }
+
+    /**
+     * Searches the given BySelector in the given page by scrolling through the page.
+     *
+     * @param scroller            lookup scroller.
+     * @param selector            selector to be searched.
+     * @param name                name of the selector.
+     * @param maxTimeoutInSeconds number time to retry. By default it will run 1 time if retry given
+     *                            is 0.
+     * @return true if found otherwise false.
+     */
+    public static boolean search(UiScrollable scroller, BySelector selector, String name,
+            int maxTimeoutInSeconds) {
+        if (scroller != null) {
+            Log.d(TAG,
+                    format("Looking for %s[%s] in the List[%s] within %s seconds", name, selector,
+                            scroller.getSelector(), maxTimeoutInSeconds));
+        } else {
+            Log.d(TAG,
+                    format("Looking for %s[%s] within %s seconds", name, selector,
+                            maxTimeoutInSeconds));
+        }
+        try {
+            long endTime = currentTimeMillis() + SECONDS.toMillis(maxTimeoutInSeconds);
+            // Checks is searchable selector is null
+            if (selector == null) {
+                Log.w(TAG, format("Selector[%s] is null", selector));
+                return false;
+            }
+            // Checks if given selector is present on the current screen.
+            if (getUiDevice().wait(hasObject(selector),
+                    SECONDS.toMillis(SHORT_WAIT_IN_SECONDS))) {
+                return true;
+            }
+            // If given scroller is null and search the selector on the current page.
+            if (scroller == null) {
+                Log.w(TAG, format("Scroller is null. So looking for %s[%s] in in %s seconds", name,
+                        selector, maxTimeoutInSeconds));
+                return getUiDevice().wait(hasObject(selector),
+                        SECONDS.toMillis(maxTimeoutInSeconds));
+            }
+            // Checks if given scroller exist or not.
+            if (!search(scroller.getSelector(), maxTimeoutInSeconds)) {
+                Log.w(TAG, format("Given Scroller[%s] was not found in %s seconds",
+                        scroller.getSelector(), maxTimeoutInSeconds));
+                return false;
+            }
+            // Looking for given selector in the given list.
+            do {
+                scroller.scrollToBeginning(MAX_SWIPE_STEPS);
+                // Max page scroll to lookup item in list 5 Swipe through the screen to search given
+                // selector
+                int maxSwipe = 5;
+                while (maxSwipe >= 0) {
+                    if (getUiDevice().hasObject(selector)) {
+                        return true;
+                    }
+                    scroller.swipeUp(MAX_SWIPE_STEPS);
+                    maxSwipe--;
+                }
+            } while (currentTimeMillis() <= endTime);
+        } catch (UiObjectNotFoundException e) {
+            throw new RuntimeException(e);
+        }
+        return false;
+    }
+
+    /**
+     * Looks for the given UiSelector with given max timeout.
+     *
+     * @param selector            selector to be searched.
+     * @param maxTimeoutInSeconds max time to look for the selector.
+     * @return if found return true otherwise false.
+     */
+    public static boolean search(UiSelector selector, int maxTimeoutInSeconds) {
+        Log.d(TAG, format("Looking for Selector[%s] within %s seconds", selector,
+                maxTimeoutInSeconds));
+        // Checks is searchable selector is null
+        if (selector == null) {
+            Log.w(TAG, format("Selector[%s] is null", selector));
+            return false;
+        }
+
+        // Looking for the given selector.
+        long endTime = currentTimeMillis() + SECONDS.toMillis(maxTimeoutInSeconds);
+        while (currentTimeMillis() <= endTime) {
+            if (getUiDevice().findObject(selector).exists()) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
diff --git a/libraries/tapl-common/Android.bp b/libraries/tapl-common/Android.bp
new file mode 100644
index 0000000..8bd0eeb
--- /dev/null
+++ b/libraries/tapl-common/Android.bp
@@ -0,0 +1,32 @@
+//
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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: "tapl-common",
+    libs: [
+        "androidx.test.uiautomator_uiautomator",
+        "androidx.test.runner",
+        "junit",
+        "uiautomator-helpers",
+    ],
+    srcs: [
+        "src/**/*.kt",
+    ],
+}
diff --git a/libraries/tapl-common/OWNERS b/libraries/tapl-common/OWNERS
new file mode 100644
index 0000000..657b229
--- /dev/null
+++ b/libraries/tapl-common/OWNERS
@@ -0,0 +1,2 @@
+vadimt@google.com
+nicomazz@google.com
diff --git a/libraries/tapl-common/TEST_MAPPING b/libraries/tapl-common/TEST_MAPPING
new file mode 100644
index 0000000..94b2533
--- /dev/null
+++ b/libraries/tapl-common/TEST_MAPPING
@@ -0,0 +1,8 @@
+{
+  "imports": [
+    {
+      "path": "vendor/google_testing/integration/tests/scenarios/src/android/platform/test/scenario/sysui"
+    }
+  ]
+}
+
diff --git a/libraries/tapl-common/src/android/platform/test/scenario/tapl_common/Gestures.kt b/libraries/tapl-common/src/android/platform/test/scenario/tapl_common/Gestures.kt
new file mode 100644
index 0000000..d28549d
--- /dev/null
+++ b/libraries/tapl-common/src/android/platform/test/scenario/tapl_common/Gestures.kt
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.tapl_common
+
+import android.platform.uiautomator_helpers.BetterSwipe
+import androidx.test.uiautomator.StaleObjectException
+import androidx.test.uiautomator.UiObject2
+import androidx.test.uiautomator.UiObject2Condition
+import androidx.test.uiautomator.Until
+import java.time.Duration
+import org.junit.Assert.assertTrue
+
+/**
+ * A collection of gestures for UI objects that implements flake-proof patterns and adds
+ * diagnostics. Don't use these gestures directly from the test, this class should be used only by
+ * TAPL.
+ */
+object Gestures {
+    private val WAIT_TIME = Duration.ofSeconds(10)
+
+    private fun waitForObjectCondition(
+        uiObject: UiObject2,
+        objectName: String,
+        condition: UiObject2Condition<Boolean>,
+        conditionName: String
+    ) {
+        assertTrue(
+            "UI object '$objectName' is not $conditionName.",
+            uiObject.wait(condition, WAIT_TIME.toMillis())
+        )
+    }
+
+    private fun waitForObjectEnabled(uiObject: UiObject2, objectName: String) {
+        waitForObjectCondition(uiObject, objectName, Until.enabled(true), "enabled")
+    }
+
+    private fun waitForObjectClickable(uiObject: UiObject2, waitReason: String) {
+        waitForObjectCondition(uiObject, waitReason, Until.clickable(true), "clickable")
+    }
+
+    private fun waitForObjectLongClickable(uiObject: UiObject2, waitReason: String) {
+        waitForObjectCondition(uiObject, waitReason, Until.longClickable(true), "long-clickable")
+    }
+
+    /**
+     * Wait for the object to become clickable and enabled, then clicks the object.
+     *
+     * @param [uiObject] The object to click
+     * @param [objectName] Name of the object for diags
+     */
+    @JvmStatic
+    fun click(uiObject: UiObject2, objectName: String) {
+        try {
+            waitForObjectEnabled(uiObject, objectName)
+            waitForObjectClickable(uiObject, objectName)
+            clickNow(uiObject)
+        } catch (e: StaleObjectException) {
+            throw AssertionError(
+                "UI object '$objectName' has disappeared from the screen during the click gesture.",
+                e
+            )
+        }
+    }
+
+    /** The result of [longClickDown]. The caller has to call the [up] method. */
+    class LongClick internal constructor(swipe: BetterSwipe.Swipe) {
+        private val mSwipe: BetterSwipe.Swipe = swipe
+
+        fun up() {
+            mSwipe.release()
+        }
+    }
+
+    /**
+     * Waits for the object to become long-clickable and enabled, then presses the object down.
+     *
+     * @param [uiObject] The object to click
+     * @param [objectName] Name of the object for diags
+     * @return the object with [LongClick.up] method that needs to be called.
+     */
+    @JvmStatic
+    fun longClickDown(uiObject: UiObject2, objectName: String): LongClick {
+        try {
+            waitForObjectEnabled(uiObject, objectName)
+            waitForObjectLongClickable(uiObject, objectName)
+            return LongClick(BetterSwipe.from(uiObject.visibleCenter))
+        } catch (e: StaleObjectException) {
+            throw AssertionError(
+                "UI object '$objectName' has disappeared from " +
+                    "the screen during the long click gesture.",
+                e
+            )
+        }
+    }
+
+    /**
+     * Click on the ui object right away without waiting for animation.
+     *
+     * [UiObject2.click] would wait for all animations finished before clicking. Not waiting for
+     * animations because in some scenarios there is a playing animations when the click is
+     * attempted.
+     */
+    private fun clickNow(uiObject: UiObject2) {
+        BetterSwipe.from(uiObject.visibleCenter).release()
+    }
+}
diff --git a/libraries/tapl-common/src/android/platform/test/scenario/tapl_common/TaplUiDevice.kt b/libraries/tapl-common/src/android/platform/test/scenario/tapl_common/TaplUiDevice.kt
new file mode 100644
index 0000000..48eeecb
--- /dev/null
+++ b/libraries/tapl-common/src/android/platform/test/scenario/tapl_common/TaplUiDevice.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.tapl_common
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.BySelector
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.Until
+import java.time.Duration
+
+/** Wrapper of UiDevice for finding TAPL UI objects and performing flake-free gestures. */
+object TaplUiDevice {
+    /**
+     * Waits for a UI object with a given selector. Fails if the object is not visible.
+     *
+     * @param [selector] Selector for the ui object.
+     * @param [objectName] Name of the object for diags
+     * @return The found UI object.
+     */
+    @JvmStatic
+    fun waitForObject(selector: BySelector, objectName: String): TaplUiObject {
+        val uiObject =
+            device.wait(Until.findObject(selector), WAIT_TIME.toMillis())
+                ?: throw AssertionError(
+                    "UI object '$objectName' is not visible; selector: $selector."
+                )
+        return TaplUiObject(uiObject, objectName)
+    }
+
+    internal val WAIT_TIME = Duration.ofSeconds(10)
+    private val device: UiDevice
+        get() = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+}
diff --git a/libraries/tapl-common/src/android/platform/test/scenario/tapl_common/TaplUiObject.kt b/libraries/tapl-common/src/android/platform/test/scenario/tapl_common/TaplUiObject.kt
new file mode 100644
index 0000000..694c75d
--- /dev/null
+++ b/libraries/tapl-common/src/android/platform/test/scenario/tapl_common/TaplUiObject.kt
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.tapl_common
+
+import android.graphics.Point
+import android.graphics.Rect
+import android.platform.uiautomator_helpers.BetterSwipe
+import android.platform.uiautomator_helpers.PRECISE_GESTURE_INTERPOLATOR
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.Direction
+import androidx.test.uiautomator.UiObject2
+import androidx.test.uiautomator.Until
+
+/**
+ * Ui object with diagnostic metadata and flake-free gestures.
+ * @param [uiObject] UI Automator object
+ * @param [name] Name of the object for diags
+ */
+class TaplUiObject constructor(val uiObject: UiObject2, private val name: String) {
+    // Margins used for gestures (avoids touching too close to the object's edge).
+    private var mMarginLeft = 5
+    private var mMarginTop = 5
+    private var mMarginRight = 5
+    private var mMarginBottom = 5
+
+    /** Sets the margins used for gestures in pixels. */
+    fun setGestureMargin(margin: Int) {
+        setGestureMargins(margin, margin, margin, margin)
+    }
+
+    /** Sets the margins used for gestures in pixels. */
+    fun setGestureMargins(left: Int, top: Int, right: Int, bottom: Int) {
+        mMarginLeft = left
+        mMarginTop = top
+        mMarginRight = right
+        mMarginBottom = bottom
+    }
+
+    /** Returns this object's visible bounds with the margins removed. */
+    private fun getVisibleBoundsForGestures(): Rect {
+        val ret: Rect = uiObject.visibleBounds
+        ret.left = ret.left + mMarginLeft
+        ret.top = ret.top + mMarginTop
+        ret.right = ret.right - mMarginRight
+        ret.bottom = ret.bottom - mMarginBottom
+        return ret
+    }
+
+    /** Wait for the object to become clickable and enabled, then clicks the object. */
+    fun click() {
+        Gestures.click(uiObject, name)
+    }
+
+    /**
+     * Waits for a child UI object with a given resource id. Fails if the object is not visible.
+     *
+     * @param [resourceId] Resource id.
+     * @param [childObjectName] Name of the object for diags.
+     * @return The found UI object.
+     */
+    fun waitForChildObject(childResourceId: String, childObjectName: String): TaplUiObject {
+        val selector = By.res(uiObject.applicationPackage, childResourceId)
+        val childObject =
+            uiObject.wait(Until.findObject(selector), TaplUiDevice.WAIT_TIME.toMillis())
+                ?: throw AssertionError(
+                    "UI object '$childObjectName' is not found in '$name'; selector: $selector."
+                )
+        return TaplUiObject(childObject, childObjectName)
+    }
+
+    /**
+     * Performs a horizontal or vertical swipe over an area.
+     *
+     * @param area The area to swipe over.
+     * @param direction The direction in which to swipe.
+     * @param percent The size of the swipe as a percentage of the total area.
+     */
+    private fun scrollRect(area: Rect, direction: Direction, percent: Float) {
+        val start: Point
+        val end: Point
+        when (direction) {
+            Direction.LEFT -> {
+                start = Point(area.right, area.centerY())
+                end = Point(area.right - (area.width() * percent).toInt(), area.centerY())
+            }
+            Direction.RIGHT -> {
+                start = Point(area.left, area.centerY())
+                end = Point(area.left + (area.width() * percent).toInt(), area.centerY())
+            }
+            Direction.UP -> {
+                start = Point(area.centerX(), area.bottom)
+                end = Point(area.centerX(), area.bottom - (area.height() * percent).toInt())
+            }
+            Direction.DOWN -> {
+                start = Point(area.centerX(), area.top)
+                end = Point(area.centerX(), area.top + (area.height() * percent).toInt())
+            }
+            else -> throw RuntimeException()
+        }
+
+        BetterSwipe.from(start).to(end, interpolator = PRECISE_GESTURE_INTERPOLATOR).release()
+    }
+
+    /**
+     * Performs a scroll gesture on this object.
+     *
+     * @param direction The direction in which to scroll.
+     * @param percent The distance to scroll as a percentage of this object's visible size.
+     */
+    fun scroll(direction: Direction, percent: Float) {
+        require(percent >= 0.0f) { "Percent must be greater than 0.0f" }
+        require(percent <= 1.0f) { "Percent must be less than 1.0f" }
+
+        // To scroll, we swipe in the opposite direction
+        val swipeDirection: Direction = Direction.reverse(direction)
+
+        // Scroll by performing repeated swipes
+        val bounds: Rect = getVisibleBoundsForGestures()
+        val segment = Math.min(percent, 1.0f)
+        scrollRect(bounds, swipeDirection, segment)
+    }
+}
diff --git a/libraries/uiautomator-helpers/Android.bp b/libraries/uiautomator-helpers/Android.bp
new file mode 100644
index 0000000..750486b
--- /dev/null
+++ b/libraries/uiautomator-helpers/Android.bp
@@ -0,0 +1,37 @@
+//
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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: "uiautomator-helpers",
+    static_libs: [
+        // This lib should have minimal dependencies, and never depend on sysui or launcher targets.
+        "androidx.test.uiautomator_uiautomator",
+        "androidx.test.runner",
+        "androidx.test.rules",
+        "truth-prebuilt",
+        "junit",
+    ],
+    srcs: [
+        // Note: this library should be kotlin first.
+        // Unless absolutely necessary, try not to add java files here.
+        "src/**/*.kt",
+    ],
+    sdk_version: "test_current",
+}
diff --git a/libraries/uiautomator-helpers/OWNERS b/libraries/uiautomator-helpers/OWNERS
new file mode 100644
index 0000000..aa88dfe
--- /dev/null
+++ b/libraries/uiautomator-helpers/OWNERS
@@ -0,0 +1,2 @@
+include platform/frameworks/base:/packages/SystemUI/OWNERS
+nicomazz@google.com
diff --git a/libraries/uiautomator-helpers/TEST_MAPPING b/libraries/uiautomator-helpers/TEST_MAPPING
new file mode 100644
index 0000000..94b2533
--- /dev/null
+++ b/libraries/uiautomator-helpers/TEST_MAPPING
@@ -0,0 +1,8 @@
+{
+  "imports": [
+    {
+      "path": "vendor/google_testing/integration/tests/scenarios/src/android/platform/test/scenario/sysui"
+    }
+  ]
+}
+
diff --git a/libraries/uiautomator-helpers/src/android/platform/uiautomator_helpers/BetterSwipe.kt b/libraries/uiautomator-helpers/src/android/platform/uiautomator_helpers/BetterSwipe.kt
new file mode 100644
index 0000000..42cc7c3
--- /dev/null
+++ b/libraries/uiautomator-helpers/src/android/platform/uiautomator_helpers/BetterSwipe.kt
@@ -0,0 +1,277 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.uiautomator_helpers
+
+import android.animation.TimeInterpolator
+import android.graphics.Point
+import android.graphics.PointF
+import android.os.SystemClock
+import android.os.SystemClock.sleep
+import android.util.Log
+import android.view.InputDevice
+import android.view.MotionEvent
+import android.view.MotionEvent.TOOL_TYPE_FINGER
+import android.view.animation.DecelerateInterpolator
+import android.view.animation.LinearInterpolator
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.google.common.truth.Truth.assertThat
+import java.time.Duration
+import java.time.temporal.ChronoUnit.MILLIS
+import java.util.concurrent.atomic.AtomicInteger
+
+private val DEFAULT_DURATION: Duration = Duration.of(500, MILLIS)
+private val GESTURE_STEP = Duration.of(16, MILLIS)
+
+/**
+ * Allows fine control of swipes on the screen.
+ *
+ * Guarantees that all touches are dispatched, as opposed to [UiDevice] APIs, that might lose
+ * touches in case of high load.
+ *
+ * It is possible to perform operation before the swipe finishes. Timestamp of touch events are set
+ * according to initial time and duration.
+ *
+ * Example usage:
+ * ```
+ * val swipe = BetterSwipe.from(startPoint).to(intermediatePoint)
+ *
+ * assertThat(someUiState).isTrue();
+ *
+ * swipe.to(anotherPoint).release()
+ * ```
+ */
+object BetterSwipe {
+
+    private val lastPointerId = AtomicInteger(0)
+
+    /** Starts a swipe from [start] at the current time. */
+    @JvmStatic fun from(start: PointF) = Swipe(start)
+
+    /** Starts a swipe from [start] at the current time. */
+    @JvmStatic fun from(start: Point) = Swipe(PointF(start.x.toFloat(), start.y.toFloat()))
+
+    /** Starts a swipe for each [starts] point at the current time. */
+    @JvmStatic fun from(vararg starts: PointF) = Swipes(*starts)
+
+    class Swipe internal constructor(start: PointF) {
+
+        private val downTime = SystemClock.uptimeMillis()
+        private val pointerId = lastPointerId.incrementAndGet()
+        private var lastPoint: PointF = start
+        private var lastTime: Long = downTime
+        private var released = false
+
+        init {
+            log("Touch $pointerId started at $start")
+            sendPointer(currentTime = downTime, action = MotionEvent.ACTION_DOWN, point = start)
+        }
+
+        /**
+         * Swipes from the current point to [end] in [duration] using [interpolator] for the gesture
+         * speed. Pass [FLING_GESTURE_INTERPOLATOR] for a fling-like gesture that may leave the
+         * surface moving by inertia. Don't use it to drag objects to a precisely specified
+         * position. [PRECISE_GESTURE_INTERPOLATOR] will result in a precise drag-like gesture not
+         * triggering inertia.
+         */
+        @JvmOverloads
+        fun to(
+            end: PointF,
+            duration: Duration = DEFAULT_DURATION,
+            interpolator: TimeInterpolator = FLING_GESTURE_INTERPOLATOR
+        ): Swipe {
+            throwIfReleased()
+            log(
+                "Swiping from $lastPoint to $end in $duration " +
+                    "using ${interpolator.javaClass.simpleName}"
+            )
+            lastTime = movePointer(duration = duration, from = lastPoint, to = end, interpolator)
+            lastPoint = end
+            return this
+        }
+
+        /**
+         * Swipes from the current point to [end] in [duration] using [interpolator] for the gesture
+         * speed. Pass [FLING_GESTURE_INTERPOLATOR] for a fling-like gesture that may leave the
+         * surface moving by inertia. Don't use it to drag objects to a precisely specified
+         * position. [PRECISE_GESTURE_INTERPOLATOR] will result in a precise drag-like gesture not
+         * triggering inertia.
+         */
+        @JvmOverloads
+        fun to(
+            end: Point,
+            duration: Duration = DEFAULT_DURATION,
+            interpolator: TimeInterpolator = FLING_GESTURE_INTERPOLATOR
+        ): Swipe {
+            return to(PointF(end.x.toFloat(), end.y.toFloat()), duration, interpolator)
+        }
+
+        /** Moves the pointer up, finishing the swipe. Further calls will result in an exception. */
+        @JvmOverloads
+        fun release(sync: Boolean = true) {
+            throwIfReleased()
+            log("Touch $pointerId released at $lastPoint")
+            sendPointer(
+                currentTime = lastTime,
+                action = MotionEvent.ACTION_UP,
+                point = lastPoint,
+                sync = sync
+            )
+            lastPointerId.decrementAndGet()
+            released = true
+        }
+
+        /** Moves the pointer by [delta], sending the event at [currentTime]. */
+        internal fun moveBy(delta: PointF, currentTime: Long, sync: Boolean) {
+            val targetPoint = PointF(lastPoint.x + delta.x, lastPoint.y + delta.y)
+            sendPointer(currentTime, MotionEvent.ACTION_MOVE, targetPoint, sync)
+            lastTime = currentTime
+            lastPoint = targetPoint
+        }
+
+        private fun throwIfReleased() {
+            check(!released) { "Trying to perform a swipe operation after pointer released" }
+        }
+
+        private fun sendPointer(
+            currentTime: Long,
+            action: Int,
+            point: PointF,
+            sync: Boolean = true
+        ) {
+            val event = getMotionEvent(downTime, currentTime, action, point, pointerId)
+            check(
+                getInstrumentation()
+                    .uiAutomation
+                    .injectInputEvent(event, sync, /* waitForAnimations= */ false)
+            ) { "Touch injection failed" }
+            event.recycle()
+        }
+
+        /** Returns the time when movement finished. */
+        private fun movePointer(
+            duration: Duration,
+            from: PointF,
+            to: PointF,
+            interpolator: TimeInterpolator
+        ): Long {
+            val stepTimeMs = GESTURE_STEP.toMillis()
+            val durationMs = duration.toMillis()
+            val steps = durationMs / stepTimeMs
+            val startTime = lastTime
+            var currentTime = lastTime
+            for (i in 0 until steps) {
+                sleep(stepTimeMs)
+                currentTime += stepTimeMs
+                val progress = interpolator.getInterpolation(i / (steps - 1f))
+                val point = from.lerp(progress, to)
+                sendPointer(currentTime, MotionEvent.ACTION_MOVE, point)
+            }
+            assertThat(currentTime).isEqualTo(startTime + stepTimeMs * steps)
+            return currentTime
+        }
+    }
+
+    /** Collection of swipes. This can be used to simulate multitouch. */
+    class Swipes internal constructor(vararg starts: PointF) {
+
+        private var lastTime: Long = SystemClock.uptimeMillis()
+        private val swipes: List<Swipe> = starts.map { Swipe(it) }
+
+        /** Moves all the swipes by [delta], in [duration] time with constant speed. */
+        fun moveBy(delta: PointF, duration: Duration = DEFAULT_DURATION): Swipes {
+            log("Moving ${swipes.size} touches by $delta")
+
+            val stepTimeMs = GESTURE_STEP.toMillis()
+            val durationMs = duration.toMillis()
+            val steps = durationMs / stepTimeMs
+            val startTime = lastTime
+            var currentTime = lastTime
+            val stepDelta = PointF(delta.x / steps, delta.y / steps)
+            (1..steps).forEach { _ ->
+                sleep(stepTimeMs)
+                currentTime += stepTimeMs
+                swipes.forEach { swipe ->
+                    // Sending the move events as not "sync". Otherwise the method waits for them
+                    // to be displatched. As here we're sending many that are supposed to happen at
+                    // the same time, we don't want the method to
+                    // wait after each single injection.
+                    swipe.moveBy(stepDelta, currentTime, sync = false)
+                }
+            }
+            assertThat(currentTime).isEqualTo(startTime + stepTimeMs * steps)
+            lastTime = currentTime
+            return this
+        }
+
+        /** Moves pointers up, finishing the swipe. Further calls will result in an exception. */
+        fun release() {
+            swipes.forEach { it.release(sync = false) }
+        }
+    }
+
+    private fun log(s: String) = Log.d("BetterSwipe", s)
+}
+
+private fun getMotionEvent(
+    downTime: Long,
+    eventTime: Long,
+    action: Int,
+    p: PointF,
+    pointerId: Int,
+): MotionEvent {
+    val properties =
+        MotionEvent.PointerProperties().apply {
+            id = pointerId
+            toolType = TOOL_TYPE_FINGER
+        }
+    val coordinates =
+        MotionEvent.PointerCoords().apply {
+            pressure = 1f
+            size = 1f
+            x = p.x
+            y = p.y
+        }
+    return MotionEvent.obtain(
+        /* downTime= */ downTime,
+        /* eventTime= */ eventTime,
+        /* action= */ action,
+        /* pointerCount= */ 1,
+        /* pointerProperties= */ arrayOf(properties),
+        /* pointerCoords= */ arrayOf(coordinates),
+        /* metaState= */ 0,
+        /* buttonState= */ 0,
+        /* xPrecision= */ 1.0f,
+        /* yPrecision= */ 1.0f,
+        /* deviceId= */ 0,
+        /* edgeFlags= */ 0,
+        /* source= */ InputDevice.SOURCE_TOUCHSCREEN,
+        /* flags= */ 0
+    )
+}
+
+private fun PointF.lerp(amount: Float, b: PointF) =
+    PointF(lerp(x, b.x, amount), lerp(y, b.y, amount))
+
+private fun lerp(start: Float, stop: Float, amount: Float): Float = start + (stop - start) * amount
+
+/**
+ * Interpolator for a fling-like gesture that may leave the surface moving by inertia. Don't use it
+ * to drag objects to a precisely specified position.
+ */
+val FLING_GESTURE_INTERPOLATOR = LinearInterpolator()
+
+/** Interpolator for a precise drag-like gesture not triggering inertia. */
+val PRECISE_GESTURE_INTERPOLATOR = DecelerateInterpolator()
diff --git a/libraries/uiautomator-helpers/src/android/platform/uiautomator_helpers/DeviceHelpers.kt b/libraries/uiautomator-helpers/src/android/platform/uiautomator_helpers/DeviceHelpers.kt
new file mode 100644
index 0000000..b475f08
--- /dev/null
+++ b/libraries/uiautomator-helpers/src/android/platform/uiautomator_helpers/DeviceHelpers.kt
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.uiautomator_helpers
+
+import android.animation.TimeInterpolator
+import android.app.Instrumentation
+import android.content.Context
+import android.graphics.PointF
+import android.os.Bundle
+import android.platform.uiautomator_helpers.TracingUtils.trace
+import android.platform.uiautomator_helpers.WaitUtils.ensureThat
+import android.platform.uiautomator_helpers.WaitUtils.waitFor
+import android.platform.uiautomator_helpers.WaitUtils.waitForNullable
+import android.platform.uiautomator_helpers.WaitUtils.waitForValueToSettle
+import android.util.Log
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.BySelector
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.UiObject2
+import java.io.IOException
+import java.time.Duration
+
+private const val TAG = "DeviceHelpers"
+
+object DeviceHelpers {
+    private val SHORT_WAIT = Duration.ofMillis(1500)
+    private val LONG_WAIT = Duration.ofSeconds(10)
+    private val DOUBLE_TAP_INTERVAL = Duration.ofMillis(100)
+
+    private val instrumentationRegistry = InstrumentationRegistry.getInstrumentation()
+
+    @JvmStatic
+    val uiDevice: UiDevice
+        get() = UiDevice.getInstance(instrumentationRegistry)
+
+    @JvmStatic
+    val context: Context
+        get() = instrumentationRegistry.targetContext
+
+    /**
+     * Waits for an object to be visible and returns it.
+     *
+     * Throws an error with message provided by [errorProvider] if the object is not found.
+     */
+    fun UiDevice.waitForObj(
+        selector: BySelector,
+        timeout: Duration = SHORT_WAIT,
+        errorProvider: () -> String = { "Object $selector not found" },
+    ): UiObject2 = waitFor("$selector object", timeout, errorProvider) { findObject(selector) }
+
+    /**
+     * Waits for an object to be visible and returns it.
+     *
+     * Throws an error with message provided by [errorProvider] if the object is not found.
+     */
+    fun UiObject2.waitForObj(
+        selector: BySelector,
+        timeout: Duration = LONG_WAIT,
+        errorProvider: () -> String = { "Object $selector not found" },
+    ): UiObject2 = waitFor("$selector object", timeout, errorProvider) { findObject(selector) }
+
+    /**
+     * Waits for an object to be visible and returns it. Returns `null` if the object is not found.
+     */
+    fun UiDevice.waitForNullableObj(
+        selector: BySelector,
+        timeout: Duration = SHORT_WAIT,
+    ): UiObject2? = waitForNullable("nullable $selector objects", timeout) { findObject(selector) }
+
+    /**
+     * Waits for objects matched by [selector] to be visible and returns them. Returns `null` if no
+     * objects are found
+     */
+    fun UiDevice.waitForNullableObjects(
+        selector: BySelector,
+        timeout: Duration = SHORT_WAIT,
+    ): List<UiObject2>? = waitForNullable("$selector objects", timeout) { findObjects(selector) }
+
+    /**
+     * Asserts visibility of a [selector], waiting for [timeout] until visibility matches the
+     * expected.
+     *
+     * If [container] is provided, the object is searched only inside of it.
+     */
+    @JvmOverloads
+    @JvmStatic
+    fun UiDevice.assertVisibility(
+        selector: BySelector,
+        visible: Boolean = true,
+        timeout: Duration = LONG_WAIT,
+        customMessageProvider: (() -> String)? = null,
+    ) {
+        ensureThat(
+            "$selector is ${visible.asVisibilityBoolean()}",
+            timeout,
+            customMessageProvider
+        ) { hasObject(selector) == visible }
+    }
+
+    private fun Boolean.asVisibilityBoolean(): String =
+        when (this) {
+            true -> "visible"
+            false -> "invisible"
+        }
+
+    /**
+     * Asserts visibility of a [selector] inside this [UiObject2], waiting for [timeout] until
+     * visibility matches the expected.
+     */
+    fun UiObject2.assertVisibility(
+        selector: BySelector,
+        visible: Boolean,
+        timeout: Duration = LONG_WAIT,
+        customMessageProvider: (() -> String)? = null,
+    ) {
+        ensureThat(
+            "$selector is ${visible.asVisibilityBoolean()} inside $this",
+            timeout,
+            customMessageProvider
+        ) { hasObject(selector) == visible }
+    }
+
+    /** Asserts that a this selector is visible. Throws otherwise. */
+    fun BySelector.assertVisible(
+        timeout: Duration = LONG_WAIT,
+        customMessageProvider: (() -> String)? = null
+    ) {
+        uiDevice.assertVisibility(
+            selector = this,
+            visible = true,
+            timeout = timeout,
+            customMessageProvider = customMessageProvider
+        )
+    }
+    /** Asserts that a this selector is invisible. Throws otherwise. */
+    fun BySelector.assertInvisible(
+        timeout: Duration = LONG_WAIT,
+        customMessageProvider: (() -> String)? = null
+    ) {
+        uiDevice.assertVisibility(
+            selector = this,
+            visible = false,
+            timeout = timeout,
+            customMessageProvider = customMessageProvider
+        )
+    }
+
+    /**
+     * Executes a shell command on the device.
+     *
+     * Adds some logging. Throws [RuntimeException] In case of failures.
+     */
+    @JvmStatic
+    fun UiDevice.shell(command: String): String =
+        trace("Executing shell command: $command") {
+            Log.d(TAG, "Executing Shell Command: $command")
+            return try {
+                executeShellCommand(command)
+            } catch (e: IOException) {
+                Log.e(TAG, "IOException Occurred.", e)
+                throw RuntimeException(e)
+            }
+        }
+
+    /** Perform double tap at specified x and y position */
+    @JvmStatic
+    fun UiDevice.doubleTapAt(x: Int, y: Int) {
+        click(x, y)
+        Thread.sleep(DOUBLE_TAP_INTERVAL.toMillis())
+        click(x, y)
+    }
+
+    /**
+     * Aims at replacing [UiDevice.swipe].
+     *
+     * This should be used instead of [UiDevice.swipe] as it causes less flakiness. See
+     * [BetterSwipe].
+     */
+    @JvmStatic
+    fun UiDevice.betterSwipe(
+        startX: Int,
+        startY: Int,
+        endX: Int,
+        endY: Int,
+        interpolator: TimeInterpolator = FLING_GESTURE_INTERPOLATOR
+    ) {
+        trace("Swiping ($startX,$startY) -> ($endX,$endY)") {
+            BetterSwipe.from(PointF(startX.toFloat(), startY.toFloat()))
+                .to(PointF(endX.toFloat(), endY.toFloat()), interpolator = interpolator)
+                .release()
+        }
+    }
+
+    /** [message] will be visible to the terminal when using `am instrument`. */
+    fun printInstrumentationStatus(tag: String, message: String) {
+        val result =
+            Bundle().apply {
+                putString(Instrumentation.REPORT_KEY_STREAMRESULT, "[$tag]: $message")
+            }
+        instrumentationRegistry.sendStatus(/* resultCode= */ 0, result)
+    }
+
+    /**
+     * Returns whether the screen is on.
+     *
+     * As this uses [waitForValueToSettle], it is resilient to fast screen on/off happening.
+     */
+    @JvmStatic
+    val UiDevice.isScreenOnSettled: Boolean
+        get() = waitForValueToSettle("Screen on") { isScreenOn }
+}
diff --git a/libraries/uiautomator-helpers/src/android/platform/uiautomator_helpers/ShellPrivilege.kt b/libraries/uiautomator-helpers/src/android/platform/uiautomator_helpers/ShellPrivilege.kt
new file mode 100644
index 0000000..a77aa85
--- /dev/null
+++ b/libraries/uiautomator-helpers/src/android/platform/uiautomator_helpers/ShellPrivilege.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 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.uiautomator_helpers
+
+import android.content.pm.PackageManager
+import androidx.test.platform.app.InstrumentationRegistry
+
+/**
+ * Adopt shell permissions for the target context.
+ *
+ * @param[permissions] the permission to adopt. Adopt all available permission is it's empty.
+ */
+class ShellPrivilege(vararg permissions: String) : AutoCloseable {
+
+    private val instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val targetContext = instrumentation.targetContext
+    private val uiAutomation = instrumentation.uiAutomation
+    private var permissionsGranted = false
+
+    init {
+        permissionsGranted = grantMissingPermissions(*permissions)
+    }
+
+    /**
+     * @return[Boolean] True is there are any missing permission and we've successfully granted all
+     * of them.
+     */
+    private fun grantMissingPermissions(vararg permissions: String): Boolean {
+        if (permissions.isEmpty()) {
+            uiAutomation.adoptShellPermissionIdentity()
+            return true
+        }
+        val missingPermissions = permissions.filter { !it.isGranted() }.toTypedArray()
+        if (missingPermissions.isEmpty()) return false
+        uiAutomation.adoptShellPermissionIdentity(*missingPermissions)
+        return true
+    }
+
+    override fun close() {
+        if (permissionsGranted) instrumentation.uiAutomation.dropShellPermissionIdentity()
+        permissionsGranted = false
+    }
+
+    private fun String.isGranted(): Boolean =
+        targetContext.checkCallingPermission(this) == PackageManager.PERMISSION_GRANTED
+}
diff --git a/libraries/uiautomator-helpers/src/android/platform/uiautomator_helpers/TracingUtils.kt b/libraries/uiautomator-helpers/src/android/platform/uiautomator_helpers/TracingUtils.kt
new file mode 100644
index 0000000..678f82f
--- /dev/null
+++ b/libraries/uiautomator-helpers/src/android/platform/uiautomator_helpers/TracingUtils.kt
@@ -0,0 +1,27 @@
+package android.platform.uiautomator_helpers
+
+import android.os.Trace
+import android.util.Log
+
+/** Tracing utils until androidx tracing library is updated in the tree. */
+internal object TracingUtils {
+
+    // from frameworks/base/core/java/android/os/Trace.java MAX_SECTION_NAME_LEN.
+    private const val MAX_TRACE_NAME_LEN = 127
+    private const val TAG = "TracingUtils"
+
+    inline fun <T> trace(sectionName: String, block: () -> T): T {
+        Trace.beginSection(sectionName.shortenedIfNeeded())
+        try {
+            return block()
+        } finally {
+            Trace.endSection()
+        }
+    }
+
+    private fun String.shortenedIfNeeded(): String =
+        if (length > MAX_TRACE_NAME_LEN) {
+            Log.w(TAG, "Section name too long: \"$this\" (len=$length, max=$MAX_TRACE_NAME_LEN)")
+            substring(0, MAX_TRACE_NAME_LEN)
+        } else this
+}
diff --git a/libraries/uiautomator-helpers/src/android/platform/uiautomator_helpers/UiObjectUtils.kt b/libraries/uiautomator-helpers/src/android/platform/uiautomator_helpers/UiObjectUtils.kt
new file mode 100644
index 0000000..ca0ee27
--- /dev/null
+++ b/libraries/uiautomator-helpers/src/android/platform/uiautomator_helpers/UiObjectUtils.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.uiautomator_helpers
+
+import android.graphics.PointF
+import android.graphics.Rect
+import android.platform.uiautomator_helpers.DeviceHelpers.uiDevice
+import android.platform.uiautomator_helpers.WaitUtils.waitForValueToSettle
+import androidx.test.uiautomator.BySelector
+import androidx.test.uiautomator.Direction
+import androidx.test.uiautomator.UiObject2
+import com.google.common.truth.Truth.assertWithMessage
+
+/**
+ * Checks if view is on the right side by checking left bound is in the middle of the screen or in
+ * the right half of the screen.
+ */
+fun UiObject2.assertOnTheRightSide() {
+    assertWithMessage("${this.resourceName} should be on the right side")
+        .that(this.stableBounds.left >= uiDevice.displayWidth / 2)
+        .isTrue()
+}
+
+/**
+ * Checks if view is on the left side by checking right bound is in the middle of the screen or in
+ * the left half of the screen.
+ */
+fun UiObject2.assertOnTheLeftSide() {
+    assertWithMessage("${this.resourceName} should be on the left side")
+        .that(this.stableBounds.right <= uiDevice.displayWidth / 2)
+        .isTrue()
+}
+
+private val UiObject2.stableBounds: Rect
+    get() = waitForValueToSettle("${this.resourceName} bounds") { visibleBounds }
+
+private const val MAX_FIND_ELEMENT_ATTEMPT = 15
+
+/**
+ * Scrolls [this] in [direction] ([Direction.DOWN] by default) until finding [selector]. It returns
+ * the first object that matches [selector] or `null` if it's not found after
+ * [MAX_FIND_ELEMENT_ATTEMPT] scrolls.
+ *
+ * Uses [BetterSwipe] to perform the scroll.
+ */
+@JvmOverloads
+fun UiObject2.scrollUntilFound(
+    selector: BySelector,
+    direction: Direction = Direction.DOWN
+): UiObject2? {
+    val (from, to) = getPointsToScroll(direction)
+    (0 until MAX_FIND_ELEMENT_ATTEMPT).forEach { _ ->
+        val f = findObject(selector)
+        if (f != null) return f
+        BetterSwipe.from(from).to(to, interpolator = FLING_GESTURE_INTERPOLATOR).release()
+    }
+    return null
+}
+
+private data class Vector2F(val from: PointF, val to: PointF)
+
+private fun UiObject2.getPointsToScroll(direction: Direction): Vector2F {
+    return when (direction) {
+        Direction.DOWN -> {
+            Vector2F(
+                PointF(visibleBounds.exactCenterX(), visibleBounds.bottom.toFloat() - 1f),
+                PointF(visibleBounds.exactCenterX(), visibleBounds.top.toFloat() + 1f)
+            )
+        }
+        Direction.UP -> {
+            Vector2F(
+                PointF(visibleBounds.exactCenterX(), visibleBounds.top.toFloat() + 1f),
+                PointF(visibleBounds.exactCenterX(), visibleBounds.bottom.toFloat() - 1f)
+            )
+        }
+        Direction.LEFT -> {
+            Vector2F(
+                PointF(visibleBounds.left.toFloat() + 1f, visibleBounds.exactCenterY()),
+                PointF(visibleBounds.right.toFloat() - 1f, visibleBounds.exactCenterY())
+            )
+        }
+        Direction.RIGHT -> {
+            Vector2F(
+                PointF(visibleBounds.right.toFloat() - 1f, visibleBounds.exactCenterY()),
+                PointF(visibleBounds.left.toFloat() + 1f, visibleBounds.exactCenterY())
+            )
+        }
+    }
+}
diff --git a/libraries/uiautomator-helpers/src/android/platform/uiautomator_helpers/WaitUtils.kt b/libraries/uiautomator-helpers/src/android/platform/uiautomator_helpers/WaitUtils.kt
new file mode 100644
index 0000000..cd1c59c
--- /dev/null
+++ b/libraries/uiautomator-helpers/src/android/platform/uiautomator_helpers/WaitUtils.kt
@@ -0,0 +1,287 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 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.uiautomator_helpers
+
+import android.os.SystemClock.sleep
+import android.os.SystemClock.uptimeMillis
+import android.os.Trace
+import android.platform.uiautomator_helpers.TracingUtils.trace
+import android.platform.uiautomator_helpers.WaitUtils.LoggerImpl.Companion.withEventualLogging
+import android.util.Log
+import java.io.Closeable
+import java.time.Duration
+import java.time.Instant.now
+
+/**
+ * Collection of utilities to ensure a certain conditions is met.
+ *
+ * Those are meant to make tests more understandable from perfetto traces, and less flaky.
+ */
+object WaitUtils {
+    private val DEFAULT_DEADLINE = Duration.ofSeconds(10)
+    private val POLLING_WAIT = Duration.ofMillis(100)
+    private val DEFAULT_SETTLE_TIME = Duration.ofSeconds(3)
+    private const val TAG = "WaitUtils"
+    private const val VERBOSE = true
+
+    /**
+     * Ensures that [condition] succeeds within [timeout], or fails with [errorProvider] message.
+     *
+     * This also logs with atrace each iteration, and its entire execution. Those traces are then
+     * visible in perfetto. Note that logs are output only after the end of the method, all
+     * together.
+     *
+     * Example of usage:
+     * ```
+     * ensureThat("screen is on") { uiDevice.isScreenOn }
+     * ```
+     */
+    @JvmStatic
+    @JvmOverloads
+    fun ensureThat(
+        description: String? = null,
+        timeout: Duration = DEFAULT_DEADLINE,
+        errorProvider: (() -> String)? = null,
+        ignoreFailure: Boolean = false,
+        condition: () -> Boolean,
+    ) {
+        val traceName =
+            if (description != null) {
+                "Ensuring $description"
+            } else {
+                "ensure"
+            }
+        val errorProvider =
+            errorProvider
+                ?: { "Error ensuring that \"$description\" within ${timeout.toMillis()}ms" }
+        trace(traceName) {
+            val startTime = uptimeMillis()
+            val timeoutMs = timeout.toMillis()
+            Log.d(TAG, "Starting $traceName")
+            withEventualLogging(logTimeDelta = true) {
+                log(traceName)
+                var i = 1
+                while (uptimeMillis() < startTime + timeoutMs) {
+                    trace("iteration $i") {
+                        try {
+                            if (condition()) {
+                                log("[#$i] Condition true")
+                                return
+                            }
+                        } catch (t: Throwable) {
+                            log("[#$i] Condition failing with exception")
+                            throw RuntimeException("[#$i] iteration failed.", t)
+                        }
+
+                        log("[#$i] Condition false, might retry.")
+                        sleep(POLLING_WAIT.toMillis())
+                        i++
+                    }
+                }
+                log("[#$i] Condition has always been false. Failing.")
+                if (ignoreFailure) {
+                    Log.w(TAG, "Ignoring ensureThat failure: ${errorProvider()}")
+                } else {
+                    throw FailedEnsureException(errorProvider())
+                }
+            }
+        }
+    }
+
+    /**
+     * Same as [waitForNullableValueToSettle], but assumes that [supplier] return value is non-null.
+     */
+    @JvmStatic
+    @JvmOverloads
+    fun <T> waitForValueToSettle(
+        description: String? = null,
+        minimumSettleTime: Duration = DEFAULT_SETTLE_TIME,
+        timeout: Duration = DEFAULT_DEADLINE,
+        errorProvider: () -> String =
+            defaultWaitForSettleError(minimumSettleTime, description, timeout),
+        supplier: () -> T,
+    ): T {
+        return waitForNullableValueToSettle(
+            description,
+            minimumSettleTime,
+            timeout,
+            errorProvider,
+            supplier
+        )
+            ?: error(errorProvider())
+    }
+
+    /**
+     * Waits for [supplier] to return the same value for at least [minimumSettleTime].
+     *
+     * If the value changes, the timer gets restarted. Fails when reaching [timeoutMs]. The minimum
+     * running time of this method is [minimumSettleTime], in case the value is stable since the
+     * beginning.
+     *
+     * Fails if [supplier] throws an exception.
+     *
+     * Outputs atraces visible with perfetto.
+     *
+     * Example of usage:
+     * ```
+     * val screenOn = waitForValueToSettle("Screen on") { uiDevice.isScreenOn }
+     * ```
+     *
+     * Note: Prefer using [waitForValueToSettle] when [supplier] doesn't return a null value.
+     *
+     * @return the settled value. Throws if it doesn't settle.
+     */
+    @JvmStatic
+    @JvmOverloads
+    fun <T> waitForNullableValueToSettle(
+        description: String? = null,
+        minimumSettleTime: Duration = DEFAULT_SETTLE_TIME,
+        timeout: Duration = DEFAULT_DEADLINE,
+        errorProvider: () -> String =
+            defaultWaitForSettleError(minimumSettleTime, description, timeout),
+        supplier: () -> T?,
+    ): T? {
+        val prefix =
+            if (description != null) {
+                "Waiting for \"$description\" to settle"
+            } else {
+                "waitForValueToSettle"
+            }
+        val traceName =
+            prefix +
+                " (settleTime=${minimumSettleTime.toMillis()}ms, deadline=${timeout.toMillis()}ms)"
+        trace(traceName) {
+            Log.d(TAG, "Starting $traceName")
+            withEventualLogging(logTimeDelta = true) {
+                log(traceName)
+
+                val startTime = now()
+                var settledSince = startTime
+                var previousValue: T? = null
+                var previousValueSet = false
+                while (now().isBefore(startTime + timeout)) {
+                    val newValue =
+                        try {
+                            supplier()
+                        } catch (t: Throwable) {
+                            if (previousValueSet) {
+                                Trace.endSection()
+                            }
+                            log("Supplier has thrown an exception")
+                            throw RuntimeException(t)
+                        }
+                    val currentTime = now()
+                    if (previousValue != newValue || !previousValueSet) {
+                        log("value changed to $newValue")
+                        settledSince = currentTime
+                        if (previousValueSet) {
+                            Trace.endSection()
+                        }
+                        Trace.beginSection("New value: $newValue")
+                        previousValue = newValue
+                        previousValueSet = true
+                    } else if (now().isAfter(settledSince + minimumSettleTime)) {
+                        log("Got settled value. Returning \"$previousValue\"")
+                        Trace.endSection() // previousValue is guaranteed to be non-null.
+                        return previousValue
+                    }
+                    sleep(POLLING_WAIT.toMillis())
+                }
+                if (previousValueSet) {
+                    Trace.endSection()
+                }
+                error(errorProvider())
+            }
+        }
+    }
+
+    private fun defaultWaitForSettleError(
+        minimumSettleTime: Duration,
+        description: String?,
+        timeout: Duration
+    ): () -> String {
+        return {
+            "Error getting settled (${minimumSettleTime.toMillis()}) " +
+                "value for \"$description\" within ${timeout.toMillis()}."
+        }
+    }
+
+    /**
+     * Waits for [supplier] to return a non-null value within [timeout].
+     *
+     * Returns null after the timeout finished.
+     */
+    fun <T> waitForNullable(
+        description: String,
+        timeout: Duration = DEFAULT_DEADLINE,
+        supplier: () -> T?
+    ): T? {
+        var result: T? = null
+
+        ensureThat("Waiting for \"$description\"", timeout, ignoreFailure = true) {
+            result = supplier()
+            result != null
+        }
+        return result
+    }
+
+    /**
+     * Waits for [supplier] to return a non-null value within [timeout].
+     *
+     * Throws an exception with [errorProvider] provided message if [supplier] failed to produce a
+     * non-null value within [timeout].
+     */
+    fun <T> waitFor(
+        description: String,
+        timeout: Duration = DEFAULT_DEADLINE,
+        errorProvider: () -> String = {
+            "Didn't get a non-null value for \"$description\" within ${timeout.toMillis()}ms"
+        },
+        supplier: () -> T?
+    ): T = waitForNullable(description, timeout, supplier) ?: error(errorProvider())
+
+    /** Generic logging interface. */
+    private interface Logger {
+        fun log(s: String)
+    }
+
+    /** Logs all messages when closed. */
+    private class LoggerImpl private constructor(private val logTimeDelta: Boolean) :
+        Closeable, Logger {
+        private val logs = mutableListOf<String>()
+        private val startTime = uptimeMillis()
+
+        companion object {
+            /** Executes [block] and prints all logs at the end. */
+            inline fun <T> withEventualLogging(
+                logTimeDelta: Boolean = false,
+                block: Logger.() -> T
+            ): T = LoggerImpl(logTimeDelta).use { it.block() }
+        }
+
+        override fun log(s: String) {
+            logs += if (logTimeDelta) "+${uptimeMillis() - startTime}ms $s" else s
+        }
+
+        override fun close() {
+            if (VERBOSE) {
+                Log.d(TAG, logs.joinToString("\n"))
+            }
+        }
+    }
+}
+
+/** Exception thrown when [WaitUtils.ensureThat] fails. */
+class FailedEnsureException(message: String? = null) : IllegalStateException(message)
diff --git a/tests/automotive/health/mediacenter/src/android/platform/scenario/mediacenter/Scroll.java b/tests/automotive/health/mediacenter/src/android/platform/scenario/mediacenter/Scroll.java
new file mode 100644
index 0000000..6a8682f
--- /dev/null
+++ b/tests/automotive/health/mediacenter/src/android/platform/scenario/mediacenter/Scroll.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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;
+
+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;
+
+/** Scroll down and up in Media */
+@Scenario
+@RunWith(JUnit4.class)
+public class Scroll {
+    static HelperAccessor<IAutoMediaHelper> sHelper =
+            new HelperAccessor<>(IAutoMediaHelper.class);
+
+    @Test
+    public void testScrollDownAndUp() {
+        sHelper.get().scrollDownOnePage();
+        sHelper.get().scrollUpOnePage();
+    }
+}
diff --git a/tests/automotive/health/mediacenter/tests/src/android/platform/scenario/mediacenter/ScrollMicrobenchmark.java b/tests/automotive/health/mediacenter/tests/src/android/platform/scenario/mediacenter/ScrollMicrobenchmark.java
new file mode 100644
index 0000000..e214414
--- /dev/null
+++ b/tests/automotive/health/mediacenter/tests/src/android/platform/scenario/mediacenter/ScrollMicrobenchmark.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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;
+
+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();
+    }
+}