Snap for 8589293 from 66b17022f95b3c42b7d3e8929e916912f56948dd to sc-v2-platform-release
Change-Id: I8b3455c7ca047d056cb8e5eca9476bd38ab47e24
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"],
}