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