Merge commit '75c6421176188027b8ab877163921749b0c2b9cd' of sso://googleplex-android/platform/platform_testing into HEAD
Change-Id: I1be65b079497131d3bb5fa3cf59492a97f15b67b
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 790da94..faa1035 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
@@ -236,4 +236,11 @@
      * <p>presses the video name to play.
      */
     public void playYourVideo(String videoName);
+
+    /**
+     * Setup expectation: YouTube is in the PIP mode on launcher.
+     *
+     * @return true if pip mode in launhcer.
+     */
+    public boolean isYouTubePipModeOnLauncher();
 }
diff --git a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IChromeHelper.java b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IChromeHelper.java
index 1ff75ea..a87a611 100644
--- a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IChromeHelper.java
+++ b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IChromeHelper.java
@@ -263,4 +263,13 @@
     public default UiObject2 getWebPage() {
         throw new UnsupportedOperationException("Not yet implemented.");
     }
+
+    /**
+     * Setup expectation: Chrome was loading a web page.
+     *
+     * <p>Returns a boolean to state if current page is loaded.
+     */
+    public default boolean isWebPageLoaded() {
+        throw new UnsupportedOperationException("Not yet implemented.");
+    }
 }
diff --git a/libraries/automotive-helpers/app-grid-helper/src/android/platform/helpers/AppGridHelperImpl.java b/libraries/automotive-helpers/app-grid-helper/src/android/platform/helpers/AppGridHelperImpl.java
index 5e3f555..f21f1b9 100644
--- a/libraries/automotive-helpers/app-grid-helper/src/android/platform/helpers/AppGridHelperImpl.java
+++ b/libraries/automotive-helpers/app-grid-helper/src/android/platform/helpers/AppGridHelperImpl.java
@@ -19,21 +19,12 @@
 import android.os.SystemClock;
 import android.app.Instrumentation;
 import android.support.test.uiautomator.By;
-import android.support.test.uiautomator.BySelector;
 import android.support.test.uiautomator.UiObject2;
 
 public class AppGridHelperImpl extends AbstractAutoStandardAppHelper implements IAutoAppGridHelper {
 
-    private static final String PAGE_UP_BUTTON_ID = "page_up";
-    private static final String PAGE_DOWN_BUTTON_ID = "page_down";
-
     private static final int UI_RESPONSE_WAIT_MS = 5000;
 
-    private final BySelector PAGE_UP_BUTTON =
-            By.res(getApplicationConfig(AutoConfigConstants.APP_GRID_PACKAGE), PAGE_UP_BUTTON_ID);
-    private final BySelector PAGE_DOWN_BUTTON =
-            By.res(getApplicationConfig(AutoConfigConstants.APP_GRID_PACKAGE), PAGE_DOWN_BUTTON_ID);
-
     public AppGridHelperImpl(Instrumentation instr) {
         super(instr);
     }
@@ -98,7 +89,12 @@
     @Override
     public boolean isTop() {
         if (isAppInForeground()) {
-            UiObject2 pageUp = mDevice.findObject(PAGE_UP_BUTTON);
+            UiObject2 pageUp =
+                    findUiObject(
+                            getResourceFromConfig(
+                                    AutoConfigConstants.APP_GRID,
+                                    AutoConfigConstants.APP_GRID_VIEW,
+                                    AutoConfigConstants.UP_BUTTON));
             if (pageUp != null) {
                 return !pageUp.isEnabled();
             } else {
@@ -114,7 +110,12 @@
     @Override
     public boolean isBottom() {
         if (isAppInForeground()) {
-            UiObject2 pageDown = mDevice.findObject(PAGE_DOWN_BUTTON);
+            UiObject2 pageDown =
+                    findUiObject(
+                            getResourceFromConfig(
+                                    AutoConfigConstants.APP_GRID,
+                                    AutoConfigConstants.APP_GRID_VIEW,
+                                    AutoConfigConstants.DOWN_BUTTON));
             if (pageDown != null) {
                 return !pageDown.isEnabled();
             } else {
diff --git a/libraries/automotive-helpers/dial-app-helper/src/android/platform/helpers/DialHelperImpl.java b/libraries/automotive-helpers/dial-app-helper/src/android/platform/helpers/DialHelperImpl.java
index 02eda5d..1e1f33c 100644
--- a/libraries/automotive-helpers/dial-app-helper/src/android/platform/helpers/DialHelperImpl.java
+++ b/libraries/automotive-helpers/dial-app-helper/src/android/platform/helpers/DialHelperImpl.java
@@ -306,11 +306,11 @@
                         getResourceFromConfig(
                                 AutoConfigConstants.PHONE,
                                 AutoConfigConstants.IN_CALL_VIEW,
-                                AutoConfigConstants.DIALED_CONTACT_NUMBER));
+                                AutoConfigConstants.DIALED_CONTACT_TYPE));
         if (contactDetail != null) {
-            return contactDetail.getText();
+            return contactDetail.getText().trim();
         } else {
-            throw new UnknownUiException("Unable to find contact details.");
+            throw new UnknownUiException("Unable to find Contact Type on In Call Screen.");
         }
     }
 
@@ -585,7 +585,15 @@
         }
         char[] array = phoneNumber.toCharArray();
         for (char ch : array) {
-            UiObject2 numberButton = findUiObject(By.text(Character.toString(ch)));
+            UiObject2 numberButton =
+                    findUiObject(
+                            getResourceFromConfig(
+                                    AutoConfigConstants.PHONE,
+                                    AutoConfigConstants.DIAL_PAD_VIEW,
+                                    Character.toString(ch)));
+            if (numberButton == null) {
+                numberButton = findUiObject(By.text(Character.toString(ch)));
+            }
             if (numberButton == null) {
                 throw new UnknownUiException("Unable to find number" + phoneNumber);
             }
diff --git a/libraries/automotive-helpers/notifications-app-helper/src/android/platform/helpers/AutoNotificationHelperImpl.java b/libraries/automotive-helpers/notifications-app-helper/src/android/platform/helpers/AutoNotificationHelperImpl.java
index 2ac3b96..4fec003 100644
--- a/libraries/automotive-helpers/notifications-app-helper/src/android/platform/helpers/AutoNotificationHelperImpl.java
+++ b/libraries/automotive-helpers/notifications-app-helper/src/android/platform/helpers/AutoNotificationHelperImpl.java
@@ -153,4 +153,38 @@
                                 AutoConfigConstants.CLEAR_ALL_BUTTON));
         return clear_all_btn != null;
     }
+
+    @Override
+    public boolean scrollDownOnePage() {
+        UiObject2 notification_list =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.NOTIFICATIONS,
+                                AutoConfigConstants.EXPANDED_NOTIFICATIONS_SCREEN,
+                                AutoConfigConstants.NOTIFICATION_LIST));
+
+        if (notification_list == null) {
+            throw new RuntimeException("Unable to scroll through notifications");
+        }
+
+        notification_list.scroll(Direction.DOWN, 20, 300);
+        return true;
+    }
+
+    @Override
+    public boolean scrollUpOnePage() {
+        UiObject2 notification_list =
+                findUiObject(
+                        getResourceFromConfig(
+                                AutoConfigConstants.NOTIFICATIONS,
+                                AutoConfigConstants.EXPANDED_NOTIFICATIONS_SCREEN,
+                                AutoConfigConstants.NOTIFICATION_LIST));
+
+        if (notification_list == null) {
+            throw new RuntimeException("Unable to scroll through notifications");
+        }
+
+        notification_list.scroll(Direction.UP, 20, 300);
+        return true;
+    }
 }
diff --git a/libraries/automotive-helpers/settings-app-helper/src/android/platform/helpers/SettingHelperImpl.java b/libraries/automotive-helpers/settings-app-helper/src/android/platform/helpers/SettingHelperImpl.java
index c37c453..0ef19e4 100644
--- a/libraries/automotive-helpers/settings-app-helper/src/android/platform/helpers/SettingHelperImpl.java
+++ b/libraries/automotive-helpers/settings-app-helper/src/android/platform/helpers/SettingHelperImpl.java
@@ -84,7 +84,7 @@
     @Override
     public void openSetting(String setting) {
         openFullSettings();
-        openMenuWith(getSettingPath(setting));
+        findSettingMenuAndClick(setting);
         verifyAvailableOptions(setting);
     }
 
@@ -117,7 +117,7 @@
         if (settingMenu != null) {
             clickAndWaitForIdleScreen(settingMenu);
         } else {
-            throw new RuntimeException("Unable to find setting menu");
+            throw new RuntimeException("Unable to find setting menu: " + setting);
         }
     }
 
@@ -285,11 +285,12 @@
                                 AutoConfigConstants.SETTINGS,
                                 AutoConfigConstants.FULL_SETTINGS,
                                 AutoConfigConstants.SEARCH_RESULTS));
-        int numberOfResults = searchResults.getChildren().size();
+        int numberOfResults = searchResults.getChildren().get(0).getChildren().size();
         if (numberOfResults == 0) {
             throw new RuntimeException("No results found");
         }
-        clickAndWaitForIdleScreen(searchResults.getChildren().get(selectedIndex));
+        clickAndWaitForIdleScreen(
+                searchResults.getChildren().get(0).getChildren().get(selectedIndex));
         SystemClock.sleep(UI_RESPONSE_WAIT_MS);
 
         UiObject2 object = findUiObject(By.textContains(item));
@@ -348,11 +349,13 @@
     /** {@inheritDoc} */
     @Override
     public void openMenuWith(String... menuOptions) {
+        // Scroll and Find Subsettings
         for (String menu : menuOptions) {
             Pattern menuPattern = Pattern.compile(menu, Pattern.CASE_INSENSITIVE);
-            UiObject2 menuButton = scrollAndFindUiObject(By.text(menuPattern));
+            UiObject2 menuButton =
+                    scrollAndFindUiObject(By.text(menuPattern), getScrollScreenIndex());
             if (menuButton == null) {
-                throw new RuntimeException("Unable to find menu item");
+                throw new RuntimeException("Unable to find menu item: " + menu);
             }
             clickAndWaitForIdleScreen(menuButton);
             waitForIdle();
@@ -392,7 +395,7 @@
         Pattern menuPattern = Pattern.compile(menu, Pattern.CASE_INSENSITIVE);
         UiObject2 menuButton = scrollAndFindUiObject(By.text(menuPattern), index);
         if (menuButton == null) {
-            throw new RuntimeException("Unable to find menu item");
+            throw new RuntimeException("Unable to find menu item: " + menu);
         }
         return menuButton;
     }
@@ -473,4 +476,12 @@
                 ? DayNightMode.NIGHT_MODE
                 : DayNightMode.DAY_MODE;
     }
+
+    private int getScrollScreenIndex() {
+        int scrollScreenIndex = 0;
+        if (hasSplitScreenSettingsUI()) {
+            scrollScreenIndex = 1;
+        }
+        return scrollScreenIndex;
+    }
 }
diff --git a/libraries/automotive-helpers/settings-app-helper/src/android/platform/helpers/SettingsDateTimeHelperImpl.java b/libraries/automotive-helpers/settings-app-helper/src/android/platform/helpers/SettingsDateTimeHelperImpl.java
index 2d9e1c5..2385b71 100644
--- a/libraries/automotive-helpers/settings-app-helper/src/android/platform/helpers/SettingsDateTimeHelperImpl.java
+++ b/libraries/automotive-helpers/settings-app-helper/src/android/platform/helpers/SettingsDateTimeHelperImpl.java
@@ -32,11 +32,16 @@
 import java.time.Month;
 import java.util.List;
 import java.util.Locale;
+import java.util.Map;
+import java.util.HashMap;
+
+import android.util.Log;
 
 public class SettingsDateTimeHelperImpl extends AbstractAutoStandardAppHelper
         implements IAutoDateTimeSettingsHelper {
     private static final Locale LOCALE = Locale.ENGLISH;
     private static final TextStyle TEXT_STYLE = TextStyle.SHORT;
+    private static final String LOG_TAG = SettingsDateTimeHelperImpl.class.getSimpleName();
 
     public SettingsDateTimeHelperImpl(Instrumentation instr) {
         super(instr);
@@ -137,6 +142,9 @@
     /** {@inheritDoc} */
     @Override
     public void setTimeInTwelveHourFormat(int hour, int minute, boolean am) {
+        // Get current time
+        String currentTime = getTime();
+
         // check Automatic date & time switch is turned off
         UiObject2 autoDateTimeSwitchWidget = getAutoDateTimeSwitchWidget();
         UiObject2 autoDateTimeMenu =
@@ -167,15 +175,18 @@
         if (minute < 10) {
             minute_string = "0" + minute;
         }
-        setTime(2, minute_string);
-        setTime(0, String.valueOf(hour));
-        setTime(1, am_pm);
+        setTime(2, minute_string, currentTime);
+        setTime(0, String.valueOf(hour), currentTime);
+        setTime(1, am_pm, currentTime);
         pressBack();
     }
 
     /** {@inheritDoc} */
     @Override
     public void setTimeInTwentyFourHourFormat(int hour, int minute) {
+        // Get current time
+        String currentTime = getTime();
+
         // check Automatic date & time switch is turned off
         UiObject2 autoDateTimeSwitchWidget = getAutoDateTimeSwitchWidget();
         UiObject2 autoDateTimeMenu =
@@ -208,12 +219,12 @@
         if (hour < 10) {
             hour_string = "0" + hour;
         }
-        setTime(2, minute_string);
-        setTime(0, hour_string);
+        setTime(2, minute_string, currentTime);
+        setTime(0, hour_string, currentTime);
         pressBack();
     }
 
-    private void setTime(int index, String s) {
+    private void setTime(int index, String s, String currentTime) {
         UiSelector selector =
                 new UiSelector()
                         .className(
@@ -239,6 +250,30 @@
                 throw new RuntimeException(e);
             }
             if (curAM_PM.equals("PM")) scrollForwards = false;
+        } else if (index == 2) {
+            int currentMinute = Integer.parseInt(currentTime.split(":")[1].split("\\s+")[0]);
+            int setMinute = Integer.parseInt(s);
+
+            /* Set scrollForwards such that the minute is scrolled a max of 30 times */
+            if (currentMinute > setMinute) {
+                if (currentMinute - setMinute <= 30) scrollForwards = false;
+            } else if (setMinute > currentMinute) {
+                if (setMinute - currentMinute > 30) scrollForwards = false;
+            }
+        } else {
+            int currentHour = Integer.parseInt(currentTime.split(":")[0]);
+            int setHour = Integer.parseInt(s);
+
+            /* Set max scrolls based on whether we're in 12 or 24 hour format */
+            int maxScrolls =
+                    (currentTime.trim().endsWith("AM") || currentTime.trim().endsWith("PM")) ? 6 : 12;
+
+            /* Calculate forward or backward like we did for minutes */
+            if (currentHour > setHour) {
+                if (currentHour - setHour <= maxScrolls) scrollForwards = false;
+            } else if (setHour > currentHour) {
+                if (setHour - currentHour > maxScrolls) scrollForwards = false;
+            }
         }
         scrollToObjectInPicker(index, s, scrollForwards);
     }
@@ -262,26 +297,77 @@
                                                 AutoConfigConstants.SETTINGS,
                                                 AutoConfigConstants.DATE_AND_TIME_SETTINGS,
                                                 AutoConfigConstants.EDIT_TEXT_WIDGET)));
-        while (obj == null) {
+
+        /* For hour and minute, search by child object instead of text */
+        if (index == 0 || index == 2) {
+            UiSelector dayOrMonthSelector = selector.childSelector(
+                    new UiSelector().className(
+                            getResourceValue(
+                                    AutoConfigConstants.SETTINGS,
+                                    AutoConfigConstants.DATE_AND_TIME_SETTINGS,
+                                    AutoConfigConstants.EDIT_TEXT_WIDGET
+                            )
+                    )
+            );
+
+            /* Once we have the child selector, search for text within that selector */
+            String currentValue = "";
             try {
-                if (scrollForwards) {
-                    scrollable.scrollForward();
-                } else {
-                    scrollable.scrollBackward();
-                }
+                currentValue = new UiObject(dayOrMonthSelector).getText().trim();
             } catch (Exception e) {
                 throw new RuntimeException(e);
             }
-            obj =
-                    findUiObject(
-                            By.text(s)
-                                    .clazz(
-                                            getResourceValue(
-                                                    AutoConfigConstants.SETTINGS,
-                                                    AutoConfigConstants.DATE_AND_TIME_SETTINGS,
-                                                    AutoConfigConstants.EDIT_TEXT_WIDGET)));
+
+            while (!currentValue.equals(s.trim())) {
+                try {
+                    if (scrollForwards) {
+                        scrollable.scrollForward();
+                    } else {
+                        scrollable.scrollBackward();
+                    }
+                } catch (Exception e) {
+                    throw new RuntimeException(e);
+                }
+
+                dayOrMonthSelector = selector.childSelector(
+                        new UiSelector().className(
+                                getResourceValue(
+                                        AutoConfigConstants.SETTINGS,
+                                        AutoConfigConstants.DATE_AND_TIME_SETTINGS,
+                                        AutoConfigConstants.EDIT_TEXT_WIDGET
+                                )
+                        )
+                );
+
+                try {
+                    currentValue = new UiObject(dayOrMonthSelector).getText().trim();
+                } catch (Exception e) {
+                    throw new RuntimeException(e);
+                }
+            }
+        } else {
+            while (obj == null) {
+                try {
+                    if (scrollForwards) {
+                        scrollable.scrollForward();
+                    } else {
+                        scrollable.scrollBackward();
+                    }
+                } catch (Exception e) {
+                    throw new RuntimeException(e);
+                }
+                obj =
+                        findUiObject(
+                                By.text(s)
+                                        .clazz(
+                                                getResourceValue(
+                                                        AutoConfigConstants.SETTINGS,
+                                                        AutoConfigConstants.DATE_AND_TIME_SETTINGS,
+                                                        AutoConfigConstants.EDIT_TEXT_WIDGET)));
+            }
+
+            if (obj == null) throw new RuntimeException("cannot find value in the picker");
         }
-        if (obj == null) throw new RuntimeException("cannot find value in the picker");
     }
 
     /** {@inheritDoc} */
@@ -429,7 +515,7 @@
         BySelector selector = By.hasDescendant(bySelector);
         UiObject2 object = scrollAndFindUiObject(selector, getScrollScreenIndex());
         List<UiObject2> list = object.getParent().getChildren();
-        UiObject2 switchWidget = list.get(1).getChildren().get(0);
+        UiObject2 switchWidget = list.get(1).getChildren().get(0).getChildren().get(0);
         return switchWidget;
     }
 
diff --git a/libraries/automotive-helpers/settings-app-helper/src/android/platform/helpers/SettingsSecurityHelperImpl.java b/libraries/automotive-helpers/settings-app-helper/src/android/platform/helpers/SettingsSecurityHelperImpl.java
index 6d82330..cee79e1 100644
--- a/libraries/automotive-helpers/settings-app-helper/src/android/platform/helpers/SettingsSecurityHelperImpl.java
+++ b/libraries/automotive-helpers/settings-app-helper/src/android/platform/helpers/SettingsSecurityHelperImpl.java
@@ -2,7 +2,6 @@
 
 import android.app.Instrumentation;
 import android.support.test.uiautomator.By;
-import android.support.test.uiautomator.BySelector;
 import android.support.test.uiautomator.UiObject2;
 
 import java.util.List;
@@ -117,9 +116,19 @@
         int length = pin.length();
         for (int i = 0; i < length; i++) {
             char c = pin.charAt(i);
-            String numberText = "" + c;
-            BySelector number_selector = By.text(numberText);
-            UiObject2 number = findUiObject(number_selector);
+            UiObject2 number =
+                    findUiObject(
+                            getResourceFromConfig(
+                                    AutoConfigConstants.SETTINGS,
+                                    AutoConfigConstants.SECURITY_SETTINGS,
+                                    Character.toString(c)));
+            if (number == null) {
+                number = findUiObject(By.text(Character.toString(c)));
+            }
+            if (number == null) {
+                throw new RuntimeException(
+                        "Unable to find number on pin pad: " + Character.toString(c));
+            }
             clickAndWaitForWindowUpdate(
                     getApplicationConfig(AutoConfigConstants.SETTINGS_PACKAGE), number);
         }
diff --git a/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoAppGridConfigUtility.java b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoAppGridConfigUtility.java
index a77dc39..0f94f95 100644
--- a/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoAppGridConfigUtility.java
+++ b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoAppGridConfigUtility.java
@@ -168,6 +168,12 @@
                 AutoConfigConstants.APPLICATION_NAME,
                 new AutoConfigResource(
                         AutoConfigConstants.RESOURCE_ID, "app_name", APP_GRID_PACKAGE));
+        appGridViewConfiguration.addResource(
+                AutoConfigConstants.UP_BUTTON,
+                new AutoConfigResource(AutoConfigConstants.DESCRIPTION, "Scroll up"));
+        appGridViewConfiguration.addResource(
+                AutoConfigConstants.DOWN_BUTTON,
+                new AutoConfigResource(AutoConfigConstants.DESCRIPTION, "Scroll down"));
         mAppGridConfigMap.put(AutoConfigConstants.APP_GRID_VIEW, appGridViewConfiguration);
     }
 }
diff --git a/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoConfigConstants.java b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoConfigConstants.java
index a5cf1ed..8e530f4 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
@@ -174,6 +174,7 @@
     public static final String IN_CALL_VIEW = "IN_CALL_VIEW";
     public static final String DIALED_CONTACT_TITLE = "DIALED_CONTACT_TITLE";
     public static final String DIALED_CONTACT_NUMBER = "DIALED_CONTACT_NUMBER";
+    public static final String DIALED_CONTACT_TYPE = "DIALED_CONTACT_TYPE";
     public static final String END_CALL = "END_CALL";
     public static final String MUTE_CALL = "MUTE_CALL";
     public static final String SWITCH_TO_DIAL_PAD = "SWITCH_TO_DIAL_PAD";
@@ -187,6 +188,17 @@
     public static final String DIALED_NUMBER = "DIALED_NUMBER";
     public static final String MAKE_CALL = "MAKE_CALL";
     public static final String DELETE_NUMBER = "DELETE_NUMBER";
+    // Digit Constants Reused for Security PIN
+    public static final String DIGIT_ZERO = "0";
+    public static final String DIGIT_ONE = "1";
+    public static final String DIGIT_TWO = "2";
+    public static final String DIGIT_THREE = "3";
+    public static final String DIGIT_FOUR = "4";
+    public static final String DIGIT_FIVE = "5";
+    public static final String DIGIT_SIX = "6";
+    public static final String DIGIT_SEVEN = "7";
+    public static final String DIGIT_EIGHT = "8";
+    public static final String DIGIT_NINE = "9";
     // Contacts Screen
     public static final String CONTACTS_VIEW = "CONTACTS_VIEW";
     public static final String CONTACTS_MENU = "CONTACTS_MENU";
diff --git a/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoDialConfigUtility.java b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoDialConfigUtility.java
index 47bbc04..8fd8fef 100644
--- a/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoDialConfigUtility.java
+++ b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoDialConfigUtility.java
@@ -187,6 +187,12 @@
                         "user_profile_phone_number",
                         DIAL_APP_PACKAGE));
         inCallScreenConfiguration.addResource(
+                AutoConfigConstants.DIALED_CONTACT_TYPE,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID,
+                        "user_profile_phone_label",
+                        DIAL_APP_PACKAGE));
+        inCallScreenConfiguration.addResource(
                 AutoConfigConstants.END_CALL,
                 new AutoConfigResource(
                         AutoConfigConstants.RESOURCE_ID, "end_call_button", DIAL_APP_PACKAGE));
@@ -233,6 +239,36 @@
                 AutoConfigConstants.DELETE_NUMBER,
                 new AutoConfigResource(
                         AutoConfigConstants.RESOURCE_ID, "delete_button", DIAL_APP_PACKAGE));
+        dialPadScreenConfiguration.addResource(
+                AutoConfigConstants.DIGIT_ZERO,
+                new AutoConfigResource(AutoConfigConstants.RESOURCE_ID, "zero", DIAL_APP_PACKAGE));
+        dialPadScreenConfiguration.addResource(
+                AutoConfigConstants.DIGIT_ONE,
+                new AutoConfigResource(AutoConfigConstants.RESOURCE_ID, "one", DIAL_APP_PACKAGE));
+        dialPadScreenConfiguration.addResource(
+                AutoConfigConstants.DIGIT_TWO,
+                new AutoConfigResource(AutoConfigConstants.RESOURCE_ID, "two", DIAL_APP_PACKAGE));
+        dialPadScreenConfiguration.addResource(
+                AutoConfigConstants.DIGIT_THREE,
+                new AutoConfigResource(AutoConfigConstants.RESOURCE_ID, "three", DIAL_APP_PACKAGE));
+        dialPadScreenConfiguration.addResource(
+                AutoConfigConstants.DIGIT_FOUR,
+                new AutoConfigResource(AutoConfigConstants.RESOURCE_ID, "four", DIAL_APP_PACKAGE));
+        dialPadScreenConfiguration.addResource(
+                AutoConfigConstants.DIGIT_FIVE,
+                new AutoConfigResource(AutoConfigConstants.RESOURCE_ID, "five", DIAL_APP_PACKAGE));
+        dialPadScreenConfiguration.addResource(
+                AutoConfigConstants.DIGIT_SIX,
+                new AutoConfigResource(AutoConfigConstants.RESOURCE_ID, "six", DIAL_APP_PACKAGE));
+        dialPadScreenConfiguration.addResource(
+                AutoConfigConstants.DIGIT_SEVEN,
+                new AutoConfigResource(AutoConfigConstants.RESOURCE_ID, "seven", DIAL_APP_PACKAGE));
+        dialPadScreenConfiguration.addResource(
+                AutoConfigConstants.DIGIT_EIGHT,
+                new AutoConfigResource(AutoConfigConstants.RESOURCE_ID, "eight", DIAL_APP_PACKAGE));
+        dialPadScreenConfiguration.addResource(
+                AutoConfigConstants.DIGIT_NINE,
+                new AutoConfigResource(AutoConfigConstants.RESOURCE_ID, "nine", DIAL_APP_PACKAGE));
         mDialConfigMap.put(AutoConfigConstants.DIAL_PAD_VIEW, dialPadScreenConfiguration);
     }
 
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 a6fb5a1..6d0f043 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
@@ -275,7 +275,7 @@
                 AutoConfigConstants.SEARCH_RESULTS,
                 new AutoConfigResource(
                         AutoConfigConstants.RESOURCE_ID,
-                        "recycler_view",
+                        "car_ui_recycler_view",
                         SETTING_INTELLIGENCE_PACKAGE));
         fullSettingsConfiguration.addResource(
                 AutoConfigConstants.UP_BUTTON,
@@ -581,6 +581,46 @@
                 new AutoConfigResource(
                         AutoConfigConstants.RESOURCE_ID, "pin_pad", SETTING_APP_PACKAGE));
         securitySettingsConfiguration.addResource(
+                AutoConfigConstants.DIGIT_ZERO,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "key0", SETTING_APP_PACKAGE));
+        securitySettingsConfiguration.addResource(
+                AutoConfigConstants.DIGIT_ONE,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "key1", SETTING_APP_PACKAGE));
+        securitySettingsConfiguration.addResource(
+                AutoConfigConstants.DIGIT_TWO,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "key2", SETTING_APP_PACKAGE));
+        securitySettingsConfiguration.addResource(
+                AutoConfigConstants.DIGIT_THREE,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "key3", SETTING_APP_PACKAGE));
+        securitySettingsConfiguration.addResource(
+                AutoConfigConstants.DIGIT_FOUR,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "key4", SETTING_APP_PACKAGE));
+        securitySettingsConfiguration.addResource(
+                AutoConfigConstants.DIGIT_FIVE,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "key5", SETTING_APP_PACKAGE));
+        securitySettingsConfiguration.addResource(
+                AutoConfigConstants.DIGIT_SIX,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "key6", SETTING_APP_PACKAGE));
+        securitySettingsConfiguration.addResource(
+                AutoConfigConstants.DIGIT_SEVEN,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "key7", SETTING_APP_PACKAGE));
+        securitySettingsConfiguration.addResource(
+                AutoConfigConstants.DIGIT_EIGHT,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "key8", SETTING_APP_PACKAGE));
+        securitySettingsConfiguration.addResource(
+                AutoConfigConstants.DIGIT_NINE,
+                new AutoConfigResource(
+                        AutoConfigConstants.RESOURCE_ID, "key9", SETTING_APP_PACKAGE));
+        securitySettingsConfiguration.addResource(
                 AutoConfigConstants.ENTER_PIN_BUTTON,
                 new AutoConfigResource(
                         AutoConfigConstants.RESOURCE_ID, "key_enter", SETTING_APP_PACKAGE));
diff --git a/libraries/collectors-helper/statsd/src/com/android/helpers/StatsdStatsHelper.java b/libraries/collectors-helper/statsd/src/com/android/helpers/StatsdStatsHelper.java
index 4278494..b2a9405 100644
--- a/libraries/collectors-helper/statsd/src/com/android/helpers/StatsdStatsHelper.java
+++ b/libraries/collectors-helper/statsd/src/com/android/helpers/StatsdStatsHelper.java
@@ -99,11 +99,14 @@
     }
 
     private static void populateAtomStats(
-            StatsLog.StatsdStatsReport.AtomStats[] atomStats, Map<String, Long> resultMap) {
+            StatsLog.StatsdStatsReport.AtomStats[] stats, Map<String, Long> resultMap) {
         final String metricKeyPrefix =
                 MetricUtility.constructKey(STATSDSTATS_PREFIX, ATOM_STATS_PREFIX);
 
-        for (final StatsLog.StatsdStatsReport.AtomStats dataItem : atomStats) {
+        int summaryCount = 0;
+        int summaryErrorCount = 0;
+
+        for (final StatsLog.StatsdStatsReport.AtomStats dataItem : stats) {
             final String metricKeyPrefixWithTag =
                     MetricUtility.constructKey(metricKeyPrefix, String.valueOf(dataItem.tag));
 
@@ -113,7 +116,16 @@
             resultMap.put(
                     MetricUtility.constructKey(metricKeyPrefixWithTag, "error_count"),
                     Long.valueOf(dataItem.errorCount));
+
+            summaryCount += dataItem.count;
+            summaryErrorCount += dataItem.errorCount;
         }
+
+        resultMap.put(
+                MetricUtility.constructKey(metricKeyPrefix, "count"), Long.valueOf(summaryCount));
+        resultMap.put(
+                MetricUtility.constructKey(metricKeyPrefix, "error_count"),
+                Long.valueOf(summaryErrorCount));
     }
 
     private static void populateConfigStats(
@@ -138,71 +150,121 @@
                     MetricUtility.constructKey(metricKeyPrefixWithTag, "alert_count"),
                     Long.valueOf(dataItem.alertCount));
 
-            populateMatcherStats(dataItem.matcherStats, resultMap, metricKeyPrefixWithTag);
-            populateConditionStats(dataItem.conditionStats, resultMap, metricKeyPrefixWithTag);
-            populateMetricStats(dataItem.metricStats, resultMap, metricKeyPrefixWithTag);
-            populateAlertStats(dataItem.alertStats, resultMap, metricKeyPrefixWithTag);
+            populateMatcherStats(
+                    dataItem.matcherStats, resultMap, metricKeyPrefixWithTag, metricKeyPrefix);
+            populateConditionStats(
+                    dataItem.conditionStats, resultMap, metricKeyPrefixWithTag, metricKeyPrefix);
+            populateMetricStats(
+                    dataItem.metricStats, resultMap, metricKeyPrefixWithTag, metricKeyPrefix);
+            populateAlertStats(
+                    dataItem.alertStats, resultMap, metricKeyPrefixWithTag, metricKeyPrefix);
         }
     }
 
     private static void populateMetricStats(
             StatsLog.StatsdStatsReport.MetricStats[] stats,
             Map<String, Long> resultMap,
+            String metricKeyPrefixWithTag,
             String metricKeyPrefix) {
+        int summaryCount = 0;
+
         for (final StatsLog.StatsdStatsReport.MetricStats dataItem : stats) {
             final String metricKey =
                     MetricUtility.constructKey(
-                            metricKeyPrefix,
+                            metricKeyPrefixWithTag,
                             METRIC_STATS_PREFIX,
                             String.valueOf(dataItem.id),
                             "max_tuple_counts");
             resultMap.put(metricKey, Long.valueOf(dataItem.maxTupleCounts));
+
+            summaryCount += dataItem.maxTupleCounts;
         }
+
+        final String metricKey =
+                MetricUtility.constructKey(
+                        metricKeyPrefix, METRIC_STATS_PREFIX, "max_tuple_counts");
+        resultMap.put(metricKey, Long.valueOf(summaryCount));
     }
 
     private static void populateConditionStats(
             StatsLog.StatsdStatsReport.ConditionStats[] stats,
             Map<String, Long> resultMap,
+            String metricKeyPrefixWithTag,
             String metricKeyPrefix) {
+        int summaryCount = 0;
+
         for (final StatsLog.StatsdStatsReport.ConditionStats dataItem : stats) {
             final String metricKey =
                     MetricUtility.constructKey(
-                            metricKeyPrefix,
+                            metricKeyPrefixWithTag,
                             CONDITION_STATS_PREFIX,
                             String.valueOf(dataItem.id),
                             "max_tuple_counts");
             resultMap.put(metricKey, Long.valueOf(dataItem.maxTupleCounts));
+
+            summaryCount += dataItem.maxTupleCounts;
         }
+
+        final String metricKey =
+                MetricUtility.constructKey(
+                        metricKeyPrefix, CONDITION_STATS_PREFIX, "max_tuple_counts");
+        resultMap.put(metricKey, Long.valueOf(summaryCount));
     }
 
     private static void populateMatcherStats(
             StatsLog.StatsdStatsReport.MatcherStats[] stats,
             Map<String, Long> resultMap,
+            String metricKeyPrefixWithTag,
             String metricKeyPrefix) {
+        int summaryCount = 0;
+
         for (final StatsLog.StatsdStatsReport.MatcherStats dataItem : stats) {
             final String metricKey =
                     MetricUtility.constructKey(
+                            metricKeyPrefixWithTag,
+                            MATCHER_STATS_PREFIX,
+                            String.valueOf(dataItem.id),
+                            "matched_times");
+            resultMap.put(metricKey, Long.valueOf(dataItem.matchedTimes));
+
+            final String sumPerIdMetricKey =
+                    MetricUtility.constructKey(
                             metricKeyPrefix,
                             MATCHER_STATS_PREFIX,
                             String.valueOf(dataItem.id),
                             "matched_times");
-            resultMap.put(metricKey, Long.valueOf(dataItem.matchedTimes));
+            resultMap.merge(sumPerIdMetricKey, Long.valueOf(dataItem.matchedTimes), Long::sum);
+
+            summaryCount += dataItem.matchedTimes;
         }
+
+        final String metricKey =
+                MetricUtility.constructKey(metricKeyPrefix, MATCHER_STATS_PREFIX, "matched_times");
+        resultMap.merge(metricKey, Long.valueOf(summaryCount), Long::sum);
     }
 
     private static void populateAlertStats(
             StatsLog.StatsdStatsReport.AlertStats[] stats,
             Map<String, Long> resultMap,
+            String metricKeyPrefixWithTag,
             String metricKeyPrefix) {
+        int summaryCount = 0;
+
         for (final StatsLog.StatsdStatsReport.AlertStats dataItem : stats) {
             final String metricKey =
                     MetricUtility.constructKey(
-                            metricKeyPrefix,
+                            metricKeyPrefixWithTag,
                             ALERT_STATS_PREFIX,
                             String.valueOf(dataItem.id),
                             "alerted_times");
             resultMap.put(metricKey, Long.valueOf(dataItem.alertedTimes));
+
+            summaryCount += dataItem.alertedTimes;
         }
+
+        final String metricKey =
+                MetricUtility.constructKey(metricKeyPrefix, ALERT_STATS_PREFIX, "alerted_times");
+        resultMap.merge(metricKey, Long.valueOf(summaryCount), Long::sum);
     }
 
     private static void populateAnomalyAlarmStats(
@@ -218,12 +280,23 @@
     }
 
     private static void populatePulledAtomStats(
-            StatsLog.StatsdStatsReport.PulledAtomStats[] pulledAtomStats,
-            Map<String, Long> resultMap) {
+            StatsLog.StatsdStatsReport.PulledAtomStats[] stats, Map<String, Long> resultMap) {
         final String metricKeyPrefix =
                 MetricUtility.constructKey(STATSDSTATS_PREFIX, PULLED_ATOM_STATS_PREFIX);
 
-        for (final StatsLog.StatsdStatsReport.PulledAtomStats dataItem : pulledAtomStats) {
+        long summaryTotalPull = 0;
+        long summaryTotalPullFromCache = 0;
+        long summaryDataError = 0;
+        long summaryPullTimeout = 0;
+        long summaryExceedMaxDelay = 0;
+        long summaryFailed = 0;
+        long summaryEmptyData = 0;
+        long summaryAtomErrorCount = 0;
+        long summaryBinderCallFailed = 0;
+        long summaryFailedUIDProviderNotFound = 0;
+        long summaryPullerNotFound = 0;
+
+        for (final StatsLog.StatsdStatsReport.PulledAtomStats dataItem : stats) {
             final String metricKeyWithTag =
                     MetricUtility.constructKey(metricKeyPrefix, String.valueOf(dataItem.atomId));
             resultMap.put(
@@ -277,16 +350,61 @@
             resultMap.put(
                     MetricUtility.constructKey(metricKeyWithTag, "puller_not_found"),
                     Long.valueOf(dataItem.pullerNotFound));
+
+            summaryTotalPull += dataItem.totalPull;
+            summaryTotalPullFromCache += dataItem.totalPullFromCache;
+            summaryDataError += dataItem.dataError;
+            summaryPullTimeout += dataItem.pullTimeout;
+            summaryExceedMaxDelay += dataItem.pullExceedMaxDelay;
+            summaryFailed += dataItem.pullFailed;
+            summaryEmptyData += dataItem.emptyData;
+            summaryAtomErrorCount += dataItem.atomErrorCount;
+            summaryBinderCallFailed += dataItem.binderCallFailed;
+            summaryFailedUIDProviderNotFound += dataItem.failedUidProviderNotFound;
+            summaryPullerNotFound += dataItem.pullerNotFound;
         }
+
+        resultMap.put(MetricUtility.constructKey(metricKeyPrefix, "total_pull"), summaryTotalPull);
+        resultMap.put(
+                MetricUtility.constructKey(metricKeyPrefix, "total_pull_from_cache"),
+                summaryTotalPullFromCache);
+        resultMap.put(MetricUtility.constructKey(metricKeyPrefix, "data_error"), summaryDataError);
+        resultMap.put(
+                MetricUtility.constructKey(metricKeyPrefix, "pull_timeout"), summaryPullTimeout);
+        resultMap.put(
+                MetricUtility.constructKey(metricKeyPrefix, "pull_exceed_max_delay"),
+                summaryExceedMaxDelay);
+        resultMap.put(MetricUtility.constructKey(metricKeyPrefix, "pull_failed"), summaryFailed);
+        resultMap.put(MetricUtility.constructKey(metricKeyPrefix, "empty_data"), summaryEmptyData);
+        resultMap.put(
+                MetricUtility.constructKey(metricKeyPrefix, "atom_error_count"),
+                summaryAtomErrorCount);
+        resultMap.put(
+                MetricUtility.constructKey(metricKeyPrefix, "binder_call_failed"),
+                summaryBinderCallFailed);
+        resultMap.put(
+                MetricUtility.constructKey(metricKeyPrefix, "failed_uid_provider_not_found"),
+                summaryFailedUIDProviderNotFound);
+        resultMap.put(
+                MetricUtility.constructKey(metricKeyPrefix, "puller_not_found"),
+                summaryPullerNotFound);
     }
 
     private static void populateAtomMetricStats(
-            StatsLog.StatsdStatsReport.AtomMetricStats[] atomMetricStats,
-            Map<String, Long> resultMap) {
+            StatsLog.StatsdStatsReport.AtomMetricStats[] stats, Map<String, Long> resultMap) {
         final String metricKeyPrefix =
                 MetricUtility.constructKey(STATSDSTATS_PREFIX, ATOM_METRIC_STATS_PREFIX);
 
-        for (StatsLog.StatsdStatsReport.AtomMetricStats dataItem : atomMetricStats) {
+        long summaryHardDimensionLimitReached = 0;
+        long summaryLateLogEventSkipped = 0;
+        long summarySkippedForwardBuckets = 0;
+        long summaryBadValueType = 0;
+        long summaryConditionChangeInNextBucket = 0;
+        long summaryInvalidatedBucket = 0;
+        long summaryBucketDropped = 0;
+        long summaryBucketUnknownCondition = 0;
+
+        for (StatsLog.StatsdStatsReport.AtomMetricStats dataItem : stats) {
             final String metricKeyPrefixWithTag =
                     MetricUtility.constructKey(metricKeyPrefix, String.valueOf(dataItem.metricId));
 
@@ -327,7 +445,40 @@
             resultMap.put(
                     MetricUtility.constructKey(metricKeyPrefixWithTag, "bucket_count"),
                     dataItem.bucketCount);
+
+            summaryHardDimensionLimitReached += dataItem.hardDimensionLimitReached;
+            summaryLateLogEventSkipped += dataItem.lateLogEventSkipped;
+            summarySkippedForwardBuckets += dataItem.skippedForwardBuckets;
+            summaryBadValueType += dataItem.badValueType;
+            summaryConditionChangeInNextBucket += dataItem.conditionChangeInNextBucket;
+            summaryInvalidatedBucket += dataItem.invalidatedBucket;
+            summaryBucketDropped += dataItem.bucketDropped;
+            summaryBucketUnknownCondition += dataItem.bucketUnknownCondition;
         }
+
+        resultMap.put(
+                MetricUtility.constructKey(metricKeyPrefix, "hard_dimension_limit_reached"),
+                summaryHardDimensionLimitReached);
+        resultMap.put(
+                MetricUtility.constructKey(metricKeyPrefix, "late_log_event_skipped"),
+                summaryLateLogEventSkipped);
+        resultMap.put(
+                MetricUtility.constructKey(metricKeyPrefix, "skipped_forward_buckets"),
+                summarySkippedForwardBuckets);
+        resultMap.put(
+                MetricUtility.constructKey(metricKeyPrefix, "bad_value_type"), summaryBadValueType);
+        resultMap.put(
+                MetricUtility.constructKey(metricKeyPrefix, "condition_change_in_next_bucket"),
+                summaryConditionChangeInNextBucket);
+        resultMap.put(
+                MetricUtility.constructKey(metricKeyPrefix, "invalidated_bucket"),
+                summaryInvalidatedBucket);
+        resultMap.put(
+                MetricUtility.constructKey(metricKeyPrefix, "bucket_dropped"),
+                summaryBucketDropped);
+        resultMap.put(
+                MetricUtility.constructKey(metricKeyPrefix, "bucket_unknown_condition"),
+                summaryBucketUnknownCondition);
     }
 
     private static void populateDetectedLogLossStats(
diff --git a/libraries/collectors-helper/statsd/test/src/com/android/helpers/StatsdStatsHelperTest.java b/libraries/collectors-helper/statsd/test/src/com/android/helpers/StatsdStatsHelperTest.java
index d51f83d..1b40012 100644
--- a/libraries/collectors-helper/statsd/test/src/com/android/helpers/StatsdStatsHelperTest.java
+++ b/libraries/collectors-helper/statsd/test/src/com/android/helpers/StatsdStatsHelperTest.java
@@ -16,6 +16,7 @@
 package com.android.helpers;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
 import androidx.test.runner.AndroidJUnit4;
@@ -537,6 +538,34 @@
                 Long.valueOf(fieldValue++));
     }
 
+    private static void verifySummaryMetrics(Map<String, Long> result) {
+        final String metrics[] = {
+            "statsdstats_atom_stats_count",
+            "statsdstats_atom_stats_error_count",
+            "statsdstats_pulled_atom_stats_pull_failed",
+            "statsdstats_pulled_atom_stats_pull_timeout",
+            "statsdstats_pulled_atom_stats_pull_exceed_max_delay",
+            "statsdstats_pulled_atom_stats_empty_data",
+            "statsdstats_pulled_atom_stats_atom_error_count",
+            "statsdstats_pulled_atom_stats_binder_call_failed",
+            "statsdstats_pulled_atom_stats_failed_uid_provider_not_found",
+            "statsdstats_pulled_atom_stats_puller_not_found",
+            "statsdstats_pulled_atom_stats_total_pull",
+            "statsdstats_pulled_atom_stats_total_pull_from_cache",
+            "statsdstats_atom_metric_stats_hard_dimension_limit_reached",
+            "statsdstats_atom_metric_stats_late_log_event_skipped",
+            "statsdstats_atom_metric_stats_skipped_forward_buckets",
+            "statsdstats_atom_metric_stats_bad_value_type",
+            "statsdstats_atom_metric_stats_condition_change_in_next_bucket",
+            "statsdstats_atom_metric_stats_invalidated_bucket",
+            "statsdstats_atom_metric_stats_bucket_dropped",
+            "statsdstats_atom_metric_stats_bucket_unknown_condition"
+        };
+        for (int i = 0; i < metrics.length; i++) {
+            assertNotNull(result.get(metrics[i]));
+        }
+    }
+
     @Test
     public void testNonEmptyReport() throws Exception {
         StatsdStatsHelper.IStatsdHelper statsdHelper = new TestNonEmptyStatsdHelper();
@@ -557,6 +586,7 @@
         verifyAtomMetricStats(result, TestNonEmptyStatsdHelper.ATOM_METRIC_STATS_COUNT);
         verifyDetectedLogLossStats(result, TestNonEmptyStatsdHelper.DETECTED_LOG_LOSS_STATS_COUNT);
         verifyEventQueueOverfowStats(result);
+        verifySummaryMetrics(result);
         assertTrue(statsdStatsHelper.stopCollecting());
     }
 
@@ -567,7 +597,7 @@
 
         assertTrue(statsdStatsHelper.startCollecting());
         final Map<String, Long> result = statsdStatsHelper.getMetrics();
-        assertEquals(result.size(), 0);
+        verifySummaryMetrics(result);
         assertTrue(statsdStatsHelper.stopCollecting());
     }
 }
diff --git a/libraries/compatibility-common-util/Android.bp b/libraries/compatibility-common-util/Android.bp
index 5b58337..485a5f4 100644
--- a/libraries/compatibility-common-util/Android.bp
+++ b/libraries/compatibility-common-util/Android.bp
@@ -34,6 +34,7 @@
     visibility: [
         "//test/suite_harness/common/util",
         "//platform_testing/libraries/compatibility-common-util/tests",
+        "//platform_testing/libraries/sts-common-util/util",
     ],
     srcs: ["src/**/*.java"],
     host_supported: true,
diff --git a/libraries/device-collectors/src/main/java/android/device/collectors/BaseMetricListener.java b/libraries/device-collectors/src/main/java/android/device/collectors/BaseMetricListener.java
index fcd5963..9d4a132 100644
--- a/libraries/device-collectors/src/main/java/android/device/collectors/BaseMetricListener.java
+++ b/libraries/device-collectors/src/main/java/android/device/collectors/BaseMetricListener.java
@@ -101,6 +101,10 @@
     private int mCollectIterationInterval = 1;
     private int mSkipMetricUntilIteration = 0;
 
+    // Whether to report the results as instrumentation results. Used by metric collector rules,
+    // which do not have the information to invoke InstrumentationRunFinished() to report metrics.
+    private boolean mReportAsInstrumentationResults = false;
+
     public BaseMetricListener() {
         mIncludeFilters = new ArrayList<>();
         mExcludeFilters = new ArrayList<>();
@@ -190,9 +194,13 @@
             }
             if (mTestData.hasMetrics()) {
                 // Only send the status progress if there are metrics
+                if (mReportAsInstrumentationResults) {
+                    getInstrumentation().addResults(mTestData.createBundleFromMetrics());
+                } else {
                 SendToInstrumentation.sendBundle(getInstrumentation(),
                         mTestData.createBundleFromMetrics());
             }
+            }
         }
         super.testFinished(description);
     }
@@ -333,20 +341,33 @@
     }
 
     /**
+     * Create a directory inside external storage, and optionally empty it.
+     *
+     * @param dir full path to the dir to be created.
+     * @param empty whether to empty the new dirctory.
+     * @return directory file created
+     */
+    public File createDirectory(String dir, boolean empty) {
+        File rootDir = Environment.getExternalStorageDirectory();
+        File destDir = new File(rootDir, dir);
+        if (empty) {
+            executeCommandBlocking("rm -rf " + destDir.getAbsolutePath());
+        }
+        if (!destDir.exists() && !destDir.mkdirs()) {
+            Log.e(getTag(), "Unable to create dir: " + destDir.getAbsolutePath());
+            return null;
+        }
+        return destDir;
+    }
+
+    /**
      * Create a directory inside external storage, and empty it.
      *
      * @param dir full path to the dir to be created.
      * @return directory file created
      */
     public File createAndEmptyDirectory(String dir) {
-        File rootDir = Environment.getExternalStorageDirectory();
-        File destDir = new File(rootDir, dir);
-        executeCommandBlocking("rm -rf " + destDir.getAbsolutePath());
-        if (!destDir.exists() && !destDir.mkdirs()) {
-            Log.e(getTag(), "Unable to create dir: " + destDir.getAbsolutePath());
-            return null;
-        }
-        return destDir;
+        return createDirectory(dir, true);
     }
 
     /**
@@ -368,6 +389,11 @@
         }
     }
 
+    /** Sets whether metrics should be reported directly to instrumentation results. */
+    public final void setReportAsInstrumentationResults(boolean enabled) {
+        mReportAsInstrumentationResults = enabled;
+    }
+
     /**
      * Returns the name of the current class to be used as a logging tag.
      */
diff --git a/libraries/device-collectors/src/main/java/android/device/collectors/ScreenRecordCollector.java b/libraries/device-collectors/src/main/java/android/device/collectors/ScreenRecordCollector.java
index 895ddd1..da9c98c 100644
--- a/libraries/device-collectors/src/main/java/android/device/collectors/ScreenRecordCollector.java
+++ b/libraries/device-collectors/src/main/java/android/device/collectors/ScreenRecordCollector.java
@@ -18,6 +18,7 @@
 import static org.junit.Assert.assertNotNull;
 
 import android.device.collectors.annotations.OptionClass;
+import android.os.Bundle;
 import android.os.SystemClock;
 import android.util.Log;
 import androidx.annotation.VisibleForTesting;
@@ -41,6 +42,16 @@
  */
 @OptionClass(alias = "screen-record-collector")
 public class ScreenRecordCollector extends BaseMetricListener {
+    // Quality is relative to screen resolution.
+    // *  "medium" is 1/2 the resolution.
+    // *  "low" is 1/8 the resolution.
+    // *  Otherwise, use the resolution.
+    @VisibleForTesting static final String QUALITY_ARG = "video-quality";
+    // Option for whether to empty the output directory before collecting. Defaults to true. Setting
+    // to false is useful when multiple test classes need recordings and recordings are pulled at
+    // the end of the test run.
+    @VisibleForTesting static final String EMPTY_OUTPUT_DIR_ARG = "empty-output-dir";
+    // Maximum parts per test (each part is <= 3min).
     @VisibleForTesting static final int MAX_RECORDING_PARTS = 5;
     private static final long VIDEO_TAIL_BUFFER = 500;
 
@@ -51,13 +62,67 @@
 
     private RecordingThread mCurrentThread;
 
+    private String mVideoDimensions;
+    private boolean mEmptyOutputDir;
+
     // Tracks the test iterations to ensure that each failure gets unique filenames.
     // Key: test description; value: number of iterations.
     private Map<String, Integer> mTestIterations = new HashMap<String, Integer>();
 
+    public ScreenRecordCollector() {
+        super();
+    }
+
+    /** Constructors for overriding instrumentation arguments only. */
+    @VisibleForTesting
+    ScreenRecordCollector(Bundle args) {
+        super(args);
+    }
+
     @Override
-    public void onTestRunStart(DataRecord runData, Description description) {
-        mDestDir = createAndEmptyDirectory(OUTPUT_DIR);
+    public void onSetUp() {
+        mDestDir = createDirectory(OUTPUT_DIR, mEmptyOutputDir);
+    }
+
+    @Override
+    public void setupAdditionalArgs() {
+        mEmptyOutputDir =
+                Boolean.parseBoolean(
+                        getArgsBundle().getString(EMPTY_OUTPUT_DIR_ARG, String.valueOf(true)));
+
+        try {
+            long scaleDown = 1;
+            switch (getArgsBundle().getString(QUALITY_ARG, "default")) {
+                case "high":
+                    scaleDown = 1;
+                    break;
+
+                case "medium":
+                    scaleDown = 2;
+                    break;
+
+                case "low":
+                    scaleDown = 8;
+                    break;
+
+                default:
+                    return;
+            }
+
+            // Display metrics isn't the absolute size, so use "wm size".
+            String[] dims =
+                    getDevice()
+                            .executeShellCommand("wm size")
+                            .substring("Physical size: ".length())
+                            .trim()
+                            .split("x");
+            int width = Integer.parseInt(dims[0]);
+            int height = Integer.parseInt(dims[1]);
+            mVideoDimensions = String.format("%dx%d", width / scaleDown, height / scaleDown);
+            Log.v(getTag(), String.format("Using video dimensions: %s", mVideoDimensions));
+        } catch (Exception e) {
+            Log.e(getTag(), "Failed to query the device dimensions. Using default.", e);
+        }
     }
 
     @Override
@@ -110,18 +175,24 @@
 
     /** Returns the recording's name for part {@code part} of test {@code description}. */
     private File getOutputFile(Description description, int part) {
-        final String baseName =
-                String.format("%s.%s", description.getClassName(), description.getMethodName());
-        // Omit the iteration number for the first iteration.
+        StringBuilder builder = new StringBuilder(description.getClassName());
+        if (description.getMethodName() != null) {
+            builder.append(".");
+            builder.append(description.getMethodName());
+        }
         int iteration = mTestIterations.get(description.getDisplayName());
-        final String fileName =
-                String.format(
-                        "%s-video%s.mp4",
-                        iteration == 1
-                                ? baseName
-                                : String.join("-", baseName, String.valueOf(iteration)),
-                        part == 1 ? "" : part);
-        return Paths.get(mDestDir.getAbsolutePath(), fileName).toFile();
+        // Omit the iteration number for the first iteration.
+        if (iteration > 1) {
+            builder.append("-");
+            builder.append(iteration);
+        }
+        builder.append("-video");
+        // Omit the part number for the first part.
+        if (part > 1) {
+            builder.append(part);
+        }
+        builder.append(".mp4");
+        return Paths.get(mDestDir.getAbsolutePath(), builder.toString()).toFile();
     }
 
     /** Returns a buffer duration for the end of the video. */
@@ -167,9 +238,15 @@
                     // Make sure not to block on this background command in the main thread so
                     // that the test continues to run, but block in this thread so it does not
                     // trigger a new screen recording session before the prior one completes.
+                    String dimensionsOpt =
+                            mVideoDimensions == null
+                                    ? ""
+                                    : String.format("--size=%s", mVideoDimensions);
                     getDevice()
                             .executeShellCommand(
-                                    String.format("screenrecord %s", output.getAbsolutePath()));
+                                    String.format(
+                                            "screenrecord %s %s",
+                                            dimensionsOpt, output.getAbsolutePath()));
                 }
             } catch (IOException e) {
                 throw new RuntimeException("Caught exception while screen recording.");
diff --git a/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/droidfood/droidfood-run-level.pb b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/droidfood/droidfood-run-level.pb
index 752abda..89f31ab 100644
--- a/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/droidfood/droidfood-run-level.pb
+++ b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/droidfood/droidfood-run-level.pb
Binary files differ
diff --git a/libraries/device-collectors/src/test/java/android/device/collectors/BaseMetricListenerInstrumentedTest.java b/libraries/device-collectors/src/test/java/android/device/collectors/BaseMetricListenerInstrumentedTest.java
index 5e73d15..c7fe145 100644
--- a/libraries/device-collectors/src/test/java/android/device/collectors/BaseMetricListenerInstrumentedTest.java
+++ b/libraries/device-collectors/src/test/java/android/device/collectors/BaseMetricListenerInstrumentedTest.java
@@ -725,4 +725,34 @@
         assertEquals(RUN_END_VALUE, resultBundle.getString(RUN_END_KEY));
         assertEquals(2, resultBundle.size());
     }
+
+    /** Test that the report as instrumentation result option works. */
+    @MetricOption(group = "testGroup")
+    @Test
+    public void testReportAsInstrumentationResultsIfEnabled() throws Exception {
+        mListener.setReportAsInstrumentationResults(true);
+
+        Description runDescription = Description.createSuiteDescription("run");
+        mListener.testRunStarted(runDescription);
+        Description testDescription = Description.createTestDescription("class", "method");
+        mListener.testStarted(testDescription);
+        mListener.testFinished(testDescription);
+        mListener.testRunFinished(new Result());
+        // AJUR runner is then gonna call instrumentationRunFinished
+        Bundle resultBundle = new Bundle();
+        mListener.instrumentationRunFinished(System.out, resultBundle, new Result());
+
+        // Check that results are reported via Instrumentation.addResults().
+        ArgumentCaptor<Bundle> capture = ArgumentCaptor.forClass(Bundle.class);
+        Mockito.verify(mMockInstrumentation, Mockito.times(1)).addResults(capture.capture());
+        Bundle addedResult = capture.getValue();
+        assertTrue(addedResult.containsKey(TEST_END_KEY));
+        assertEquals(TEST_END_VALUE + "method", addedResult.getString(TEST_END_KEY));
+
+        // Rather than Instrumentation.sendStatus().
+        Mockito.verify(mMockInstrumentation, Mockito.never())
+                .sendStatus(
+                        Mockito.eq(SendToInstrumentation.INST_STATUS_IN_PROGRESS),
+                        Mockito.any(Bundle.class));
+    }
 }
diff --git a/libraries/device-collectors/src/test/java/android/device/collectors/ScreenRecordCollectorTest.java b/libraries/device-collectors/src/test/java/android/device/collectors/ScreenRecordCollectorTest.java
index 664a294..71162f4 100644
--- a/libraries/device-collectors/src/test/java/android/device/collectors/ScreenRecordCollectorTest.java
+++ b/libraries/device-collectors/src/test/java/android/device/collectors/ScreenRecordCollectorTest.java
@@ -16,13 +16,18 @@
 package android.device.collectors;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.AdditionalMatchers.not;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.endsWith;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.matches;
+import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -85,10 +90,15 @@
         }
     }
 
-    private ScreenRecordCollector initListener() throws IOException {
-        ScreenRecordCollector listener = spy(new ScreenRecordCollector());
+    private ScreenRecordCollector initListener(Bundle b) throws IOException {
+        ScreenRecordCollector listener;
+        if (b != null) {
+            listener = spy(new ScreenRecordCollector(b));
+        } else {
+            listener = spy(new ScreenRecordCollector());
+        }
         listener.setInstrumentation(mInstrumentation);
-        doReturn(mLogDir).when(listener).createAndEmptyDirectory(anyString());
+        doReturn(mLogDir).when(listener).createDirectory(anyString(), anyBoolean());
         doReturn(0L).when(listener).getTailBuffer();
         doReturn(mDevice).when(listener).getDevice();
         doReturn("1234").when(mDevice).executeShellCommand(eq("pidof screenrecord"));
@@ -102,11 +112,11 @@
      */
     @Test
     public void testScreenRecord() throws Exception {
-        mListener = initListener();
+        mListener = initListener(null);
 
         // Verify output directories are created on test run start.
         mListener.testRunStarted(mRunDesc);
-        verify(mListener).createAndEmptyDirectory(ScreenRecordCollector.OUTPUT_DIR);
+        verify(mListener).createDirectory(ScreenRecordCollector.OUTPUT_DIR, true);
 
         // Walk through a number of test cases to simulate behavior.
         for (int i = 1; i <= NUM_TEST_CASE; i++) {
@@ -149,7 +159,14 @@
         int videoCount = 0;
         for (Bundle bundle : capturedBundle) {
             for (String key : bundle.keySet()) {
-                if (key.contains("mp4")) videoCount++;
+                if (key.contains("mp4")) {
+                    videoCount++;
+                    assertTrue(key.contains(mTestDesc.getClassName()));
+                    assertTrue(key.contains(mTestDesc.getMethodName()));
+                    String fileName = bundle.getString(key);
+                    assertTrue(fileName.contains(mTestDesc.getClassName()));
+                    assertTrue(fileName.contains(mTestDesc.getMethodName()));
+                }
             }
         }
         assertEquals(NUM_TEST_CASE * ScreenRecordCollector.MAX_RECORDING_PARTS, videoCount);
@@ -158,7 +175,7 @@
     /** Test that screen recording is properly done for multiple tests and labels iterations. */
     @Test
     public void testScreenRecord_multipleTests() throws Exception {
-        mListener = initListener();
+        mListener = initListener(null);
 
         // Run through a sequence of `NUM_TEST_CASE` failing tests.
         mListener.testRunStarted(mRunDesc);
@@ -190,4 +207,141 @@
             }
         }
     }
+
+    /** Test that quality options (high) are respected by screen recordings. */
+    @Test
+    public void testScreenRecord_qualityHigh() throws Exception {
+        Bundle args = new Bundle();
+        args.putString(ScreenRecordCollector.QUALITY_ARG, "high");
+        mListener = initListener(args);
+        doReturn("Physical size: 1080x720 ").when(mDevice).executeShellCommand("wm size");
+
+        mListener.testRunStarted(mRunDesc);
+        mListener.testStarted(mTestDesc);
+
+        // Delay verification by 100 ms to ensure the thread was started.
+        SystemClock.sleep(100);
+        verify(mDevice).executeShellCommand(matches("screenrecord --size=1080x720 .*video.mp4"));
+    }
+
+    /** Test that quality options (medium) are respected by screen recordings. */
+    @Test
+    public void testScreenRecord_qualityMedium() throws Exception {
+        Bundle args = new Bundle();
+        args.putString(ScreenRecordCollector.QUALITY_ARG, "medium");
+        mListener = initListener(args);
+        doReturn("Physical size: 1080x720 ").when(mDevice).executeShellCommand("wm size");
+
+        mListener.testRunStarted(mRunDesc);
+        mListener.testStarted(mTestDesc);
+
+        // Delay verification by 100 ms to ensure the thread was started.
+        SystemClock.sleep(100);
+        verify(mDevice).executeShellCommand(matches("screenrecord --size=540x360 .*video.mp4"));
+    }
+
+    /** Test that quality options (low) are respected by screen recordings. */
+    @Test
+    public void testScreenRecord_qualityLow() throws Exception {
+        Bundle args = new Bundle();
+        args.putString(ScreenRecordCollector.QUALITY_ARG, "low");
+        mListener = initListener(args);
+        doReturn("Physical size: 1080x720 ").when(mDevice).executeShellCommand("wm size");
+
+        mListener.testRunStarted(mRunDesc);
+        mListener.testStarted(mTestDesc);
+
+        // Delay verification by 100 ms to ensure the thread was started.
+        SystemClock.sleep(100);
+        verify(mDevice).executeShellCommand(matches("screenrecord --size=135x90 .*video.mp4"));
+    }
+
+    /** Test that quality options (invalid) defaults to 1x. */
+    @Test
+    public void testScreenRecord_qualityUnknown() throws Exception {
+        Bundle args = new Bundle();
+        args.putString(ScreenRecordCollector.QUALITY_ARG, "other");
+        mListener = initListener(args);
+
+        mListener.testRunStarted(mRunDesc);
+        mListener.testStarted(mTestDesc);
+
+        // Delay verification by 100 ms to ensure the thread was started.
+        SystemClock.sleep(100);
+        verify(mDevice, never()).executeShellCommand(matches("screenrecord.*size.*video.mp4"));
+        verify(mDevice, atLeastOnce())
+                .executeShellCommand(not(matches("screenrecord .*video.mp4")));
+    }
+
+    /** Test that unexpected wm size contents defaults to unspecified size/quality option. */
+    @Test
+    public void testScreenRecord_dimensionsInvalid() throws Exception {
+        Bundle args = new Bundle();
+        args.putString(ScreenRecordCollector.QUALITY_ARG, "high");
+        mListener = initListener(args);
+        doReturn("Physical size: axb ").when(mDevice).executeShellCommand("wm size");
+
+        mListener.testRunStarted(mRunDesc);
+        mListener.testStarted(mTestDesc);
+
+        // Delay verification by 100 ms to ensure the thread was started.
+        SystemClock.sleep(100);
+        verify(mDevice, never()).executeShellCommand(matches("screenrecord.*size.*video.mp4"));
+        verify(mDevice, atLeastOnce())
+                .executeShellCommand(not(matches("screenrecord .*video.mp4")));
+    }
+
+    /** Test that the empty-output-dir works. */
+    @Test
+    public void testEmptyrOutputDirOptionSetToFalse() throws Exception {
+        Bundle args = new Bundle();
+        args.putString(ScreenRecordCollector.EMPTY_OUTPUT_DIR_ARG, "false");
+        mListener = initListener(args);
+
+        // Verify output directories are created on test run start.
+        mListener.testRunStarted(mRunDesc);
+        verify(mListener).createDirectory(ScreenRecordCollector.OUTPUT_DIR, false);
+    }
+
+    /**
+     * Test that descriptions with null method names only result in class names in the video file
+     * names.
+     */
+    @Test
+    public void testNullMethodNameDoesNotAppearInVideoName() throws Exception {
+        mListener = initListener(null);
+
+        mListener.testRunStarted(mRunDesc);
+
+        // mRunDesc does not have a method name.
+        mListener.testStarted(mRunDesc);
+        // Delay verification by 100 ms to ensure the thread was started.
+        SystemClock.sleep(100);
+        mListener.testFinished(mRunDesc);
+        mListener.testRunFinished(new Result());
+
+        Bundle resultBundle = new Bundle();
+        mListener.instrumentationRunFinished(System.out, resultBundle, new Result());
+
+        ArgumentCaptor<Bundle> capture = ArgumentCaptor.forClass(Bundle.class);
+        Mockito.verify(mInstrumentation, times(1))
+                .sendStatus(
+                        Mockito.eq(SendToInstrumentation.INST_STATUS_IN_PROGRESS),
+                        capture.capture());
+        Bundle metrics = capture.getValue();
+        // Ensure that we have recordings, and none of them have "null" in their file name or metric
+        // key.
+        boolean hasRecordings = false;
+        for (String key : metrics.keySet()) {
+            if (key.startsWith(mListener.getTag())) {
+                hasRecordings = true;
+                assertTrue(key.contains(mRunDesc.getClassName()));
+                assertFalse(key.contains("null"));
+                String fileName = metrics.getString(key);
+                assertTrue(fileName.contains(mRunDesc.getClassName()));
+                assertFalse(fileName.contains("null"));
+            }
+        }
+        assertTrue(hasRecordings);
+    }
 }
diff --git a/libraries/health/rules/src/android/platform/test/rule/ClassMetricRule.java b/libraries/health/rules/src/android/platform/test/rule/ClassMetricRule.java
new file mode 100644
index 0000000..2a2ff50
--- /dev/null
+++ b/libraries/health/rules/src/android/platform/test/rule/ClassMetricRule.java
@@ -0,0 +1,67 @@
+/*
+ * 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.rule;
+
+import android.app.Instrumentation;
+import android.device.collectors.BaseMetricListener;
+import android.os.Bundle;
+import androidx.annotation.VisibleForTesting;
+import androidx.test.InstrumentationRegistry;
+
+/**
+ * A rule that collects class-level metrics using a supplied list of metric collectors.
+ *
+ * <p>The metric collectors are passed in using the "class-metric-collectors" option, and the rule
+ * works by invoking the correct test-level callbacks on them at the corresponding stages of the
+ * test lifecycle. The metric collectors must be subclasses of {@link BaseMetricListener}, and can
+ * be passed in by their fully qualified class name, or simple class name if they are under the
+ * {@code android.device.collectors} package (but not subpackages).
+ *
+ * <p>Multiple metric collectors are supported as comma-separated values, The order they are
+ * triggered follows this example: for {@code -e class-metric-collectors Collector1,Collector2}, the
+ * evaluation order would be {@code Collector1#testStarted()}, {@code Collector2#testStarted()}, the
+ * entire test class, {@code Collector1#testFinished()}, {@code Collector1#testFinished()}.
+ *
+ * <p>For {@code Microbenchmark}s, this rule can be dynamically injected either inside or outside
+ * hardcoded rules (see {@code Microbenchmark})'s JavaDoc).
+ *
+ * <p>Note that metrics collected from this rule are reported as run metrics. Therefore, there is
+ * the risk of metric key collision if a run contains multiple classes that report metrics under the
+ * same key. At the moment, it's the responsibility of the metric collector to prevent collision
+ * across test classes.
+ *
+ * <p>Exceptions from metric listeners are silently logged. This behavior is in accordance with the
+ * approach taken by {@link BaseMetricListener}.
+ */
+public class ClassMetricRule extends TestMetricRule {
+    @VisibleForTesting static final String METRIC_COLLECTORS_OPTION = "class-metric-collectors";
+
+    public ClassMetricRule() {
+        this(InstrumentationRegistry.getArguments(), InstrumentationRegistry.getInstrumentation());
+    }
+
+    @VisibleForTesting
+    ClassMetricRule(Bundle args, Instrumentation instrumentation) {
+        super(
+                args,
+                instrumentation,
+                METRIC_COLLECTORS_OPTION,
+                ClassMetricRule.class.getSimpleName());
+        for (BaseMetricListener listener : mMetricListeners) {
+            listener.setReportAsInstrumentationResults(true);
+        }
+    }
+}
diff --git a/libraries/health/rules/src/android/platform/test/rule/TestMetricRule.java b/libraries/health/rules/src/android/platform/test/rule/TestMetricRule.java
index 3989728..2098a4f 100644
--- a/libraries/health/rules/src/android/platform/test/rule/TestMetricRule.java
+++ b/libraries/health/rules/src/android/platform/test/rule/TestMetricRule.java
@@ -15,6 +15,7 @@
  */
 package android.platform.test.rule;
 
+import android.app.Instrumentation;
 import android.device.collectors.BaseMetricListener;
 import android.os.Bundle;
 import android.util.Log;
@@ -50,12 +51,12 @@
  * approach taken by {@link BaseMetricListener}.
  */
 public class TestMetricRule extends TestWatcher {
-    private static final String LOG_TAG = TestMetricRule.class.getSimpleName();
-
     @VisibleForTesting static final String METRIC_COLLECTORS_OPTION = "test-metric-collectors";
     @VisibleForTesting static final String METRIC_COLLECTORS_PACKAGE = "android.device.collectors";
 
-    private List<BaseMetricListener> mMetricListeners = new ArrayList<>();
+    protected List<BaseMetricListener> mMetricListeners = new ArrayList<>();
+
+    private final String mLogTag;
 
     public TestMetricRule() {
         this(InstrumentationRegistry.getArguments());
@@ -63,8 +64,25 @@
 
     @VisibleForTesting
     TestMetricRule(Bundle args) {
+        this(
+                args,
+                InstrumentationRegistry.getInstrumentation(),
+                METRIC_COLLECTORS_OPTION,
+                TestMetricRule.class.getSimpleName());
+    }
+
+    /**
+     * A constructor that allows subclasses to change out various components used at initialization
+     * time.
+     */
+    protected TestMetricRule(
+            Bundle args,
+            Instrumentation instrumentation,
+            String collectorsOptionName,
+            String logTag) {
+        mLogTag = logTag;
         List<String> listenerNames =
-                Arrays.asList(args.getString(METRIC_COLLECTORS_OPTION, "").split(","));
+                Arrays.asList(args.getString(collectorsOptionName, "").split(","));
         for (String listenerName : listenerNames) {
             if (listenerName.isEmpty()) {
                 continue;
@@ -73,7 +91,7 @@
             // We could use a regex here, but this is simpler and should work just as well.
             if (listenerName.contains(".")) {
                 Log.i(
-                        LOG_TAG,
+                        mLogTag,
                         String.format(
                                 "Attempting to dynamically load metric collector with fully "
                                         + "qualified name %s.",
@@ -91,7 +109,7 @@
             } else {
                 String fullName = String.format("%s.%s", METRIC_COLLECTORS_PACKAGE, listenerName);
                 Log.i(
-                        LOG_TAG,
+                        mLogTag,
                         String.format(
                                 "Attempting to dynamically load metric collector with simple class "
                                         + "name %s (fully qualified name: %s).",
@@ -111,19 +129,21 @@
         }
         // Initialize each listener.
         for (BaseMetricListener listener : mMetricListeners) {
-            listener.setInstrumentation(InstrumentationRegistry.getInstrumentation());
-            listener.setupAdditionalArgs();
+            listener.setInstrumentation(instrumentation);
         }
     }
 
     @Override
     protected void starting(Description description) {
         for (BaseMetricListener listener : mMetricListeners) {
+            listener.setUp();
+        }
+        for (BaseMetricListener listener : mMetricListeners) {
             try {
                 listener.testStarted(description);
             } catch (Exception e) {
                 Log.e(
-                        LOG_TAG,
+                        mLogTag,
                         String.format(
                                 "Exception from listener %s during starting().",
                                 listener.getClass().getCanonicalName()),
@@ -139,13 +159,16 @@
                 listener.testFinished(description);
             } catch (Exception e) {
                 Log.e(
-                        LOG_TAG,
+                        mLogTag,
                         String.format(
                                 "Exception from listener %s during finished().",
                                 listener.getClass().getCanonicalName()),
                         e);
             }
         }
+        for (BaseMetricListener listener : mMetricListeners) {
+            listener.cleanUp();
+        }
     }
 
     @Override
@@ -156,7 +179,7 @@
                 listener.testFailure(failure);
             } catch (Exception e) {
                 Log.e(
-                        LOG_TAG,
+                        mLogTag,
                         String.format(
                                 "Exception from listener %s during failed().",
                                 listener.getClass().getCanonicalName()),
diff --git a/libraries/health/rules/tests/src/android/platform/test/rule/ClassMetricRuleTest.java b/libraries/health/rules/tests/src/android/platform/test/rule/ClassMetricRuleTest.java
new file mode 100644
index 0000000..ec93a37
--- /dev/null
+++ b/libraries/health/rules/tests/src/android/platform/test/rule/ClassMetricRuleTest.java
@@ -0,0 +1,141 @@
+/*
+ * 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.rule;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.app.Instrumentation;
+import android.device.collectors.BaseMetricListener;
+import android.device.collectors.DataRecord;
+import android.os.Bundle;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.Description;
+import org.junit.runner.Result;
+import org.junit.runners.model.Statement;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+
+import java.util.List;
+
+/**
+ * Tests for {@link ClassMetricRule}.
+ *
+ * <p>This test will focus on testing that collectors are loaded with the correct argument, and that
+ * they are reporting their results as run metrics. All the other logic has been tested in {@link
+ * TestMetricRuleTest}.
+ */
+public class ClassMetricRuleTest {
+
+    private static final Description DESCRIPTION =
+            Description.createTestDescription("class", "method");
+
+    private static final Statement TEST_STATEMENT =
+            new Statement() {
+                @Override
+                public void evaluate() {}
+            };
+
+    @Mock private Instrumentation mMockInstrumentation;
+
+    @Captor private ArgumentCaptor<Bundle> addResultsCaptor;
+
+    @Before
+    public void setUp() {
+        initMocks(this);
+    }
+
+    @Test
+    public void testRunsSpecifiedCollectorsAndReportRunMetrics() throws Throwable {
+        ClassMetricRule rule =
+                createWithMetricCollectorNames(
+                        "android.platform.test.rule.ClassMetricRuleTest$TestableCollector2",
+                        "android.platform.test.rule.ClassMetricRuleTest$TestableCollector1");
+        rule.apply(TEST_STATEMENT, DESCRIPTION).evaluate();
+
+        // We have two metric collectors, hence results are reported two times.
+        verify(mMockInstrumentation, times(2)).addResults(addResultsCaptor.capture());
+        List<Bundle> results = addResultsCaptor.getAllValues();
+        boolean hasCollector1 = false, hasCollector2 = false;
+        for (Bundle result : results) {
+            if (result.containsKey("TestableCollector1-test")) {
+                hasCollector1 = true;
+            } else if (result.containsKey("TestableCollector2-test")) {
+                hasCollector2 = true;
+            }
+        }
+        assertTrue(hasCollector1);
+        assertTrue(hasCollector2);
+    }
+
+    @Test
+    public void testUsesTestCallbackRatherThanRunCallback() throws Throwable {
+        ClassMetricRule rule =
+                createWithMetricCollectorNames(
+                        "android.platform.test.rule.ClassMetricRuleTest$TestableCollector1");
+        rule.apply(TEST_STATEMENT, DESCRIPTION).evaluate();
+
+        // We have one metric collector, hence results are reported a single time.
+        verify(mMockInstrumentation, times(1)).addResults(addResultsCaptor.capture());
+        Bundle result = addResultsCaptor.getValue();
+        assertTrue(result.containsKey("TestableCollector1-test"));
+        assertFalse(result.containsKey("TestableCollector1-run"));
+    }
+
+    private ClassMetricRule createWithMetricCollectorNames(String... names) {
+        Bundle args = new Bundle();
+        args.putString(ClassMetricRule.METRIC_COLLECTORS_OPTION, String.join(",", names));
+
+        return new ClassMetricRule(args, mMockInstrumentation);
+    }
+
+    public static class BaseTestableCollector extends BaseMetricListener {
+        private final String mName;
+
+        public BaseTestableCollector(String name) {
+            mName = name;
+        }
+
+        @Override
+        public void onTestEnd(DataRecord testData, Description description) {
+            testData.addStringMetric(mName + "-test", "value");
+        }
+
+        // This method should never be used by the rule.
+        @Override
+        public void onTestRunEnd(DataRecord runData, Result result) {
+            runData.addStringMetric(mName + "-run", "value");
+        }
+    }
+
+    public static class TestableCollector1 extends BaseTestableCollector {
+        public TestableCollector1() {
+            super(TestableCollector1.class.getSimpleName());
+        }
+    }
+
+    public static class TestableCollector2 extends BaseTestableCollector {
+        public TestableCollector2() {
+            super(TestableCollector2.class.getSimpleName());
+        }
+    }
+}
diff --git a/libraries/health/rules/tests/src/android/platform/test/rule/TestMetricRuleTest.java b/libraries/health/rules/tests/src/android/platform/test/rule/TestMetricRuleTest.java
index b8ebe1c..506cc7e 100644
--- a/libraries/health/rules/tests/src/android/platform/test/rule/TestMetricRuleTest.java
+++ b/libraries/health/rules/tests/src/android/platform/test/rule/TestMetricRuleTest.java
@@ -80,9 +80,11 @@
                 .containsExactly(
                         "TestableCollector1#setInstrumentation",
                         "TestableCollector1#setupAdditionalArgs",
+                        "TestableCollector1#onSetUp",
                         String.format("Test %s: TestableCollector1#onTestStart", DESCRIPTION),
                         "Test execution",
-                        String.format("Test %s: TestableCollector1#onTestEnd", DESCRIPTION))
+                        String.format("Test %s: TestableCollector1#onTestEnd", DESCRIPTION),
+                        "TestableCollector1#onCleanUp")
                 .inOrder();
     }
 
@@ -98,6 +100,7 @@
                 .containsExactly(
                         "TestableCollector1#setInstrumentation",
                         "TestableCollector1#setupAdditionalArgs",
+                        "TestableCollector1#onSetUp",
                         String.format("Test %s: TestableCollector1#onTestStart", DESCRIPTION),
                         "Test execution",
                         String.format(
@@ -105,7 +108,8 @@
                                 DESCRIPTION,
                                 new Failure(
                                         DESCRIPTION, new RuntimeException(TEST_FAILURE_MESSAGE))),
-                        String.format("Test %s: TestableCollector1#onTestEnd", DESCRIPTION))
+                        String.format("Test %s: TestableCollector1#onTestEnd", DESCRIPTION),
+                        "TestableCollector1#onCleanUp")
                 .inOrder();
     }
 
@@ -121,9 +125,11 @@
         assertThat(sLogs)
                 .containsExactly(
                         "TestableCollector1#setInstrumentation",
-                        "TestableCollector1#setupAdditionalArgs",
                         "TestableCollector2#setInstrumentation",
+                        "TestableCollector1#setupAdditionalArgs",
+                        "TestableCollector1#onSetUp",
                         "TestableCollector2#setupAdditionalArgs",
+                        "TestableCollector2#onSetUp",
                         String.format("Test %s: TestableCollector1#onTestStart", DESCRIPTION),
                         String.format("Test %s: TestableCollector2#onTestStart", DESCRIPTION),
                         "Test execution",
@@ -134,7 +140,9 @@
                                 "Test %s: TestableCollector2#onTestFail with failure %s",
                                 DESCRIPTION, failure),
                         String.format("Test %s: TestableCollector1#onTestEnd", DESCRIPTION),
-                        String.format("Test %s: TestableCollector2#onTestEnd", DESCRIPTION))
+                        String.format("Test %s: TestableCollector2#onTestEnd", DESCRIPTION),
+                        "TestableCollector1#onCleanUp",
+                        "TestableCollector2#onCleanUp")
                 .inOrder();
     }
 
@@ -163,6 +171,29 @@
         TestMetricRule rule = createWithMetricCollectorNames(simpleName);
     }
 
+    @Test
+    public void testInitWithDifferentOptionName() throws Throwable {
+        String optionName = "another-" + TestMetricRule.METRIC_COLLECTORS_OPTION;
+
+        Bundle args = new Bundle();
+        args.putString(
+                optionName, "android.platform.test.rule.TestMetricRuleTest$TestableCollector1");
+        TestMetricRule rule =
+                new TestMetricRule(args, new Instrumentation(), optionName, "log tag");
+
+        rule.apply(PASSING_STATEMENT, DESCRIPTION).evaluate();
+        assertThat(sLogs)
+                .containsExactly(
+                        "TestableCollector1#setInstrumentation",
+                        "TestableCollector1#setupAdditionalArgs",
+                        "TestableCollector1#onSetUp",
+                        String.format("Test %s: TestableCollector1#onTestStart", DESCRIPTION),
+                        "Test execution",
+                        String.format("Test %s: TestableCollector1#onTestEnd", DESCRIPTION),
+                        "TestableCollector1#onCleanUp")
+                .inOrder();
+    }
+
     private TestMetricRule createWithMetricCollectorNames(String... names) {
         Bundle args = new Bundle();
         args.putString(TestMetricRule.METRIC_COLLECTORS_OPTION, String.join(",", names));
@@ -187,6 +218,16 @@
         }
 
         @Override
+        public void onSetUp() {
+            sLogs.add(String.format("%s#%s", mName, "onSetUp"));
+        }
+
+        @Override
+        public void onCleanUp() {
+            sLogs.add(String.format("%s#%s", mName, "onCleanUp"));
+        }
+
+        @Override
         public void onTestStart(DataRecord testData, Description description) {
             sLogs.add(String.format("Test %s: %s#%s", description, mName, "onTestStart"));
         }
diff --git a/libraries/sts-common-util/OWNERS b/libraries/sts-common-util/OWNERS
new file mode 100644
index 0000000..69d2081
--- /dev/null
+++ b/libraries/sts-common-util/OWNERS
@@ -0,0 +1,3 @@
+# STS Owners
+cdombroski@google.com
+musashi@google.com
diff --git a/libraries/sts-common-util/device-side/Android.bp b/libraries/sts-common-util/device-side/Android.bp
new file mode 100644
index 0000000..1692a7c
--- /dev/null
+++ b/libraries/sts-common-util/device-side/Android.bp
@@ -0,0 +1,32 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library_static {
+    name: "sts-device-util",
+    sdk_version: "test_current",
+
+    srcs: ["src/**/*.java"],
+
+    static_libs: [
+        "sts-common-util-devicesidelib",
+    ],
+
+    libs: [
+        "compatibility-device-util-axt",
+    ],
+}
diff --git a/libraries/sts-common-util/device-side/src/com/android/sts/common/util/StsExtraBusinessLogicTestCase.java b/libraries/sts-common-util/device-side/src/com/android/sts/common/util/StsExtraBusinessLogicTestCase.java
new file mode 100644
index 0000000..5f53362
--- /dev/null
+++ b/libraries/sts-common-util/device-side/src/com/android/sts/common/util/StsExtraBusinessLogicTestCase.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sts.common.util;
+
+import android.os.Build;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.compatibility.common.util.ExtraBusinessLogicTestCase;
+import com.android.compatibility.common.util.PropertyUtil;
+
+import org.junit.Rule;
+import org.junit.runner.Description;
+
+import java.time.LocalDate;
+import java.util.List;
+
+/** The device-side implementation of StsLogic. */
+public class StsExtraBusinessLogicTestCase extends ExtraBusinessLogicTestCase implements StsLogic {
+
+    private LocalDate deviceSpl = null;
+    private LocalDate kernelSpl = null;
+    @Rule public DescriptionProvider descriptionProvider = new DescriptionProvider();
+
+    protected StsExtraBusinessLogicTestCase() {
+        mDependentOnBusinessLogic = false;
+    }
+
+    @Override
+    public List<String> getExtraBusinessLogics() {
+        // set in test/sts/tools/sts-tradefed/res/config/sts-base-dynamic-*.xml
+        String stsDynamicPlan =
+                InstrumentationRegistry.getArguments().getString("sts-dynamic-plan");
+        return StsLogic.getExtraBusinessLogicForPlan(stsDynamicPlan);
+    }
+
+    @Override
+    public Description getTestDescription() {
+        return descriptionProvider.getDescription();
+    }
+
+    @Override
+    public LocalDate getPlatformSpl() {
+        if (deviceSpl == null) {
+            deviceSpl = SplUtils.localDateFromSplString(Build.VERSION.SECURITY_PATCH);
+        }
+        return deviceSpl;
+    }
+
+    @Override
+    public LocalDate getKernelSpl() {
+        if (kernelSpl == null) {
+            // set in:
+            // test/sts/tools/sts-tradefed/src/com/android/tradefed/targetprep/multi/KernelSPL.java
+            String kernelSplString =
+                    PropertyUtil.getProperty("persist.sts.build_version_kernel_security_patch");
+            if (kernelSplString == null) {
+                return null;
+            }
+            kernelSpl = SplUtils.localDateFromSplString(kernelSplString);
+        }
+        return kernelSpl;
+    }
+
+    @Override
+    public boolean shouldUseKernelSpl() {
+        // set in test/sts/tools/sts-tradefed/res/config/sts-base-use-kernel-spl.xml
+        String useKernelSplString =
+                InstrumentationRegistry.getArguments().getString("sts-use-kernel-spl");
+        return Boolean.parseBoolean(useKernelSplString);
+    }
+
+    /**
+     * Specify the latest release bulletin. Control this from the command-line with the following:
+     * --test-arg
+     * com.android.tradefed.testtype.AndroidJUnitTest:instrumentation-arg:release-bulletin-spl:=2020-06
+     */
+    @Override
+    public LocalDate getReleaseBulletinSpl() {
+        // set manually with command-line args at runtime
+        String releaseBulletinSpl =
+                InstrumentationRegistry.getArguments().getString("release-bulletin-spl");
+        if (releaseBulletinSpl == null) {
+            return null;
+        }
+        // bulletin is released by month; add any day - only the year and month are compared.
+        releaseBulletinSpl =
+                String.format("%s-%02d", releaseBulletinSpl, SplUtils.Type.PARTIAL.day);
+        return SplUtils.localDateFromSplString(releaseBulletinSpl);
+    }
+
+    @Override
+    public void logInfo(String logTag, String format, Object... args) {
+        Log.i(logTag, String.format(format, args));
+    }
+
+    @Override
+    public void logDebug(String logTag, String format, Object... args) {
+        Log.d(logTag, String.format(format, args));
+    }
+
+    @Override
+    public void logWarn(String logTag, String format, Object... args) {
+        Log.w(logTag, String.format(format, args));
+    }
+
+    @Override
+    public void logError(String logTag, String format, Object... args) {
+        Log.e(logTag, String.format(format, args));
+    }
+}
diff --git a/libraries/sts-common-util/host-side/Android.bp b/libraries/sts-common-util/host-side/Android.bp
new file mode 100644
index 0000000..f151cd3
--- /dev/null
+++ b/libraries/sts-common-util/host-side/Android.bp
@@ -0,0 +1,34 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library_host {
+    name: "sts-host-util",
+    defaults: ["cts_error_prone_rules"],
+
+    srcs: ["src/**/*.java"],
+
+    static_libs: [
+        "sts-common-util-lib",
+    ],
+
+    libs: [
+        "compatibility-tradefed",
+        "guava",
+        "tradefed",
+    ],
+}
diff --git a/libraries/sts-common-util/host-side/rootcanal/Android.bp b/libraries/sts-common-util/host-side/rootcanal/Android.bp
new file mode 100644
index 0000000..a5b7495
--- /dev/null
+++ b/libraries/sts-common-util/host-side/rootcanal/Android.bp
@@ -0,0 +1,11 @@
+sh_test {
+    name: "sts-rootcanal-sidebins",
+    src: "empty.sh",
+    test_suites: ["sts"],
+    data_bins: [
+        "android.hardware.bluetooth@1.1-service.sim",
+        "android.hardware.bluetooth@1.1-impl-sim"
+    ],
+    data: ["android.hardware.bluetooth@1.1-service.sim.rc"],
+}
+
diff --git a/libraries/sts-common-util/host-side/rootcanal/android.hardware.bluetooth@1.1-service.sim.rc b/libraries/sts-common-util/host-side/rootcanal/android.hardware.bluetooth@1.1-service.sim.rc
new file mode 100644
index 0000000..2626841
--- /dev/null
+++ b/libraries/sts-common-util/host-side/rootcanal/android.hardware.bluetooth@1.1-service.sim.rc
@@ -0,0 +1,4 @@
+service vendor.bluetooth-1-1 /vendor/bin/hw/android.hardware.bluetooth@1.1-service.sim
+    class hal
+    user bluetooth
+    group bluetooth
diff --git a/libraries/sts-common-util/host-side/rootcanal/empty.sh b/libraries/sts-common-util/host-side/rootcanal/empty.sh
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/libraries/sts-common-util/host-side/rootcanal/empty.sh
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
new file mode 100644
index 0000000..bdd65e7
--- /dev/null
+++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/CommandUtil.java
@@ -0,0 +1,67 @@
+/*
+ * 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 com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+
+public final class CommandUtil {
+
+    private CommandUtil() {}
+
+    /**
+     * Execute shell command on device, throws AssertionError if command does not return 0.
+     *
+     * @param device the device to use
+     * @param cmd the command to run
+     * @return the result of device.executeShellV2Command
+     */
+    public static CommandResult runAndCheck(ITestDevice device, String cmd)
+            throws DeviceNotAvailableException {
+        return runAndCheck(device, cmd, 0);
+    }
+
+    /**
+     * Execute shell command on device, throws AssertionError if command does not return 0.
+     *
+     * @param device the device to use
+     * @param cmd the command to run
+     * @param retries the number of retries to attempt
+     * @return the result of device.executeShellV2Command
+     */
+    public static CommandResult runAndCheck(ITestDevice device, String cmd, int retries)
+            throws DeviceNotAvailableException {
+        int attempt = 0;
+        CommandResult res;
+
+        do {
+            attempt += 1;
+            res = device.executeShellV2Command(cmd);
+        } while (res.getStatus() != CommandStatus.SUCCESS && attempt <= retries);
+
+        String failMsg =
+                String.format(
+                        "cmd failed: %s\ncode: %s\nstdout:\n%s\nstderr:\n%s",
+                        cmd, res.getExitCode(), res.getStdout(), res.getStderr());
+        assertEquals(failMsg, res.getStatus(), CommandStatus.SUCCESS);
+        return res;
+    }
+}
diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/OverlayFsUtils.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/OverlayFsUtils.java
new file mode 100644
index 0000000..1fa55de
--- /dev/null
+++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/OverlayFsUtils.java
@@ -0,0 +1,175 @@
+/*
+ * 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.assertTrue;
+import static org.junit.Assert.assertNotNull;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+import com.google.common.hash.Hashing;
+
+
+/** TestWatcher that enables writing to read-only partitions and reboots device when done. */
+public class OverlayFsUtils extends TestWatcher {
+    private static final String OVERLAYFS_PREFIX = "sts_overlayfs_";
+    private static final Path WRITABLE_DIR = Paths.get("/data", "local", "tmp");
+
+    private final BaseHostJUnit4Test test;
+
+    // output of `stat`, e.g. "root shell 755 u:object_r:vendor_file:s0"
+    static final Pattern PERM_PATTERN =
+            Pattern.compile(
+                    "^(?<user>[a-zA-Z0-9_-]+) (?<group>[a-zA-Z0-9_-]+) (?<perm>[0-7]+)"
+                            + " (?<secontext>.*)$");
+
+    private Map<ITestDevice, List<String>> workingDirs = new HashMap<>();
+
+    public OverlayFsUtils(BaseHostJUnit4Test test) {
+        assertNotNull("Need to pass in a valid testcase object.", test);
+        this.test = test;
+    }
+
+    /**
+     * Mounts an OverlayFS dir over the top most common dir in the list.
+     *
+     * <p>The directory should be writable after this returns successfully. To cleanup, reboot the
+     * device as unfortunately unmounting overlayfs is complicated.
+     *
+     * @param dir The directory to make writable. Directories with single quotes are not supported.
+     */
+    public void makeWritable(final String dir, int megabytes)
+            throws DeviceNotAvailableException, IOException, IllegalStateException {
+        ITestDevice device = test.getDevice();
+        assertNotNull("device not set.", device);
+        assertTrue("dir needs to be an absolute path.", dir.startsWith("/"));
+
+        // losetup doesn't work for image paths 64 bytes or longer, so we have to truncate
+        String dirHash = Hashing.md5().hashString(dir, StandardCharsets.UTF_8).toString();
+        int pathPrefixLength = WRITABLE_DIR.toString().length() + 1 + OVERLAYFS_PREFIX.length();
+        int dirHashLength = Math.min(64 - pathPrefixLength - 5, dirHash.length());
+        assertTrue("Can't fit overlayFS image path in 64 chars.", dirHashLength >= 5);
+        String id = OVERLAYFS_PREFIX + dirHash.substring(0, dirHashLength);
+
+        // Check and make sure we have not already mounted over this dir. We do that by hashing
+        // the lower dir path and put that as part of the device ID for `mount`.
+        CommandResult res = device.executeShellV2Command("mount | grep -q " + id);
+        if (res.getStatus() == CommandStatus.SUCCESS) {
+            // a mount with the same ID already exists
+            throw new IllegalStateException(dir + " has already been made writable.");
+        }
+
+        assertTrue("Can't acquire root for " + device.getSerialNumber(), device.enableAdbRoot());
+
+        // Match permissions of upper dir to lower dir
+        String statOut =
+                CommandUtil.runAndCheck(device, "stat -c '%U %G %a %C' '" + dir + "'").getStdout();
+        Matcher m = PERM_PATTERN.matcher(statOut);
+        assertTrue("Bad stats output: " + statOut, m.find());
+        String user = m.group("user");
+        String group = m.group("group");
+        String unixPerm = m.group("perm");
+        String seContext = m.group("secontext");
+
+        // Disable SELinux enforcement and mount a loopback ext4 image
+        CommandUtil.runAndCheck(device, "setenforce 0");
+        Path tempdir = WRITABLE_DIR.resolve(id);
+        Path tempimg = tempdir.getParent().resolve(tempdir.getFileName().toString() + ".img");
+        CommandUtil.runAndCheck(
+                device,
+                String.format("dd if=/dev/zero of='%s' bs=%dM count=1", tempimg, megabytes));
+        CommandUtil.runAndCheck(device, String.format("mkdir '%s'", tempdir));
+        CommandUtil.runAndCheck(device, String.format("mkfs.ext4 '%s'", tempimg));
+        CommandUtil.runAndCheck(
+                device, String.format("mount -o loop '%s' '%s'", tempimg, tempdir), 3);
+
+        List<String> dirs;
+        if (!workingDirs.containsKey(device)) {
+            dirs = new ArrayList<>(2);
+            workingDirs.put(device, dirs);
+        } else {
+            dirs = workingDirs.get(device);
+        }
+        dirs.add(tempdir.toString());
+        dirs.add(tempimg.toString());
+
+        String upperdir = tempdir.resolve("upper").toString();
+        String workdir = tempdir.resolve("workdir").toString();
+
+        CommandUtil.runAndCheck(device, String.format("mkdir -p '%s' '%s'", upperdir, workdir));
+        CommandUtil.runAndCheck(device, String.format("chown %s:%s '%s'", user, group, upperdir));
+        CommandUtil.runAndCheck(device, String.format("chcon '%s' '%s'", seContext, upperdir));
+        CommandUtil.runAndCheck(device, String.format("chmod %s '%s'", unixPerm, upperdir));
+
+        String mountCmd =
+                String.format(
+                        "mount -t overlay '%s' -o lowerdir='%s',upperdir='%s',workdir='%s' '%s'",
+                        id, dir, upperdir, workdir, dir);
+        CommandUtil.runAndCheck(device, mountCmd);
+    }
+
+    public boolean anyOverlayFsMounted() throws DeviceNotAvailableException {
+        ITestDevice device = test.getDevice();
+        assertNotNull("Device not set", device);
+        CommandResult res = device.executeShellV2Command("mount | grep -q " + OVERLAYFS_PREFIX);
+        return res.getStatus() == CommandStatus.SUCCESS;
+    }
+
+    @Override
+    public void finished(Description d) {
+        ITestDevice device = test.getDevice();
+        assertNotNull("Device not set", device);
+        try {
+            // Since we can't umount an overlayfs cleanly, reboot the device to cleanup
+            if (anyOverlayFsMounted()) {
+                device.rebootUntilOnline();
+                device.waitForDeviceAvailable();
+            }
+
+            // Remove upper and working dirs
+            assertTrue("Can't acquire root: " + device.getSerialNumber(), device.enableAdbRoot());
+            if (workingDirs.containsKey(device)) {
+                for (String dir : workingDirs.get(device)) {
+                    CommandUtil.runAndCheck(device, String.format("rm -rf '%s'", dir));
+                }
+            }
+
+            // Restore SELinux enforcement state
+            CommandUtil.runAndCheck(device, "setenforce 1");
+
+            assertTrue("Can't remove root: " + device.getSerialNumber(), device.disableAdbRoot());
+        } catch (DeviceNotAvailableException e) {
+            throw new AssertionError("Device unavailable when cleaning up", e);
+        }
+    }
+}
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
new file mode 100644
index 0000000..45eeac7
--- /dev/null
+++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/ProcessUtil.java
@@ -0,0 +1,324 @@
+/*
+ * 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.ddmlib.Log;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.TimeoutException;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+public final class ProcessUtil {
+
+    private static final String LOG_TAG = ProcessUtil.class.getSimpleName();
+
+    private static final long PROCESS_WAIT_TIMEOUT_MS = 10_000;
+    private static final long PROCESS_POLL_PERIOD_MS = 250;
+
+    private ProcessUtil() {}
+
+    /**
+     * Get the pids matching a pattern passed to `pgrep`. Because /proc/pid/comm is truncated,
+     * `pgrep` is passed with `-f` to check the full command line.
+     *
+     * @param device the device to use
+     * @param pgrepRegex a String representing the regex for pgrep
+     * @return an Optional Map of pid to command line; empty if pgrep did not return EXIT_SUCCESS
+     */
+    public static Optional<Map<Integer, String>> pidsOf(ITestDevice device, String pgrepRegex)
+            throws DeviceNotAvailableException {
+        // pgrep is available since 6.0 (Marshmallow)
+        // https://chromium.googlesource.com/aosp/platform/system/core/+/HEAD/shell_and_utilities/README.md
+        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()));
+            return Optional.empty();
+        }
+        Map<Integer, String> pidToCommand = new HashMap<>();
+        for (String line : pgrepRes.getStdout().split("\n")) {
+            String[] pidComm = line.split(" ", 2);
+            int pid = Integer.valueOf(pidComm[0]);
+            String comm = pidComm[1];
+            pidToCommand.put(pid, comm);
+        }
+        return Optional.of(pidToCommand);
+    }
+
+    /**
+     * Wait until a running process is found for a given regex.
+     *
+     * @param device the device to use
+     * @param pgrepRegex a String representing the regex for pgrep
+     * @return the pid to command map from pidsOf(...)
+     */
+    public static Map<Integer, String> waitProcessRunning(ITestDevice device, String pgrepRegex)
+            throws TimeoutException, DeviceNotAvailableException {
+        return waitProcessRunning(device, pgrepRegex, PROCESS_WAIT_TIMEOUT_MS);
+    }
+
+    /**
+     * Wait until a running process is found for a given regex.
+     *
+     * @param device the device to use
+     * @param pgrepRegex a String representing the regex for pgrep
+     * @param timeoutMs how long to wait before throwing a TimeoutException
+     * @return the pid to command map from pidsOf(...)
+     */
+    public static Map<Integer, String> waitProcessRunning(
+            ITestDevice device, String pgrepRegex, long timeoutMs)
+            throws TimeoutException, DeviceNotAvailableException {
+        long endTime = System.currentTimeMillis() + timeoutMs;
+        while (true) {
+            Optional<Map<Integer, String>> pidToCommand = pidsOf(device, pgrepRegex);
+            if (pidToCommand.isPresent()) {
+                return pidToCommand.get();
+            }
+            if (System.currentTimeMillis() > endTime) {
+                throw new TimeoutException();
+            }
+            try {
+                Thread.sleep(PROCESS_POLL_PERIOD_MS);
+            } catch (InterruptedException e) {
+                // don't care, just keep looping until we time out
+            }
+        }
+    }
+
+    /**
+     * Get the contents from /proc/pid/cmdline.
+     *
+     * @param device the device to use
+     * @param pid the id of the process to get the name for
+     * @return an Optional String of the contents of /proc/pid/cmdline; empty if the pid could not
+     *     be found
+     */
+    public static Optional<String> getProcessName(ITestDevice device, int pid)
+            throws DeviceNotAvailableException {
+        // /proc/*/comm is truncated, use /proc/*/cmdline instead
+        CommandResult res =
+                device.executeShellV2Command(String.format("cat /proc/%d/cmdline", pid));
+        if (res.getStatus() != CommandStatus.SUCCESS) {
+            return Optional.empty();
+        }
+        return Optional.of(res.getStdout());
+    }
+
+    /**
+     * Wait for a process to be exited. This is not waiting for it to change, but simply be
+     * nonexistent. It is possible, but unlikely, for a pid to be reused between polls
+     *
+     * @param device the device to use
+     * @param pid the id of the process to wait until exited
+     */
+    static void waitPidExited(ITestDevice device, int pid)
+            throws TimeoutException, DeviceNotAvailableException {
+        waitPidExited(device, pid, PROCESS_WAIT_TIMEOUT_MS);
+    }
+
+    /**
+     * Wait for a process to be exited. This is not waiting for it to change, but simply be
+     * nonexistent. It is possible, but unlikely, for a pid to be reused between polls
+     *
+     * @param device the device to use
+     * @param pid the id of the process to wait until exited
+     * @param timeoutMs how long to wait before throwing a TimeoutException
+     */
+    static void waitPidExited(ITestDevice device, int pid, long timeoutMs)
+            throws TimeoutException, DeviceNotAvailableException {
+        long endTime = System.currentTimeMillis() + timeoutMs;
+        CommandResult res = null;
+        while (true) {
+            // kill -0 asserts that the process is alive and readable
+            res = device.executeShellV2Command(String.format("kill -0 %d", pid));
+            if (res.getStatus() != CommandStatus.SUCCESS) {
+                // the process is most likely killed
+                return;
+            }
+            if (System.currentTimeMillis() > endTime) {
+                throw new TimeoutException();
+            }
+            try {
+                Thread.sleep(PROCESS_POLL_PERIOD_MS);
+            } catch (InterruptedException e) {
+                // don't care, just keep looping until we time out
+            }
+        }
+    }
+
+    /**
+     * Send SIGKILL to a process and wait for it to be exited.
+     *
+     * @param device the device to use
+     * @param pid the id of the process to wait until exited
+     * @param timeoutMs how long to wait before throwing a TimeoutException
+     */
+    static void killPid(ITestDevice device, int pid, long timeoutMs)
+            throws DeviceNotAvailableException, TimeoutException {
+        CommandUtil.runAndCheck(device, String.format("kill -9 %d", pid));
+        waitPidExited(device, pid, timeoutMs);
+    }
+
+    /**
+     * Send SIGKILL to a all processes matching a pattern.
+     *
+     * @param device the device to use
+     * @param pgrepRegex a String representing the regex for pgrep
+     * @param timeoutMs how long to wait before throwing a TimeoutException
+     * @return whether any processes were killed
+     */
+    static boolean killAll(ITestDevice device, String pgrepRegex, long timeoutMs)
+            throws DeviceNotAvailableException, TimeoutException {
+        return killAll(device, pgrepRegex, timeoutMs, true);
+    }
+
+    /**
+     * Send SIGKILL to a all processes matching a pattern.
+     *
+     * @param device the device to use
+     * @param pgrepRegex a String representing the regex for pgrep
+     * @param timeoutMs how long to wait before throwing a TimeoutException
+     * @param expectExist whether an exception should be thrown when no processes were killed
+     * @param expectExist whether an exception should be thrown when no processes were killed
+     * @return whether any processes were killed
+     */
+    static boolean killAll(
+            ITestDevice device, String pgrepRegex, long timeoutMs, boolean expectExist)
+            throws DeviceNotAvailableException, TimeoutException {
+        Optional<Map<Integer, String>> pids = pidsOf(device, pgrepRegex);
+        if (!pids.isPresent()) {
+            // no pids to kill
+            if (expectExist) {
+                throw new RuntimeException(
+                        String.format("Expected to kill processes matching %s", pgrepRegex));
+            }
+            return false;
+        }
+        for (int pid : pids.get().keySet()) {
+            killPid(device, pid, timeoutMs);
+        }
+        return true;
+    }
+
+    /**
+     * 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 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 {
+        return withProcessKill(device, pgrepRegex, beforeCloseKill, PROCESS_WAIT_TIMEOUT_MS);
+    }
+
+    /**
+     * 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 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,
+            final long timeoutMs)
+            throws DeviceNotAvailableException, TimeoutException {
+        return new AutoCloseable() {
+            {
+                if (!killAll(device, pgrepRegex, timeoutMs, /*expectExist*/ false)) {
+                    Log.d(LOG_TAG, String.format("did not kill any processes for %s", pgrepRegex));
+                }
+            }
+
+            @Override
+            public void close() throws Exception {
+                if (beforeCloseKill != null) {
+                    beforeCloseKill.run();
+                }
+                killAll(device, pgrepRegex, timeoutMs, /*expectExist*/ false);
+            }
+        };
+    }
+
+    /**
+     * Returns the currently open file names of the specified process.
+     *
+     * @param device device to be run on
+     * @param pid the id of the process to search
+     * @return an Optional of the open files; empty if the process wasn't found or the open files
+     *     couldn't be read.
+     */
+    public static Optional<List<String>> listOpenFiles(ITestDevice device, int pid)
+            throws DeviceNotAvailableException {
+        // test if we can access the open files of the specified pid
+        // `test` is available in all relevant Android versions
+        CommandResult fdRes =
+                device.executeShellV2Command(String.format("test -r /proc/%d/fd", pid));
+        if (fdRes.getStatus() != CommandStatus.SUCCESS) {
+            return Optional.empty();
+        }
+        // `find` and `realpath` are available since 6.0 (Marshmallow)
+        // https://chromium.googlesource.com/aosp/platform/system/core/+/HEAD/shell_and_utilities/README.md
+        // intentionally not using lsof because of parsing issues
+        // realpath will intentionally fail for non-filesystem file descriptors
+        CommandResult openFilesRes =
+                device.executeShellV2Command(
+                        String.format("find /proc/%d/fd -exec realpath {} + 2> /dev/null", pid));
+        String[] openFilesArray = openFilesRes.getStdout().split("\n");
+        return Optional.of(Arrays.asList(openFilesArray));
+    }
+
+    /**
+     * Returns file names of the specified file, loaded by the specified process.
+     *
+     * @param device device to be run on
+     * @param pid the id of the process to search
+     * @param filePattern a pattern of the file names to return
+     * @return an Optional of the filtered files; empty if the process wasn't found or the open
+     *     files couldn't be read.
+     */
+    public static Optional<List<String>> findFilesLoadedByProcess(
+            ITestDevice device, int pid, Pattern filePattern) throws DeviceNotAvailableException {
+        Optional<List<String>> openFilesOption = listOpenFiles(device, pid);
+        if (!openFilesOption.isPresent()) {
+            return Optional.empty();
+        }
+        List<String> openFiles = openFilesOption.get();
+        return Optional.of(
+                openFiles
+                        .stream()
+                        .filter((f) -> filePattern.matcher(f).matches())
+                        .collect(Collectors.toList()));
+    }
+}
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
new file mode 100644
index 0000000..3849b95
--- /dev/null
+++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/RootcanalUtils.java
@@ -0,0 +1,272 @@
+/*
+ * 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.assertNotNull;
+import static org.junit.Assert.assertFalse;
+import static com.android.sts.common.CommandUtil.runAndCheck;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.TimeUnit;
+import java.util.List;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathFactory;
+import javax.xml.xpath.XPathExpressionException;
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+import org.junit.rules.TestWatcher;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.RunUtil;
+
+/** TestWatcher that sets up a virtual bluetooth HAL and reboots the device once done. */
+public class RootcanalUtils extends TestWatcher {
+    private static final String LOCK_FILENAME = "/data/local/tmp/sts_rootcanal.lck";
+
+    private BaseHostJUnit4Test test;
+    private OverlayFsUtils overlayFsUtils;
+
+    public RootcanalUtils(BaseHostJUnit4Test test) {
+        assertNotNull(test);
+        this.test = test;
+        this.overlayFsUtils = new OverlayFsUtils(test);
+    }
+
+    @Override
+    public void finished(Description d) {
+        ITestDevice device = test.getDevice();
+        assertNotNull("Device not set", device);
+        try {
+            device.enableAdbRoot();
+            runAndCheck(device, String.format("rm -rf '%s'", LOCK_FILENAME));
+            device.disableAdbRoot();
+            // OverlayFsUtils' finished() will restart the device.
+            overlayFsUtils.finished(d);
+        } catch (DeviceNotAvailableException e) {
+            throw new AssertionError("Device unavailable when cleaning up", e);
+        }
+    }
+
+    /** Replace existing HAL with RootCanal HAL on current device. */
+    public void enableRootcanal()
+            throws DeviceNotAvailableException, IOException, InterruptedException,
+                    TimeoutException {
+        enableRootcanal(6111);
+    }
+
+    /**
+     * Replace existing HAL with RootCanal HAL on current device.
+     *
+     * @param port host TCP port to adb-forward to rootcanal control port.
+     */
+    public void enableRootcanal(int port)
+            throws DeviceNotAvailableException, IOException, InterruptedException,
+                    TimeoutException {
+        ITestDevice device = test.getDevice();
+        CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(test.getBuild());
+        assertNotNull("Device not set", device);
+        assertNotNull("Build not set", buildHelper);
+
+        // Check and made sure we're not calling this more than once for a device
+        assertFalse("rootcanal set up called more than once", device.doesFileExist(LOCK_FILENAME));
+        device.pushString("", LOCK_FILENAME);
+
+        // Make sure that /vendor is writable
+        try {
+            overlayFsUtils.makeWritable("/vendor", 100);
+        } catch (IllegalStateException e) {
+            CLog.w(e);
+        }
+
+        // Remove existing HAL files and push new virtual HAL files.
+        runAndCheck(device, "svc bluetooth disable");
+        runAndCheck(
+                device,
+                "rm -f /vendor/lib64/hw/android.hardware.bluetooth@* "
+                        + "/vendor/lib/hw/android.hardware.bluetooth@* "
+                        + "/vendor/bin/hw/android.hardware.bluetooth@* "
+                        + "/vendor/etc/init/android.hardware.bluetooth@*");
+
+        device.pushFile(
+                buildHelper.getTestFile("android.hardware.bluetooth@1.1-service.sim"),
+                "/vendor/bin/hw/android.hardware.bluetooth@1.1-service.sim");
+
+        // Pushing the same lib to both 32 and 64bit lib dirs because (a) it works and
+        // (b) FileUtil does not yet support "arm/lib" and "arm64/lib64" layout.
+        device.pushFile(
+                buildHelper.getTestFile("android.hardware.bluetooth@1.1-impl-sim.so"),
+                "/vendor/lib/hw/android.hardware.bluetooth@1.1-impl-sim.so");
+        device.pushFile(
+                buildHelper.getTestFile("android.hardware.bluetooth@1.1-impl-sim.so"),
+                "/vendor/lib64/hw/android.hardware.bluetooth@1.1-impl-sim.so");
+        device.pushFile(
+                buildHelper.getTestFile("android.hardware.bluetooth@1.1-service.sim.rc"),
+                "/vendor/etc/init/android.hardware.bluetooth@1.1-service.sim.rc");
+
+        // Download and patch the VINTF manifest if needed.
+        tryUpdateVintfManifest(device);
+
+        // Fix up permissions and SELinux contexts of files pushed over
+        runAndCheck(device, "cp /system/lib64/libchrome.so /vendor/lib64/libchrome.so");
+        runAndCheck(device, "chmod 755 /vendor/bin/hw/android.hardware.bluetooth@1.1-service.sim");
+        runAndCheck(
+                device,
+                "chcon u:object_r:hal_bluetooth_default_exec:s0 "
+                        + "/vendor/bin/hw/android.hardware.bluetooth@1.1-service.sim");
+        runAndCheck(
+                device,
+                "chmod 644 "
+                        + "/vendor/etc/vintf/manifest.xml "
+                        + "/vendor/lib/hw/android.hardware.bluetooth@1.1-impl-sim.so "
+                        + "/vendor/lib64/hw/android.hardware.bluetooth@1.1-impl-sim.so");
+        runAndCheck(
+                device, "chcon u:object_r:vendor_configs_file:s0 /vendor/etc/vintf/manifest.xml");
+        runAndCheck(
+                device,
+                "chcon u:object_r:vendor_file:s0 "
+                        + "/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");
+        }
+
+        // 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) {
+            runAndCheck(device, "svc bluetooth enable");
+            runAndCheck(device, "setprop vendor.bt.rootcanal_test_console true");
+            CommandResult res = device.executeShellV2Command(checkCmd);
+            if (res.getStatus() == CommandStatus.SUCCESS) {
+                break;
+            }
+        }
+
+        device.executeAdbCommand("forward", "tcp:6111", String.format("tcp:%d", port));
+    }
+
+    private void tryUpdateVintfManifest(ITestDevice device)
+            throws DeviceNotAvailableException, IOException {
+        try {
+            String vintfManifest = device.pullFileContents("/vendor/etc/vintf/manifest.xml");
+            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+            DocumentBuilder builder = factory.newDocumentBuilder();
+            Document doc = builder.parse(new InputSource(new StringReader(vintfManifest)));
+            String XPATH = "/manifest/hal[name=\"android.hardware.bluetooth\"][version!=\"1.1\"]";
+            Node node =
+                    (Node)
+                            XPathFactory.newInstance()
+                                    .newXPath()
+                                    .evaluate(XPATH, doc, XPathConstants.NODE);
+            if (node != null) {
+                Node versionNode =
+                        (Node)
+                                XPathFactory.newInstance()
+                                        .newXPath()
+                                        .evaluate("version", node, XPathConstants.NODE);
+                versionNode.setTextContent("1.1");
+
+                Node fqnameNode =
+                        (Node)
+                                XPathFactory.newInstance()
+                                        .newXPath()
+                                        .evaluate("fqname", node, XPathConstants.NODE);
+                String newFqname =
+                        fqnameNode.getTextContent().replaceAll("@[0-9]+\\.[0-9]+(::.*)", "@1.1$1");
+                fqnameNode.setTextContent(newFqname);
+
+                File outFile = File.createTempFile("stsrootcanal", null);
+                outFile.deleteOnExit();
+
+                Transformer transformer = TransformerFactory.newInstance().newTransformer();
+                DOMSource source = new DOMSource(doc);
+                StreamResult result = new StreamResult(new FileWriter(outFile));
+                transformer.transform(source, result);
+                device.pushFile(outFile, "/vendor/etc/vintf/manifest.xml");
+                CLog.d("Updated VINTF manifest");
+            } else {
+                CLog.d("Not updating VINTF manifest");
+            }
+        } catch (ParserConfigurationException
+                | SAXException
+                | XPathExpressionException
+                | TransformerException e) {
+            CLog.e("Could not parse vintf manifest: %s", e);
+        }
+    }
+
+    /** Spin wait until given property has given value. */
+    private void waitPropertyValue(ITestDevice device, String name, String value, long timeoutMs)
+            throws TimeoutException, DeviceNotAvailableException, InterruptedException {
+        long endTime = System.currentTimeMillis() + timeoutMs;
+        while (true) {
+            if (value.equals(device.getProperty(name))) {
+                return;
+            }
+            if (System.currentTimeMillis() > endTime) {
+                throw new TimeoutException();
+            }
+            TimeUnit.MILLISECONDS.sleep(250);
+        }
+    }
+}
diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/tradefed/testtype/StsExtraBusinessLogicHostTestBase.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/tradefed/testtype/StsExtraBusinessLogicHostTestBase.java
new file mode 100644
index 0000000..c31f80c
--- /dev/null
+++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/tradefed/testtype/StsExtraBusinessLogicHostTestBase.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sts.common.tradefed.testtype;
+
+import com.android.compatibility.common.tradefed.testtype.ExtraBusinessLogicHostTestBase;
+import com.android.ddmlib.Log;
+import com.android.sts.common.util.DescriptionProvider;
+import com.android.sts.common.util.SplUtils;
+import com.android.sts.common.util.StsLogic;
+import com.android.tradefed.device.DeviceNotAvailableException;
+
+import org.junit.Rule;
+import org.junit.runner.Description;
+
+import java.time.LocalDate;
+import java.util.List;
+
+/** The host-side implementation of StsLogic. */
+public class StsExtraBusinessLogicHostTestBase extends ExtraBusinessLogicHostTestBase
+        implements StsLogic {
+
+    private LocalDate deviceSpl = null;
+    private LocalDate kernelSpl = null;
+    @Rule public DescriptionProvider descriptionProvider = new DescriptionProvider();
+
+    public StsExtraBusinessLogicHostTestBase() {
+        super();
+        mDependentOnBusinessLogic = false;
+    }
+
+    @Override
+    public List<String> getExtraBusinessLogics() {
+        // set in test/sts/tools/sts-tradefed/res/config/sts-base-dynamic-*.xml
+        String stsDynamicPlan = getBuild().getBuildAttributes().get("sts-dynamic-plan");
+        return StsLogic.getExtraBusinessLogicForPlan(stsDynamicPlan);
+    }
+
+    @Override
+    public Description getTestDescription() {
+        return descriptionProvider.getDescription();
+    }
+
+    @Override
+    public LocalDate getPlatformSpl() {
+        if (deviceSpl == null) {
+            try {
+                String splString = getDevice().getProperty("ro.build.version.security_patch");
+                deviceSpl = SplUtils.localDateFromSplString(splString);
+            } catch (DeviceNotAvailableException e) {
+                throw new RuntimeException("couldn't get the security patch level", e);
+            }
+        }
+        return deviceSpl;
+    }
+
+    @Override
+    public LocalDate getKernelSpl() {
+        if (kernelSpl == null) {
+            // set in:
+            // test/sts/tools/sts-tradefed/src/com/android/tradefed/targetprep/multi/KernelSPL.java
+            String kernelSplString =
+                    getBuild().getBuildAttributes().get("cts:build_version_kernel_security_patch");
+            if (kernelSplString == null) {
+                return null;
+            }
+            kernelSpl = SplUtils.localDateFromSplString(kernelSplString);
+        }
+        return kernelSpl;
+    }
+
+    @Override
+    public boolean shouldUseKernelSpl() {
+        // set in test/sts/tools/sts-tradefed/res/config/sts-base-use-kernel-spl.xml
+        String useKernelSplString = getBuild().getBuildAttributes().get("sts-use-kernel-spl");
+        return Boolean.parseBoolean(useKernelSplString);
+    }
+
+    /**
+     * Specify the latest release bulletin. Control this from the command-line with the following
+     * command line argument: --build-attribute "release-bulletin-spl=2021-06"
+     */
+    @Override
+    public LocalDate getReleaseBulletinSpl() {
+        // set manually with command-line args at runtime
+        String releaseBulletinSpl = getBuild().getBuildAttributes().get("release-bulletin-spl");
+        if (releaseBulletinSpl == null) {
+            return null;
+        }
+        // bulletin is released by month; add any day - only the year and month are compared.
+        releaseBulletinSpl =
+                String.format("%s-%02d", releaseBulletinSpl, SplUtils.Type.PARTIAL.day);
+        return SplUtils.localDateFromSplString(releaseBulletinSpl);
+    }
+
+    @Override
+    public void logInfo(String logTag, String format, Object... args) {
+        Log.i(logTag, String.format(format, args));
+    }
+
+    @Override
+    public void logDebug(String logTag, String format, Object... args) {
+        Log.d(logTag, String.format(format, args));
+    }
+
+    @Override
+    public void logWarn(String logTag, String format, Object... args) {
+        Log.w(logTag, String.format(format, args));
+    }
+
+    @Override
+    public void logError(String logTag, String format, Object... args) {
+        Log.e(logTag, String.format(format, args));
+    }
+}
diff --git a/libraries/sts-common-util/util/Android.bp b/libraries/sts-common-util/util/Android.bp
new file mode 100644
index 0000000..403dcc2
--- /dev/null
+++ b/libraries/sts-common-util/util/Android.bp
@@ -0,0 +1,48 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+// Build the common utility library for use device-side
+java_library_static {
+    name: "sts-common-util-devicesidelib",
+    visibility: [
+        "//platform_testing/libraries/sts-common-util/device-side",
+    ],
+    sdk_version: "current",
+    srcs: ["src/**/*.java"],
+    static_libs: [
+        "junit",
+        "platform-test-annotations",
+        "compatibility-common-util-devicesidelib",
+    ],
+}
+
+java_library {
+    name: "sts-common-util-lib",
+    visibility: [
+        "//platform_testing/libraries/sts-common-util/host-side",
+    ],
+    host_supported: true,
+    srcs: ["src/**/*.java"],
+    static_libs: [
+        "compatibility-common-util-lib",
+    ],
+    libs: [
+        "junit",
+        "platform-test-annotations",
+    ],
+}
diff --git a/libraries/sts-common-util/util/src/com/android/sts/common/util/BusinessLogicSetStore.java b/libraries/sts-common-util/util/src/com/android/sts/common/util/BusinessLogicSetStore.java
new file mode 100644
index 0000000..ea63f23
--- /dev/null
+++ b/libraries/sts-common-util/util/src/com/android/sts/common/util/BusinessLogicSetStore.java
@@ -0,0 +1,53 @@
+/*
+ * 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.util;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/** Business-Logic GCL-accessible utility for sets. */
+public class BusinessLogicSetStore {
+
+    private static Map<String, Set<String>> sets = new HashMap<>();
+
+    public boolean hasSet(String setName) {
+        return sets.containsKey(setName);
+    }
+
+    public void putSet(String setName, String... elements) {
+        Set<String> set = sets.get(setName);
+        if (set == null) {
+            set = new HashSet<>();
+            sets.put(setName, set);
+        }
+
+        for (String element : elements) {
+            set.add(element);
+        }
+    }
+
+    public static Set<String> getSet(String setName) {
+        Set<String> set = sets.get(setName);
+        if (set == null) {
+            return null;
+        }
+        return Collections.unmodifiableSet(set);
+    }
+}
diff --git a/libraries/sts-common-util/util/src/com/android/sts/common/util/DescriptionProvider.java b/libraries/sts-common-util/util/src/com/android/sts/common/util/DescriptionProvider.java
new file mode 100644
index 0000000..60d46ea
--- /dev/null
+++ b/libraries/sts-common-util/util/src/com/android/sts/common/util/DescriptionProvider.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sts.common.util;
+
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+
+/** Provide a way for tests to get their description while running. */
+public class DescriptionProvider extends TestWatcher {
+    private volatile Description description;
+
+    @Override
+    protected void starting(Description description) {
+        this.description = description;
+    }
+
+    public Description getDescription() {
+        return description;
+    }
+}
diff --git a/libraries/sts-common-util/util/src/com/android/sts/common/util/SplUtils.java b/libraries/sts-common-util/util/src/com/android/sts/common/util/SplUtils.java
new file mode 100644
index 0000000..afb7b12
--- /dev/null
+++ b/libraries/sts-common-util/util/src/com/android/sts/common/util/SplUtils.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sts.common.util;
+
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+
+/** Tools for Security Patch Levels and LocalDates representing them. */
+public final class SplUtils {
+    private static final ZoneId UTC_ZONE_ID = ZoneId.of("UTC");
+    private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+
+    public enum Type {
+        PARTIAL(1), // platform
+        COMPLETE(5); // device-specific (kernel, soc, etc)
+
+        public final int day;
+
+        Type(int day) {
+            this.day = day;
+        }
+    }
+
+    public static LocalDate localDateFromMillis(long millis) {
+        return Instant.ofEpochMilli(millis).atZone(UTC_ZONE_ID).toLocalDate();
+    }
+
+    public static LocalDate localDateFromSplString(String spl) {
+        return LocalDate.parse(spl, formatter);
+    }
+
+    public static String format(LocalDate date) {
+        return date.format(formatter);
+    }
+}
diff --git a/libraries/sts-common-util/util/src/com/android/sts/common/util/StsLogic.java b/libraries/sts-common-util/util/src/com/android/sts/common/util/StsLogic.java
new file mode 100644
index 0000000..8dd749c
--- /dev/null
+++ b/libraries/sts-common-util/util/src/com/android/sts/common/util/StsLogic.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sts.common.util;
+
+import static org.junit.Assert.*;
+import static org.junit.Assume.*;
+
+import android.platform.test.annotations.AsbSecurityTest;
+
+import com.android.compatibility.common.util.BusinessLogicMapStore;
+
+import org.junit.runner.Description;
+
+import java.time.LocalDate;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/** Common STS extra business logic for host-side and device-side to implement. */
+public interface StsLogic {
+
+    static final String LOG_TAG = StsLogic.class.getSimpleName();
+
+    // keep in sync with google3:
+    // //wireless/android/partner/apbs/*/config/xtsbgusinesslogic/sts_business_logic.gcl
+    List<String> STS_EXTRA_BUSINESS_LOGIC_FULL = Arrays.asList(new String[] {
+            "uploadSpl",
+            "uploadModificationTime",
+            "uploadKernelBugs",
+            "declaredSpl",
+    });
+    List<String> STS_EXTRA_BUSINESS_LOGIC_INCREMENTAL = Arrays.asList(new String[] {
+            "uploadSpl",
+            "uploadModificationTime",
+            "uploadKernelBugs",
+            "declaredSpl",
+            "incremental",
+    });
+
+    // intentionally empty because declaredSpl and incremental skipping is not desired when
+    // developing STS tests.
+    List<String> STS_EXTRA_BUSINESS_LOGIC_DEVELOP = Arrays.asList(new String[] {
+    });
+
+    Description getTestDescription();
+
+    LocalDate getPlatformSpl();
+
+    LocalDate getKernelSpl();
+
+    boolean shouldUseKernelSpl();
+
+    LocalDate getReleaseBulletinSpl();
+
+    static List<String> getExtraBusinessLogicForPlan(String stsDynamicPlan) {
+        switch (stsDynamicPlan) {
+            case "incremental":
+                return STS_EXTRA_BUSINESS_LOGIC_INCREMENTAL;
+            case "full":
+                return STS_EXTRA_BUSINESS_LOGIC_FULL;
+            case "develop":
+                return STS_EXTRA_BUSINESS_LOGIC_DEVELOP;
+            default:
+                throw new RuntimeException(
+                        "Could not find Dynamic STS plan in InstrumentationRegistry arguments");
+        }
+    }
+
+    default long[] getCveBugIds() {
+        AsbSecurityTest annotation = getTestDescription().getAnnotation(AsbSecurityTest.class);
+        if (annotation == null) {
+            return null;
+        }
+        return annotation.cveBugId();
+    }
+
+    default boolean isBugSplDataKnownMissing() {
+        long[] bugIds = getCveBugIds();
+        if (bugIds == null) {
+            // no spl data, don't complain
+            return true;
+        }
+        // true if the bug id is older than ~ June 2020
+        return Arrays.stream(bugIds).min().getAsLong() < 157905780;
+    }
+
+    default LocalDate getDeviceSpl() {
+        if (shouldUseKernelSpl()) {
+            Set<String> bugIds = BusinessLogicSetStore.getSet("kernel_bugs");
+            boolean isKernel = false;
+            for (long bugId : getCveBugIds()) {
+                isKernel |= bugIds.contains(Long.toString(bugId));
+            }
+            if (isKernel) {
+                LocalDate kernelSpl = getKernelSpl();
+                if (kernelSpl != null) {
+                    return kernelSpl;
+                }
+                // could not get the kernel SPL even though we should use it
+                // falling back to platform SPL
+                logWarn(LOG_TAG, "could not read kernel SPL, falling back to platform SPL");
+            }
+        }
+        return getPlatformSpl();
+    }
+
+    default LocalDate getMinTestSpl() {
+        Map<String, String> map = BusinessLogicMapStore.getMap("security_bulletins");
+        if (map == null) {
+            throw new IllegalArgumentException("Could not find the security bulletin map");
+        }
+        LocalDate minSpl = null;
+        for (long cveBugId : getCveBugIds()) {
+            String splString = map.get(Long.toString(cveBugId));
+            if (splString == null) {
+                // This bug id wasn't found in the map.
+                // This is a new test or the bug was removed from the bulletin and this is an old
+                // binary. Neither is a critical issue and the test will run in these cases.
+                // New test: developer should be able to write the test without getting blocked.
+                // Removed bug + old binary: test will run.
+                logWarn(LOG_TAG, "could not find the CVE bug %d in the spl map", cveBugId);
+                continue;
+            }
+            LocalDate spl = SplUtils.localDateFromSplString(splString);
+            if (minSpl == null) {
+                minSpl = spl;
+            } else if (spl.isBefore(minSpl)) {
+                minSpl = spl;
+            }
+        }
+        return minSpl;
+    }
+
+    default LocalDate getMinModificationDate() {
+        Map<String, String> map = BusinessLogicMapStore.getMap("sts_modification_times");
+        if (map == null) {
+            throw new IllegalArgumentException("Could not find the modification date map");
+        }
+        LocalDate minModificationDate = null;
+        for (long cveBugId : getCveBugIds()) {
+            String modificationMillisString = map.get(Long.toString(cveBugId));
+            if (modificationMillisString == null) {
+                logInfo(
+                        LOG_TAG,
+                        "Could not find the CVE bug %d in the modification date map",
+                        cveBugId);
+                continue;
+            }
+            LocalDate modificationDate =
+                    SplUtils.localDateFromMillis(Long.parseLong(modificationMillisString));
+            if (minModificationDate == null) {
+                minModificationDate = modificationDate;
+            } else if (modificationDate.isBefore(minModificationDate)) {
+                minModificationDate = modificationDate;
+            }
+        }
+        return minModificationDate;
+    }
+
+    default boolean shouldSkipIncremental() {
+        logDebug(LOG_TAG, "filtering by incremental");
+
+        long[] bugIds = getCveBugIds();
+        if (bugIds == null) {
+            // There were no @AsbSecurityTest annotations
+            logInfo(LOG_TAG, "not an ASB test");
+            return false;
+        }
+
+        // check if test spl is older than the past 6 months from the device spl
+        LocalDate deviceSpl = getDeviceSpl();
+        LocalDate incrementalCutoffSpl = deviceSpl.plusMonths(-6);
+
+        LocalDate minTestModifiedDate = getMinModificationDate();
+        if (minTestModifiedDate == null) {
+            // could not get the modification date - run the test
+            if (isBugSplDataKnownMissing()) {
+                logDebug(LOG_TAG, "no data for this old test");
+                return true;
+            }
+            return false;
+        }
+        if (minTestModifiedDate.isAfter(incrementalCutoffSpl)) {
+            logDebug(LOG_TAG, "the test was recently modified");
+            return false;
+        }
+
+        LocalDate minTestSpl = getMinTestSpl();
+        if (minTestSpl == null) {
+            // could not get the test spl - run the test
+            logWarn(LOG_TAG, "could not get the test SPL");
+            return false;
+        }
+        if (minTestSpl.isAfter(incrementalCutoffSpl)) {
+            logDebug(LOG_TAG, "the test has a recent SPL");
+            return false;
+        }
+
+        logDebug(LOG_TAG, "test should skip");
+        return true;
+    }
+
+    default boolean shouldSkipDeclaredSpl() {
+        if (getCveBugIds() == null) {
+            // There were no @AsbSecurityTest annotations
+            logInfo(LOG_TAG, "not an ASB test");
+            return false;
+        }
+
+        LocalDate minTestSpl = getMinTestSpl();
+        if (!isBugSplDataKnownMissing()) {
+            LocalDate releaseBulletinSpl = getReleaseBulletinSpl();
+            if (releaseBulletinSpl != null) {
+                // this is a QA environment
+
+                // assert that the test has a known SPL when we expect the data to be fresh
+                assertNotNull("Unknown SPL for new CVE", minTestSpl);
+
+                // set the days to be the same so we only compare year-month
+                releaseBulletinSpl = releaseBulletinSpl.withDayOfMonth(minTestSpl.getDayOfMonth());
+                // the test SPL can't be equal to or after the release bulletin SPL
+                assertFalse(
+                        "Newer SPL than release bulletin", releaseBulletinSpl.isBefore(minTestSpl));
+            } else {
+                // we are in a live environment; don't run tests that have their SPL deferred
+                if (minTestSpl == null) {
+                    // can't find the test SPL for this ASB test; skip
+                    return true;
+                }
+            }
+        }
+        if (minTestSpl == null) {
+            // no SPL for this test; run normally
+            return false;
+        }
+
+        // skip if the test is newer than the device SPL
+        LocalDate deviceSpl = getDeviceSpl();
+        return minTestSpl.isAfter(deviceSpl);
+    }
+
+    default void skip(String message) {
+        assumeTrue(message, false);
+    }
+
+    public void logInfo(String logTag, String format, Object... args);
+
+    public void logDebug(String logTag, String format, Object... args);
+
+    public void logWarn(String logTag, String format, Object... args);
+
+    public void logError(String logTag, String format, Object... args);
+}
diff --git a/tests/automotive/functional/appgrid/Android.bp b/tests/automotive/functional/appgrid/Android.bp
index 54bb764..45cdcb8 100644
--- a/tests/automotive/functional/appgrid/Android.bp
+++ b/tests/automotive/functional/appgrid/Android.bp
@@ -30,5 +30,5 @@
         "platform-test-options",
     ],
     srcs: ["src/**/*.java"],
-    test_suites: ["catbox"],
+    test_suites: ["catbox","ats"],
 }
diff --git a/tests/automotive/functional/appgrid/src/android/platform/tests/AppGridTest.java b/tests/automotive/functional/appgrid/src/android/platform/tests/AppGridTest.java
index 1b89492..08efc8a 100644
--- a/tests/automotive/functional/appgrid/src/android/platform/tests/AppGridTest.java
+++ b/tests/automotive/functional/appgrid/src/android/platform/tests/AppGridTest.java
@@ -46,20 +46,20 @@
     public void testOpen() {
         // Make sure app grid is not open before testing.
         mAppGridHelper.get().exit();
-        assertFalse(mAppGridHelper.get().isAppInForeground());
+        assertFalse("App Grid is open even after exit.", mAppGridHelper.get().isAppInForeground());
         // Test open.
         mAppGridHelper.get().open();
-        assertTrue(mAppGridHelper.get().isAppInForeground());
+        assertTrue("App Grid is not open.", mAppGridHelper.get().isAppInForeground());
     }
 
     @Test
     public void testExit() {
         // Make sure app grid has been opened before testing.
         mAppGridHelper.get().open();
-        assertTrue(mAppGridHelper.get().isAppInForeground());
+        assertTrue("App Grid is not open.", mAppGridHelper.get().isAppInForeground());
         // Test exit.
         mAppGridHelper.get().exit();
-        assertFalse(mAppGridHelper.get().isAppInForeground());
+        assertFalse("App Grid is open even after exit.", mAppGridHelper.get().isAppInForeground());
     }
 
     @Test
@@ -67,13 +67,12 @@
         // Re-enter app grid.
         mAppGridHelper.get().exit();
         mAppGridHelper.get().open();
-        assertTrue(mAppGridHelper.get().isTop());
+        assertTrue("Not on top of App Grid.", mAppGridHelper.get().isTop());
         // Test scroll only when there are more than one page in app grid.
         if (!mAppGridHelper.get().isBottom()) {
             mAppGridHelper.get().scrollDownOnePage();
-            assertFalse(mAppGridHelper.get().isTop());
+            assertFalse("Scrolling did not work.", mAppGridHelper.get().isTop());
             mAppGridHelper.get().scrollUpOnePage();
-            assertTrue(mAppGridHelper.get().isTop());
         }
     }
 }
diff --git a/tests/automotive/functional/dialer/Android.bp b/tests/automotive/functional/dialer/Android.bp
index 0993fd1..d4571aa 100644
--- a/tests/automotive/functional/dialer/Android.bp
+++ b/tests/automotive/functional/dialer/Android.bp
@@ -31,5 +31,5 @@
         "platform-test-options",
     ],
     srcs: ["src/**/*.java"],
-    test_suites: ["catbox"],
+    test_suites: ["catbox","ats"],
 }
diff --git a/tests/automotive/functional/dialer/src/android/platform/tests/DialTest.java b/tests/automotive/functional/dialer/src/android/platform/tests/DialTest.java
index 5f1ae73..2fe6ae3 100644
--- a/tests/automotive/functional/dialer/src/android/platform/tests/DialTest.java
+++ b/tests/automotive/functional/dialer/src/android/platform/tests/DialTest.java
@@ -196,7 +196,7 @@
         mDialerHelper.get().callContact(DIAL_CONTACT_BY_NAME);
         assertTrue(
                 "Contact detail is not the same",
-                mDialerHelper.get().getContactType().contains(CONTACT_TYPE));
+                mDialerHelper.get().getContactType().equalsIgnoreCase(CONTACT_TYPE));
         mDialerHelper.get().endCall();
     }
 
diff --git a/tests/automotive/functional/home/Android.bp b/tests/automotive/functional/home/Android.bp
index 6659538..f62fb48 100644
--- a/tests/automotive/functional/home/Android.bp
+++ b/tests/automotive/functional/home/Android.bp
@@ -30,5 +30,5 @@
         "platform-test-options",
     ],
     srcs: ["src/**/*.java"],
-    test_suites: ["catbox","general-tests"],
+    test_suites: ["catbox","general-tests","ats"],
 }
diff --git a/tests/automotive/functional/lockscreen/Android.bp b/tests/automotive/functional/lockscreen/Android.bp
index 1ebe583..5668218 100644
--- a/tests/automotive/functional/lockscreen/Android.bp
+++ b/tests/automotive/functional/lockscreen/Android.bp
@@ -31,5 +31,5 @@
         "platform-test-options",
     ],
     srcs: ["src/**/*.java"],
-    test_suites: ["catbox"],
+    test_suites: ["catbox","ats"],
 }
diff --git a/tests/automotive/functional/lockscreen/src/android/platform/tests/LockScreenTest.java b/tests/automotive/functional/lockscreen/src/android/platform/tests/LockScreenTest.java
index e5d78f7..b87e75e 100644
--- a/tests/automotive/functional/lockscreen/src/android/platform/tests/LockScreenTest.java
+++ b/tests/automotive/functional/lockscreen/src/android/platform/tests/LockScreenTest.java
@@ -68,6 +68,8 @@
     public void testLockUnlockScreenByPassword() {
         mLockScreenHelper.get().lockScreenBy(LockType.PASSWORD, PASSWORD);
         mLockScreenHelper.get().unlockScreenBy(LockType.PASSWORD, PASSWORD);
+        assertTrue("Device is not locked", mSecuritySettingsHelper.get().isDeviceLocked());
+        mSecuritySettingsHelper.get().unlockByPassword(PASSWORD);
         mSecuritySettingsHelper.get().removeLock();
         assertTrue(
                 "Password has not been removed", !mSecuritySettingsHelper.get().isDeviceLocked());
diff --git a/tests/automotive/functional/mediacenter/Android.bp b/tests/automotive/functional/mediacenter/Android.bp
index 5e3b35c..9d50197 100644
--- a/tests/automotive/functional/mediacenter/Android.bp
+++ b/tests/automotive/functional/mediacenter/Android.bp
@@ -31,5 +31,5 @@
         "platform-test-options",
     ],
     srcs: ["src/**/*.java"],
-    test_suites: ["catbox"],
+    test_suites: ["catbox","ats"],
 }
diff --git a/tests/automotive/functional/multiuser/Android.bp b/tests/automotive/functional/multiuser/Android.bp
index c97679f..a9526a9 100644
--- a/tests/automotive/functional/multiuser/Android.bp
+++ b/tests/automotive/functional/multiuser/Android.bp
@@ -41,6 +41,6 @@
     ],
     srcs: ["src/**/*.java"],
     certificate: "platform",
-    test_suites: ["catbox"],
+    test_suites: ["catbox","ats"],
     privileged: true,
 }
diff --git a/tests/automotive/functional/navigationbar/Android.bp b/tests/automotive/functional/navigationbar/Android.bp
index f728028..1d393bf 100644
--- a/tests/automotive/functional/navigationbar/Android.bp
+++ b/tests/automotive/functional/navigationbar/Android.bp
@@ -33,5 +33,5 @@
         "hamcrest-library",
     ],
     srcs: ["src/**/*.java"],
-    test_suites: ["catbox"],
+    test_suites: ["catbox","ats"],
 }
diff --git a/tests/automotive/functional/notifications/Android.bp b/tests/automotive/functional/notifications/Android.bp
index 0fe19d4..ea68de3 100644
--- a/tests/automotive/functional/notifications/Android.bp
+++ b/tests/automotive/functional/notifications/Android.bp
@@ -29,5 +29,5 @@
         "hamcrest-library",
     ],
     srcs: ["src/**/*.java"],
-    test_suites: ["catbox","general-tests"],
+    test_suites: ["catbox","general-tests","ats"],
 }
diff --git a/tests/automotive/functional/settings/Android.bp b/tests/automotive/functional/settings/Android.bp
index 47322f5..3367649 100644
--- a/tests/automotive/functional/settings/Android.bp
+++ b/tests/automotive/functional/settings/Android.bp
@@ -30,5 +30,5 @@
         "platform-test-options",
     ],
     srcs: ["src/**/*.java"],
-    test_suites: ["catbox","general-tests"],
+    test_suites: ["catbox","general-tests","ats"],
 }
diff --git a/tests/automotive/functional/uxrestriction/Android.bp b/tests/automotive/functional/uxrestriction/Android.bp
index 078c45d..4359d16 100644
--- a/tests/automotive/functional/uxrestriction/Android.bp
+++ b/tests/automotive/functional/uxrestriction/Android.bp
@@ -32,5 +32,5 @@
         "automotive-app-grid-helper",
     ],
     srcs: ["src/**/*.java"],
-    test_suites: ["catbox"],
+    test_suites: ["catbox","ats"],
 }
diff --git a/tests/automotive/health/appgrid/tests/Android.bp b/tests/automotive/health/appgrid/tests/Android.bp
index ae5a377..72daed8 100644
--- a/tests/automotive/health/appgrid/tests/Android.bp
+++ b/tests/automotive/health/appgrid/tests/Android.bp
@@ -34,5 +34,5 @@
         "platform-test-options",
     ],
     srcs: ["src/**/*.java"],
-    test_suites: ["catbox"],
+    test_suites: ["catbox", "ats"],
 }
diff --git a/tests/automotive/health/dial/tests/Android.bp b/tests/automotive/health/dial/tests/Android.bp
index 497f865..8213cdc 100644
--- a/tests/automotive/health/dial/tests/Android.bp
+++ b/tests/automotive/health/dial/tests/Android.bp
@@ -34,5 +34,5 @@
         "platform-test-options",
     ],
     srcs: ["src/**/*.java"],
-    test_suites: ["catbox"],
+    test_suites: ["catbox","ats"],
 }
diff --git a/tests/automotive/health/mediacenter/tests/Android.bp b/tests/automotive/health/mediacenter/tests/Android.bp
index e9ddf15..6866027 100644
--- a/tests/automotive/health/mediacenter/tests/Android.bp
+++ b/tests/automotive/health/mediacenter/tests/Android.bp
@@ -34,5 +34,5 @@
         "platform-test-options",
     ],
     srcs: ["src/**/*.java"],
-    test_suites: ["catbox"],
+    test_suites: ["catbox", "ats"],
 }
diff --git a/tests/automotive/health/multiuser/tests/Android.bp b/tests/automotive/health/multiuser/tests/Android.bp
index 8409848..00ecd5e 100644
--- a/tests/automotive/health/multiuser/tests/Android.bp
+++ b/tests/automotive/health/multiuser/tests/Android.bp
@@ -41,6 +41,6 @@
     ],
     srcs: ["src/**/*.java"],
     certificate: "platform",
-    test_suites: ["catbox"],
+    test_suites: ["catbox","ats"],
     privileged: true,
 }
diff --git a/tests/automotive/health/notification/src/android/platform/scenario/notification/Scroll.java b/tests/automotive/health/notification/src/android/platform/scenario/notification/Scroll.java
index 79a26db..85d20a4 100644
--- a/tests/automotive/health/notification/src/android/platform/scenario/notification/Scroll.java
+++ b/tests/automotive/health/notification/src/android/platform/scenario/notification/Scroll.java
@@ -36,7 +36,7 @@
 
     @Test
     public void testScrollUpAndDown() {
-        sHelper.get().scrollDownOnePage(500);
-        sHelper.get().scrollUpOnePage(500);
+        sHelper.get().scrollDownOnePage();
+        sHelper.get().scrollUpOnePage();
     }
 }
diff --git a/tests/automotive/health/notification/tests/Android.bp b/tests/automotive/health/notification/tests/Android.bp
index f041ebc..f44f925 100644
--- a/tests/automotive/health/notification/tests/Android.bp
+++ b/tests/automotive/health/notification/tests/Android.bp
@@ -34,5 +34,5 @@
         "platform-test-options",
     ],
     srcs: ["src/**/*.java"],
-    test_suites: ["catbox"],
+    test_suites: ["catbox","ats"],
 }
diff --git a/tests/automotive/health/settings/tests/Android.bp b/tests/automotive/health/settings/tests/Android.bp
index 0f102f5..d380a79 100644
--- a/tests/automotive/health/settings/tests/Android.bp
+++ b/tests/automotive/health/settings/tests/Android.bp
@@ -34,5 +34,5 @@
         "platform-test-options",
     ],
     srcs: ["src/**/*.java"],
-    test_suites: ["catbox"],
+    test_suites: ["catbox","ats"],
 }