diff --git a/wa/__init__.py b/wa/__init__.py
index c7969623f..17d6efcf6 100644
--- a/wa/__init__.py
+++ b/wa/__init__.py
@@ -32,8 +32,10 @@
from wa.framework.target.descriptor import (TargetDescriptor, TargetDescription,
create_target_description, add_description_for_target)
from wa.framework.workload import (Workload, ApkWorkload, ApkUiautoWorkload,
+ ApkUiautoJankTestWorkload,
ApkReventWorkload, UIWorkload, UiautoWorkload,
- PackageHandler, ReventWorkload, TestPackageHandler)
+ PackageHandler, ReventWorkload, UiAutomatorGUI,
+ UiAutomatorJankTestGUI, TestPackageHandler)
from wa.framework.version import get_wa_version, get_wa_version_with_commit
diff --git a/wa/framework/uiauto-androidx/app/build.gradle b/wa/framework/uiauto-androidx/app/build.gradle
new file mode 100644
index 000000000..180de6b38
--- /dev/null
+++ b/wa/framework/uiauto-androidx/app/build.gradle
@@ -0,0 +1,18 @@
+apply plugin: 'com.android.library'
+
+android {
+ namespace "com.arm.wa.uiauto"
+ compileSdkVersion 28
+ defaultConfig {
+ minSdkVersion 18
+ targetSdkVersion 28
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+}
+
+dependencies {
+ implementation fileTree(include: ['*.jar'], dir: 'libs')
+ implementation 'androidx.test:runner:1.6.1'
+ implementation 'androidx.test:rules:1.6.1'
+ implementation 'androidx.test.uiautomator:uiautomator-v18:2.2.0-alpha1'
+}
diff --git a/wa/framework/uiauto-androidx/app/src/main/AndroidManifest.xml b/wa/framework/uiauto-androidx/app/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..9d2cf7263
--- /dev/null
+++ b/wa/framework/uiauto-androidx/app/src/main/AndroidManifest.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/wa/framework/uiauto-androidx/app/src/main/java/com/arm/wa/uiauto/ActionLogger.java b/wa/framework/uiauto-androidx/app/src/main/java/com/arm/wa/uiauto/ActionLogger.java
new file mode 100644
index 000000000..79dbc60bd
--- /dev/null
+++ b/wa/framework/uiauto-androidx/app/src/main/java/com/arm/wa/uiauto/ActionLogger.java
@@ -0,0 +1,60 @@
+/* Copyright 2014-2024 ARM Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.arm.wa.uiauto;
+
+import android.os.Bundle;
+import android.util.Log;
+ /**
+ * Basic marker API for workloads to generate start and end markers for
+ * deliminating and timing actions. Markers are output to logcat with debug
+ * priority. Actions represent a series of UI interactions to time.
+ *
+ * The marker API provides a way for instruments and output processors to hook into
+ * per-action timings by parsing logcat logs produced per workload iteration.
+ *
+ * The marker output consists of a logcat tag 'UX_PERF' and a message. The
+ * message consists of a name for the action and a timestamp. The timestamp
+ * is separated by a single space from the name of the action.
+ *
+ * Typical usage:
+ *
+ * ActionLogger logger = ActionLogger("testTag", parameters);
+ * logger.start();
+ * // actions to be recorded
+ * logger.stop();
+ */
+ public class ActionLogger {
+
+ private String testTag;
+ private boolean enabled;
+
+ public ActionLogger(String testTag, Bundle parameters) {
+ this.testTag = testTag;
+ this.enabled = parameters.getBoolean("markers_enabled");
+ }
+
+ public void start() {
+ if (enabled) {
+ Log.d("UX_PERF", testTag + " start " + System.nanoTime());
+ }
+ }
+
+ public void stop() throws Exception {
+ if (enabled) {
+ Log.d("UX_PERF", testTag + " end " + System.nanoTime());
+ }
+ }
+ }
diff --git a/wa/framework/uiauto-androidx/app/src/main/java/com/arm/wa/uiauto/ApplaunchInterface.java b/wa/framework/uiauto-androidx/app/src/main/java/com/arm/wa/uiauto/ApplaunchInterface.java
new file mode 100644
index 000000000..9b3169673
--- /dev/null
+++ b/wa/framework/uiauto-androidx/app/src/main/java/com/arm/wa/uiauto/ApplaunchInterface.java
@@ -0,0 +1,54 @@
+/* Copyright 2013-2024 ARM Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.arm.wa.uiauto;
+
+import android.os.Bundle;
+import androidx.test.uiautomator.UiObject;
+
+/**
+ * ApplaunchInterface.java
+ * Interface used for enabling uxperfapplaunch workload.
+ * This interface gets implemented by all workloads that support application launch
+ * instrumentation.
+ */
+
+public interface ApplaunchInterface {
+
+ /**
+ * Sets the launchEndObject of a workload, which is a UiObject that marks
+ * the end of the application launch.
+ */
+ public UiObject getLaunchEndObject();
+
+ /**
+ * Runs the Uiautomation methods for clearing the initial run
+ * dialogues on the first time installation of an application package.
+ */
+ public void runApplicationSetup() throws Exception;
+
+ /**
+ * Provides the application launch command of the application which is
+ * constructed as a string from the workload.
+ */
+ public String getLaunchCommand();
+
+ /** Passes the workload parameters. */
+ public void setWorkloadParameters(Bundle parameters);
+
+ /** Initialize the instrumentation for the workload */
+ public void initialize_instrumentation();
+
+}
diff --git a/wa/framework/uiauto-androidx/app/src/main/java/com/arm/wa/uiauto/BaseUiAutomation.java b/wa/framework/uiauto-androidx/app/src/main/java/com/arm/wa/uiauto/BaseUiAutomation.java
new file mode 100644
index 000000000..30e773776
--- /dev/null
+++ b/wa/framework/uiauto-androidx/app/src/main/java/com/arm/wa/uiauto/BaseUiAutomation.java
@@ -0,0 +1,725 @@
+/* Copyright 2013-2024 ARM Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.arm.wa.uiauto;
+
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.app.Instrumentation;
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.Rect;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.UiObject;
+import androidx.test.uiautomator.UiObjectNotFoundException;
+import androidx.test.uiautomator.UiSelector;
+import androidx.test.uiautomator.UiWatcher;
+import androidx.test.uiautomator.UiScrollable;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.InputStreamReader;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import static androidx.test.InstrumentationRegistry.getArguments;
+
+public class BaseUiAutomation {
+
+ public enum FindByCriteria { BY_ID, BY_TEXT, BY_DESC };
+ public enum Direction { UP, DOWN, LEFT, RIGHT, NULL };
+ public enum ScreenOrientation { RIGHT, NATURAL, LEFT, PORTRAIT, LANDSCAPE };
+ public enum PinchType { IN, OUT, NULL };
+
+ // Time in milliseconds
+ public long uiAutoTimeout = 4 * 1000;
+
+ public static final int CLICK_REPEAT_INTERVAL_MINIMUM = 5;
+ public static final int CLICK_REPEAT_INTERVAL_DEFAULT = 50;
+
+ public Instrumentation mInstrumentation;
+ public Context mContext;
+ public UiDevice mDevice;
+
+ @Before
+ public void initialize_instrumentation() {
+ mInstrumentation = InstrumentationRegistry.getInstrumentation();
+ mDevice = UiDevice.getInstance(mInstrumentation);
+ mContext = mInstrumentation.getTargetContext();
+ }
+
+ @Test
+ public void setup() throws Exception {
+ }
+
+ @Test
+ public void runWorkload() throws Exception {
+ }
+
+ @Test
+ public void extractResults() throws Exception {
+ }
+
+ @Test
+ public void teardown() throws Exception {
+ }
+
+ public void sleep(int second) {
+ SystemClock.sleep(second * 1000);
+ }
+
+ // Generate a package ID
+ public String getPackageID(Bundle parameters) {
+ String packageName = parameters.getString("package_name");
+ return packageName + ":id/";
+ }
+
+ public boolean takeScreenshot(String name) {
+ Bundle params = getArguments();
+ String png_dir = params.getString("workdir");
+
+ try {
+ return mDevice.takeScreenshot(new File(png_dir, name + ".png"));
+ } catch (NoSuchMethodError e) {
+ return true;
+ }
+ }
+
+ public void waitText(String text) throws UiObjectNotFoundException {
+ waitText(text, 600);
+ }
+
+ public void waitText(String text, int second) throws UiObjectNotFoundException {
+ UiSelector selector = new UiSelector();
+ UiObject text_obj = mDevice.findObject(selector.text(text)
+ .className("android.widget.TextView"));
+ waitObject(text_obj, second);
+ }
+
+ public void waitObject(UiObject obj) throws UiObjectNotFoundException {
+ waitObject(obj, 600);
+ }
+
+ public void waitObject(UiObject obj, int second) throws UiObjectNotFoundException {
+ if (!obj.waitForExists(second * 1000)) {
+ throw new UiObjectNotFoundException("UiObject is not found: "
+ + obj.getSelector().toString());
+ }
+ }
+
+ public boolean waitUntilNoObject(UiObject obj, int second) {
+ return obj.waitUntilGone(second * 1000);
+ }
+
+ public void clearLogcat() throws Exception {
+ Runtime.getRuntime().exec("logcat -c");
+ }
+
+ public void waitForLogcatText(String searchText, long timeout) throws Exception {
+ long startTime = System.currentTimeMillis();
+ Process process = Runtime.getRuntime().exec("logcat");
+ BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
+ String line;
+
+ long currentTime = System.currentTimeMillis();
+ boolean found = false;
+ while ((currentTime - startTime) < timeout) {
+ sleep(2); // poll every two seconds
+
+ while ((line = reader.readLine()) != null) {
+ if (line.contains(searchText)) {
+ found = true;
+ break;
+ }
+ }
+
+ if (found) {
+ break;
+ }
+ currentTime = System.currentTimeMillis();
+ }
+
+ process.destroy();
+
+ if ((currentTime - startTime) >= timeout) {
+ throw new TimeoutException("Timed out waiting for Logcat text \"%s\"".format(searchText));
+ }
+ }
+
+ public void registerWatcher(String name, UiWatcher watcher) {
+ mDevice.registerWatcher(name, watcher);
+ }
+
+ public void runWatchers() {
+ mDevice.runWatchers();
+ }
+
+ public void removeWatcher(String name) {
+ mDevice.removeWatcher(name);
+ }
+
+ public void setScreenOrientation(ScreenOrientation orientation) throws Exception {
+ int width = mDevice.getDisplayWidth();
+ int height = mDevice.getDisplayHeight();
+ switch (orientation) {
+ case RIGHT:
+ mDevice.setOrientationRight();
+ break;
+ case NATURAL:
+ mDevice.setOrientationNatural();
+ break;
+ case LEFT:
+ mDevice.setOrientationLeft();
+ break;
+ case LANDSCAPE:
+ if (mDevice.isNaturalOrientation()){
+ if (height > width){
+ mDevice.setOrientationRight();
+ }
+ }
+ else {
+ if (height > width){
+ mDevice.setOrientationNatural();
+ }
+ }
+ break;
+ case PORTRAIT:
+ if (mDevice.isNaturalOrientation()){
+ if (height < width){
+ mDevice.setOrientationRight();
+ }
+ }
+ else {
+ if (height < width){
+ mDevice.setOrientationNatural();
+ }
+ }
+ break;
+ default:
+ throw new Exception("No orientation specified");
+ }
+ }
+
+ public void unsetScreenOrientation() throws Exception {
+ mDevice.unfreezeRotation();
+ }
+
+ public void uiObjectPerformLongClick(UiObject view, int steps) throws Exception {
+ Rect rect = view.getBounds();
+ mDevice.swipe(rect.centerX(), rect.centerY(),
+ rect.centerX(), rect.centerY(), steps);
+ }
+
+ public int getDisplayHeight() {
+ return mDevice.getDisplayHeight();
+ }
+
+ public int getDisplayWidth() {
+ return mDevice.getDisplayWidth();
+ }
+
+ public int getDisplayCentreWidth() {
+ return getDisplayWidth() / 2;
+ }
+
+ public int getDisplayCentreHeight() {
+ return getDisplayHeight() / 2;
+ }
+
+ public void tapDisplayCentre() {
+ tapDisplay(getDisplayCentreWidth(), getDisplayCentreHeight());
+ }
+
+ public void tapDisplay(int x, int y) {
+ mDevice.click(x, y);
+ }
+
+ public void pressEnter() {
+ mDevice.pressEnter();
+ }
+
+ public void pressHome() {
+ mDevice.pressHome();
+ }
+
+ public void pressBack() {
+ mDevice.pressBack();
+ }
+
+ public void uiObjectSwipe(UiObject view, Direction direction, int steps) throws Exception {
+ switch (direction) {
+ case UP:
+ view.swipeUp(steps);
+ break;
+ case DOWN:
+ view.swipeDown(steps);
+ break;
+ case LEFT:
+ view.swipeLeft(steps);
+ break;
+ case RIGHT:
+ view.swipeRight(steps);
+ break;
+ case NULL:
+ throw new Exception("No direction specified");
+ default:
+ break;
+ }
+ }
+
+ public void uiDeviceSwipeVertical(int startY, int endY, int xCoordinate, int steps) {
+ mDevice.swipe(xCoordinate, startY, xCoordinate, endY, steps);
+ }
+
+ public void uiDeviceSwipeHorizontal(int startX, int endX, int yCoordinate, int steps) {
+ mDevice.swipe(startX, yCoordinate, endX, yCoordinate, steps);
+ }
+
+ public void uiObjectVertPinchIn(UiObject view, int steps, int percent) throws Exception {
+ final int FINGER_TOUCH_HALF_WIDTH = 20;
+
+ // Make value between 1 and 100
+ int nPercent = (percent < 0) ? 1 : (percent > 100) ? 100 : percent;
+ float percentage = nPercent / 100f;
+
+ Rect rect = view.getVisibleBounds();
+
+ if (rect.width() <= FINGER_TOUCH_HALF_WIDTH * 2) {
+ throw new IllegalStateException("Object width is too small for operation");
+ }
+
+ // Start at the top-center and bottom-center of the control
+ Point startPoint1 = new Point(rect.centerX(), rect.centerY()
+ + (int) ((rect.height() / 2) * percentage));
+ Point startPoint2 = new Point(rect.centerX(), rect.centerY()
+ - (int) ((rect.height() / 2) * percentage));
+
+ // End at the same point at the center of the control
+ Point endPoint1 = new Point(rect.centerX(), rect.centerY() + FINGER_TOUCH_HALF_WIDTH);
+ Point endPoint2 = new Point(rect.centerX(), rect.centerY() - FINGER_TOUCH_HALF_WIDTH);
+
+ view.performTwoPointerGesture(startPoint1, startPoint2, endPoint1, endPoint2, steps);
+ }
+
+ public void uiObjectVertPinchOut(UiObject view, int steps, int percent) throws Exception {
+ final int FINGER_TOUCH_HALF_WIDTH = 20;
+
+ // Make value between 1 and 100
+ int nPercent = (percent < 0) ? 1 : (percent > 100) ? 100 : percent;
+ float percentage = nPercent / 100f;
+
+ Rect rect = view.getVisibleBounds();
+
+ if (rect.width() <= FINGER_TOUCH_HALF_WIDTH * 2) {
+ throw new IllegalStateException("Object width is too small for operation");
+ }
+
+ // Start from the same point at the center of the control
+ Point startPoint1 = new Point(rect.centerX(), rect.centerY() + FINGER_TOUCH_HALF_WIDTH);
+ Point startPoint2 = new Point(rect.centerX(), rect.centerY() - FINGER_TOUCH_HALF_WIDTH);
+
+ // End at the top-center and bottom-center of the control
+ Point endPoint1 = new Point(rect.centerX(), rect.centerY()
+ + (int) ((rect.height() / 2) * percentage));
+ Point endPoint2 = new Point(rect.centerX(), rect.centerY()
+ - (int) ((rect.height() / 2) * percentage));
+
+ view.performTwoPointerGesture(startPoint1, startPoint2, endPoint1, endPoint2, steps);
+ }
+
+ public void uiObjectVertPinch(UiObject view, PinchType direction,
+ int steps, int percent) throws Exception {
+ if (direction.equals(PinchType.IN)) {
+ uiObjectVertPinchIn(view, steps, percent);
+ } else if (direction.equals(PinchType.OUT)) {
+ uiObjectVertPinchOut(view, steps, percent);
+ }
+ }
+
+ public void uiDeviceSwipeUp(int steps) {
+ mDevice.swipe(
+ getDisplayCentreWidth(),
+ (getDisplayCentreHeight() + (getDisplayCentreHeight() / 2)),
+ getDisplayCentreWidth(),
+ (getDisplayCentreHeight() / 2),
+ steps);
+ }
+
+ public void uiDeviceSwipeDown(int steps) {
+ mDevice.swipe(
+ getDisplayCentreWidth(),
+ (getDisplayCentreHeight() / 2),
+ getDisplayCentreWidth(),
+ (getDisplayCentreHeight() + (getDisplayCentreHeight() / 2)),
+ steps);
+ }
+
+ public void uiDeviceSwipeLeft(int steps) {
+ mDevice.swipe(
+ (getDisplayCentreWidth() + (getDisplayCentreWidth() / 2)),
+ getDisplayCentreHeight(),
+ (getDisplayCentreWidth() / 2),
+ getDisplayCentreHeight(),
+ steps);
+ }
+
+ public void uiDeviceSwipeRight(int steps) {
+ mDevice.swipe(
+ (getDisplayCentreWidth() / 2),
+ getDisplayCentreHeight(),
+ (getDisplayCentreWidth() + (getDisplayCentreWidth() / 2)),
+ getDisplayCentreHeight(),
+ steps);
+ }
+
+ public void uiDeviceSwipe(Direction direction, int steps) throws Exception {
+ switch (direction) {
+ case UP:
+ uiDeviceSwipeUp(steps);
+ break;
+ case DOWN:
+ uiDeviceSwipeDown(steps);
+ break;
+ case LEFT:
+ uiDeviceSwipeLeft(steps);
+ break;
+ case RIGHT:
+ uiDeviceSwipeRight(steps);
+ break;
+ case NULL:
+ throw new Exception("No direction specified");
+ default:
+ break;
+ }
+ }
+
+ public void repeatClickUiObject(UiObject view, int repeatCount, int intervalInMillis) throws Exception {
+ int repeatInterval = intervalInMillis > CLICK_REPEAT_INTERVAL_MINIMUM
+ ? intervalInMillis : CLICK_REPEAT_INTERVAL_DEFAULT;
+ if (repeatCount < 1 || !view.isClickable()) {
+ return;
+ }
+
+ for (int i = 0; i < repeatCount; ++i) {
+ view.click();
+ SystemClock.sleep(repeatInterval); // in order to register as separate click
+ }
+ }
+
+
+ public UiObject clickUiObject(FindByCriteria criteria, String matching) throws Exception {
+ return clickUiObject(criteria, matching, null, false);
+ }
+
+ public UiObject clickUiObject(FindByCriteria criteria, String matching, boolean wait) throws Exception {
+ return clickUiObject(criteria, matching, null, wait);
+ }
+
+ public UiObject clickUiObject(FindByCriteria criteria, String matching, String clazz) throws Exception {
+ return clickUiObject(criteria, matching, clazz, false);
+ }
+
+ public UiObject clickUiObject(FindByCriteria criteria, String matching, String clazz, boolean wait) throws Exception {
+ UiObject view;
+
+ switch (criteria) {
+ case BY_ID:
+ view = (clazz == null)
+ ? getUiObjectByResourceId(matching) : getUiObjectByResourceId(matching, clazz);
+ break;
+ case BY_DESC:
+ view = (clazz == null)
+ ? getUiObjectByDescription(matching) : getUiObjectByDescription(matching, clazz);
+ break;
+ case BY_TEXT:
+ default:
+ view = (clazz == null)
+ ? getUiObjectByText(matching) : getUiObjectByText(matching, clazz);
+ break;
+ }
+
+ if (wait) {
+ view.clickAndWaitForNewWindow();
+ } else {
+ view.click();
+ }
+ return view;
+ }
+
+ public UiObject getUiObjectByResourceId(String resourceId, String className) throws Exception {
+ return getUiObjectByResourceId(resourceId, className, uiAutoTimeout);
+ }
+
+ public UiObject getUiObjectByResourceId(String resourceId, String className, long timeout) throws Exception {
+ UiObject object = mDevice.findObject(new UiSelector().resourceId(resourceId)
+ .className(className));
+ if (!object.waitForExists(timeout)) {
+ throw new UiObjectNotFoundException(String.format("Could not find \"%s\" \"%s\"",
+ resourceId, className));
+ }
+ return object;
+ }
+
+ public UiObject getUiObjectByResourceId(String id) throws Exception {
+ UiObject object = mDevice.findObject(new UiSelector().resourceId(id));
+
+ if (!object.waitForExists(uiAutoTimeout)) {
+ throw new UiObjectNotFoundException("Could not find view with resource ID: " + id);
+ }
+ return object;
+ }
+
+ public UiObject getUiObjectByDescription(String description, String className) throws Exception {
+ return getUiObjectByDescription(description, className, uiAutoTimeout);
+ }
+
+ public UiObject getUiObjectByDescription(String description, String className, long timeout) throws Exception {
+ UiObject object = mDevice.findObject(new UiSelector().descriptionContains(description)
+ .className(className));
+ if (!object.waitForExists(timeout)) {
+ throw new UiObjectNotFoundException(String.format("Could not find \"%s\" \"%s\"",
+ description, className));
+ }
+ return object;
+ }
+
+ public UiObject getUiObjectByDescription(String desc) throws Exception {
+ UiObject object = mDevice.findObject(new UiSelector().descriptionContains(desc));
+
+ if (!object.waitForExists(uiAutoTimeout)) {
+ throw new UiObjectNotFoundException("Could not find view with description: " + desc);
+ }
+ return object;
+ }
+
+ public UiObject getUiObjectByText(String text, String className) throws Exception {
+ return getUiObjectByText(text, className, uiAutoTimeout);
+ }
+
+ public UiObject getUiObjectByText(String text, String className, long timeout) throws Exception {
+ UiObject object = mDevice.findObject(new UiSelector().textContains(text)
+ .className(className));
+ if (!object.waitForExists(timeout)) {
+ throw new UiObjectNotFoundException(String.format("Could not find \"%s\" \"%s\"",
+ text, className));
+ }
+ return object;
+ }
+
+ public UiObject getUiObjectByText(String text) throws Exception {
+ UiObject object = mDevice.findObject(new UiSelector().textContains(text));
+
+ if (!object.waitForExists(uiAutoTimeout)) {
+ throw new UiObjectNotFoundException("Could not find view with text: " + text);
+ }
+ return object;
+ }
+
+ // Helper to select a folder in the gallery
+ public void selectGalleryFolder(String directory) throws Exception {
+ UiObject workdir =
+ mDevice.findObject(new UiSelector().text(directory)
+ .className("android.widget.TextView"));
+ UiScrollable scrollView =
+ new UiScrollable(new UiSelector().scrollable(true));
+
+ // If the folder is not present wait for a short time for
+ // the media server to refresh its index.
+ boolean discovered = workdir.waitForExists(TimeUnit.SECONDS.toMillis(10));
+ if (!discovered && scrollView.exists()) {
+ // First check if the directory is visible on the first
+ // screen and if not scroll to the bottom of the screen to look for it.
+ discovered = scrollView.scrollIntoView(workdir);
+
+ // If still not discovered scroll back to the top of the screen and
+ // wait for a longer amount of time for the media server to refresh
+ // its index.
+ if (!discovered) {
+ // scrollView.scrollToBeggining() doesn't work for this
+ // particular scrollable view so use device method instead
+ for (int i = 0; i < 10; i++) {
+ uiDeviceSwipeUp(20);
+ }
+ discovered = workdir.waitForExists(TimeUnit.SECONDS.toMillis(60));
+
+ // Scroll to the bottom of the screen one last time
+ if (!discovered) {
+ discovered = scrollView.scrollIntoView(workdir);
+ }
+ }
+ }
+
+ if (discovered) {
+ workdir.clickAndWaitForNewWindow();
+ } else {
+ throw new UiObjectNotFoundException("Could not find folder : " + directory);
+ }
+ }
+
+
+ // If an an app is not designed for running on the latest version of android
+ // (currently Q) an additional screen can popup asking to confirm permissions.
+ public void dismissAndroidPermissionPopup() throws Exception {
+ UiObject permissionAccess =
+ mDevice.findObject(new UiSelector().textMatches(
+ ".*Choose what to allow .* to access"));
+ UiObject continueButton =
+ mDevice.findObject(new UiSelector().resourceId("com.android.permissioncontroller:id/continue_button")
+ .textContains("Continue"));
+ if (permissionAccess.exists() && continueButton.exists()) {
+ continueButton.click();
+ }
+ }
+
+
+ // If an an app is not designed for running on the latest version of android
+ // (currently Q) dissmiss the warning popup if present.
+ public void dismissAndroidVersionPopup() throws Exception {
+
+ // Ensure we have dissmied any permission screens before looking for the version popup
+ dismissAndroidPermissionPopup();
+
+ UiObject warningText =
+ mDevice.findObject(new UiSelector().textContains(
+ "This app was built for an older version of Android"));
+ UiObject acceptButton =
+ mDevice.findObject(new UiSelector().resourceId("android:id/button1")
+ .className("android.widget.Button"));
+ if (warningText.exists() && acceptButton.exists()) {
+ acceptButton.click();
+ }
+ }
+
+
+ // If Chrome is a fresh install then these popups may be presented
+ // dismiss them if visible.
+ public void dismissChromePopup() throws Exception {
+ UiObject accept =
+ mDevice.findObject(new UiSelector().resourceId("com.android.chrome:id/terms_accept")
+ .className("android.widget.Button"));
+ if (accept.waitForExists(3000)){
+ accept.click();
+ UiObject negative =
+ mDevice.findObject(new UiSelector().resourceId("com.android.chrome:id/negative_button")
+ .className("android.widget.Button"));
+ if (negative.waitForExists(10000)) {
+ negative.click();
+ }
+ }
+ UiObject lite =
+ mDevice.findObject(new UiSelector().resourceId("com.android.chrome:id/button_secondary")
+ .className("android.widget.Button"));
+ if (lite.exists()){
+ lite.click();
+ }
+ }
+
+ // Override getParams function to decode a url encoded parameter bundle before
+ // passing it to workloads.
+ public Bundle getParams() {
+ // Get the original parameter bundle
+ Bundle parameters = getArguments();
+
+ // Decode each parameter in the bundle, except null values and "class", as this
+ // used to control instrumentation and therefore not encoded.
+ for (String key : parameters.keySet()) {
+ String param = parameters.getString(key);
+ if (param != null && !key.equals("class")) {
+ param = android.net.Uri.decode(param);
+ parameters = decode(parameters, key, param);
+ }
+ }
+ return parameters;
+ }
+
+ // Helper function to decode a string and insert it as an appropriate type
+ // into a provided bundle with its key.
+ // Each bundle parameter will be a urlencoded string with 2 characters prefixed to the value
+ // used to store the original type information, e.g. 'fl' -> list of floats.
+ private Bundle decode(Bundle parameters, String key, String value) {
+ char value_type = value.charAt(0);
+ char value_dimension = value.charAt(1);
+ String param = value.substring(2);
+
+ if (value_dimension == 's') {
+ if (value_type == 's') {
+ parameters.putString(key, param);
+ } else if (value_type == 'f') {
+ parameters.putFloat(key, Float.parseFloat(param));
+ } else if (value_type == 'd') {
+ parameters.putDouble(key, Double.parseDouble(param));
+ } else if (value_type == 'b') {
+ parameters.putBoolean(key, Boolean.parseBoolean(param));
+ } else if (value_type == 'i') {
+ parameters.putInt(key, Integer.parseInt(param));
+ } else if (value_type == 'n') {
+ parameters.putString(key, "None");
+ } else {
+ throw new IllegalArgumentException("Error decoding:" + key + value
+ + " - unknown format");
+ }
+ } else if (value_dimension == 'l') {
+ return decodeArray(parameters, key, value_type, param);
+ } else {
+ throw new IllegalArgumentException("Error decoding:" + key + value
+ + " - unknown format");
+ }
+ return parameters;
+ }
+
+ // Helper function to deal with decoding arrays and update the bundle with
+ // an appropriate array type. The string "0newelement0" is used to distinguish
+ // each element from each other in the array when encoded.
+ private Bundle decodeArray(Bundle parameters, String key, char type, String value) {
+ String[] string_list = value.split("0newelement0");
+ if (type == 's') {
+ parameters.putStringArray(key, string_list);
+ }
+ else if (type == 'i') {
+ int[] int_list = new int[string_list.length];
+ for (int i = 0; i < string_list.length; i++){
+ int_list[i] = Integer.parseInt(string_list[i]);
+ }
+ parameters.putIntArray(key, int_list);
+ } else if (type == 'f') {
+ float[] float_list = new float[string_list.length];
+ for (int i = 0; i < string_list.length; i++){
+ float_list[i] = Float.parseFloat(string_list[i]);
+ }
+ parameters.putFloatArray(key, float_list);
+ } else if (type == 'd') {
+ double[] double_list = new double[string_list.length];
+ for (int i = 0; i < string_list.length; i++){
+ double_list[i] = Double.parseDouble(string_list[i]);
+ }
+ parameters.putDoubleArray(key, double_list);
+ } else if (type == 'b') {
+ boolean[] boolean_list = new boolean[string_list.length];
+ for (int i = 0; i < string_list.length; i++){
+ boolean_list[i] = Boolean.parseBoolean(string_list[i]);
+ }
+ parameters.putBooleanArray(key, boolean_list);
+ } else {
+ throw new IllegalArgumentException("Error decoding array: " +
+ value + " - unknown format");
+ }
+ return parameters;
+ }
+}
diff --git a/wa/framework/uiauto-androidx/app/src/main/java/com/arm/wa/uiauto/UiAutoUtils.java b/wa/framework/uiauto-androidx/app/src/main/java/com/arm/wa/uiauto/UiAutoUtils.java
new file mode 100644
index 000000000..88f3e0cb5
--- /dev/null
+++ b/wa/framework/uiauto-androidx/app/src/main/java/com/arm/wa/uiauto/UiAutoUtils.java
@@ -0,0 +1,35 @@
+/* Copyright 2013-2024 ARM Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.arm.wa.uiauto;
+
+import android.os.Bundle;
+
+public final class UiAutoUtils {
+
+ /** Construct launch command of an application. */
+ public static String createLaunchCommand(Bundle parameters) {
+ String launchCommand;
+ String activityName = parameters.getString("launch_activity");
+ String packageName = parameters.getString("package_name");
+ if (activityName.equals("None")) {
+ launchCommand = String.format("am start --user -3 %s", packageName);
+ }
+ else {
+ launchCommand = String.format("am start --user -3 -n %s/%s", packageName, activityName);
+ }
+ return launchCommand;
+ }
+}
diff --git a/wa/framework/uiauto-androidx/app/src/main/java/com/arm/wa/uiauto/UxPerfUiAutomation.java b/wa/framework/uiauto-androidx/app/src/main/java/com/arm/wa/uiauto/UxPerfUiAutomation.java
new file mode 100644
index 000000000..7db184c49
--- /dev/null
+++ b/wa/framework/uiauto-androidx/app/src/main/java/com/arm/wa/uiauto/UxPerfUiAutomation.java
@@ -0,0 +1,55 @@
+/* Copyright 2013-2024 ARM Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.arm.wa.uiauto;
+
+import android.os.Bundle;
+
+import java.util.logging.Logger;
+
+import com.arm.wa.uiauto.BaseUiAutomation.Direction;
+import com.arm.wa.uiauto.BaseUiAutomation.PinchType;
+
+
+public class UxPerfUiAutomation {
+
+ private Logger logger = Logger.getLogger(UxPerfUiAutomation.class.getName());
+
+ public enum GestureType { UIDEVICE_SWIPE, UIOBJECT_SWIPE, PINCH };
+
+ public static class GestureTestParams {
+ public GestureType gestureType;
+ public Direction gestureDirection;
+ public PinchType pinchType;
+ public int percent;
+ public int steps;
+
+ public GestureTestParams(GestureType gesture, Direction direction, int steps) {
+ this.gestureType = gesture;
+ this.gestureDirection = direction;
+ this.pinchType = PinchType.NULL;
+ this.steps = steps;
+ this.percent = 0;
+ }
+
+ public GestureTestParams(GestureType gesture, PinchType pinchType, int steps, int percent) {
+ this.gestureType = gesture;
+ this.gestureDirection = Direction.NULL;
+ this.pinchType = pinchType;
+ this.steps = steps;
+ this.percent = percent;
+ }
+ }
+}
diff --git a/wa/framework/uiauto-androidx/build.gradle b/wa/framework/uiauto-androidx/build.gradle
new file mode 100644
index 000000000..95f37533a
--- /dev/null
+++ b/wa/framework/uiauto-androidx/build.gradle
@@ -0,0 +1,25 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ repositories {
+ google()
+ mavenCentral()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:8.5.0'
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/wa/framework/uiauto-androidx/build.sh b/wa/framework/uiauto-androidx/build.sh
new file mode 100755
index 000000000..b9d276256
--- /dev/null
+++ b/wa/framework/uiauto-androidx/build.sh
@@ -0,0 +1,32 @@
+#!/bin/bash
+# Copyright 2013-2024 ARM Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+set -e
+
+# Ensure gradelw exists before starting
+if [[ ! -f gradlew ]]; then
+ echo 'gradlew file not found! Check that you are in the right directory.'
+ exit 9
+fi
+
+# Build and return appropriate exit code if failed
+./gradlew clean :app:assembleDebug
+exit_code=$?
+if [ $exit_code -ne 0 ]; then
+ echo "ERROR: 'gradle build' exited with code $exit_code"
+ exit $exit_code
+fi
+
+cp app/build/outputs/aar/app-debug.aar ./uiauto.aar
diff --git a/wa/framework/uiauto-androidx/gradle.properties b/wa/framework/uiauto-androidx/gradle.properties
new file mode 100644
index 000000000..5bac8ac50
--- /dev/null
+++ b/wa/framework/uiauto-androidx/gradle.properties
@@ -0,0 +1 @@
+android.useAndroidX=true
diff --git a/wa/framework/uiauto-androidx/gradle/wrapper/gradle-wrapper.jar b/wa/framework/uiauto-androidx/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..13372aef5
Binary files /dev/null and b/wa/framework/uiauto-androidx/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/wa/framework/uiauto-androidx/gradle/wrapper/gradle-wrapper.properties b/wa/framework/uiauto-androidx/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..e899a313c
--- /dev/null
+++ b/wa/framework/uiauto-androidx/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Tue 27 Aug 2024 12:28:27 PM BST
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-all.zip
diff --git a/wa/framework/uiauto-androidx/gradlew b/wa/framework/uiauto-androidx/gradlew
new file mode 100755
index 000000000..9d82f7891
--- /dev/null
+++ b/wa/framework/uiauto-androidx/gradlew
@@ -0,0 +1,160 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/wa/framework/uiauto-androidx/gradlew.bat b/wa/framework/uiauto-androidx/gradlew.bat
new file mode 100644
index 000000000..8a0b282aa
--- /dev/null
+++ b/wa/framework/uiauto-androidx/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/wa/framework/uiauto-androidx/settings.gradle b/wa/framework/uiauto-androidx/settings.gradle
new file mode 100644
index 000000000..e7b4def49
--- /dev/null
+++ b/wa/framework/uiauto-androidx/settings.gradle
@@ -0,0 +1 @@
+include ':app'
diff --git a/wa/framework/uiauto-androidx/uiauto.aar b/wa/framework/uiauto-androidx/uiauto.aar
new file mode 100644
index 000000000..7e6a650d8
Binary files /dev/null and b/wa/framework/uiauto-androidx/uiauto.aar differ
diff --git a/wa/framework/workload.py b/wa/framework/workload.py
index 5abb90e3f..75939feb7 100644
--- a/wa/framework/workload.py
+++ b/wa/framework/workload.py
@@ -16,6 +16,7 @@
import os
import threading
import time
+import re
try:
from shlex import quote
@@ -395,6 +396,13 @@ def setup(self, context):
super(ApkUiautoWorkload, self).setup(context)
+class ApkUiautoJankTestWorkload(ApkUiautoWorkload):
+
+ def __init__(self, target, **kwargs):
+ super(ApkUiautoJankTestWorkload, self).__init__(target, **kwargs)
+ self.gui = UiAutomatorJankTestGUI(self)
+
+
class ApkReventWorkload(ApkUIWorkload):
# May be optionally overwritten by subclasses
@@ -580,6 +588,82 @@ def _execute(self, stage, timeout):
time.sleep(2)
+class UiAutomatorJankTestGUI(UiAutomatorGUI):
+
+ # The list of jank tests, within the testing class, that should
+ # be invoked.
+ jank_tests = []
+ # The default class for jank testing.
+ uiauto_jank_class = 'UiAutomationJankTests'
+ # The default runner for the jank tests, using androidx.
+ uiauto_runner = 'androidx.test.runner.AndroidJUnitRunner'
+ # Output of each of the executed tests.
+ output = {}
+
+ # A couple regular expressions used to parse frame metrics output by the
+ # android jank tests.
+ _OUTPUT_SECTION_REGEX = re.compile(
+ r'(\s*INSTRUMENTATION_STATUS: gfx-[\w-]+=[-+\d.]+\n)+'
+ r'\s*INSTRUMENTATION_STATUS_CODE: (?P[-+\d]+)\n?', re.M)
+ _OUTPUT_GFXINFO_REGEX = re.compile(
+ r'INSTRUMENTATION_STATUS: (?P[\w-]+)=(?P[-+\d.]+)')
+
+ def init_commands(self):
+ # Let UiAutomatorGUI handle the initialization of instrumented test
+ # commands.
+ super(UiAutomatorJankTestGUI, self).init_commands()
+
+ # Now initialize the jank test commands.
+ if not self.jank_tests:
+ raise RuntimeError('List of jank tests is empty')
+
+ params_dict = self.uiauto_params
+ params_dict['workdir'] = self.target.working_directory
+ params = ''
+ for k, v in params_dict.iter_encoded_items():
+ params += ' -e {} {}'.format(k, v)
+
+ for test in self.jank_tests:
+ class_string = '{}.{}#{}'.format(self.uiauto_package,
+ self.uiauto_jank_class,
+ test)
+ instrumentation_string = '{}/{}'.format(self.uiauto_package,
+ self.uiauto_runner)
+ cmd_template = 'am instrument -w -r{} -e class {} {}'
+ self.commands[test] = cmd_template.format(params, class_string,
+ instrumentation_string)
+
+ def run(self, timeout=None):
+ if not self.commands:
+ raise RuntimeError('Commands have not been initialized')
+
+ # Validate that each test has been initialized with their own set of commands.
+ for test in self.jank_tests:
+ if not self.commands[test]:
+ raise RuntimeError('Commands for test "{}" not initialized'.format(test))
+
+ # Run the jank tests and capture output for each one of them.
+ for test in self.jank_tests:
+ self.output[test] = self.target.execute(self.commands[test], self.timeout)
+
+ if 'FAILURE' in self.output[test]:
+ raise WorkloadError(self.output[test])
+ else:
+ self.logger.debug(self.output[test])
+
+ def parse_metrics(self, context):
+ # Parse the test results and filter out the metrics so we can output
+ # a meaningful results file.
+ for test, test_output in self.output.items():
+ for section in self._OUTPUT_SECTION_REGEX.finditer(test_output):
+ if int(section.group('code')) != -1:
+ msg = 'Run failed (INSTRUMENTATION_STATUS_CODE: {}). See log.'
+ raise RuntimeError(msg.format(section.group('code')))
+ for metric in self._OUTPUT_GFXINFO_REGEX.finditer(section.group()):
+ context.add_metric(metric.group('name'), metric.group('value'),
+ classifiers={'test_name': test})
+
+
class ReventGUI(object):
def __init__(self, workload, target, setup_timeout, run_timeout,
diff --git a/wa/workloads/jetnews/__init__.py b/wa/workloads/jetnews/__init__.py
new file mode 100755
index 000000000..f75e3ef0d
--- /dev/null
+++ b/wa/workloads/jetnews/__init__.py
@@ -0,0 +1,85 @@
+# Copyright 2024 ARM Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from wa import Parameter, ApkUiautoJankTestWorkload, TestPackageHandler
+
+from wa.utils.types import list_of_strs
+
+
+class Jetnews(ApkUiautoJankTestWorkload): # pylint: disable=too-many-ancestors
+
+ name = 'jetnews'
+ package_names = ['com.example.jetnews']
+ description = '''
+ This workload uses the JetNews sample app to run a set of UiAutomation
+ tests, with the goal of gathering frame metrics and calculating jank
+ frame percentages.
+
+ It uses two APK's, the JetNews app itself (modified to contain more posts)
+ and the UiAutomation tests that interact with the app.
+
+ There are 3 available tests, two in portrait mode and 1 in landscape mode.
+
+ Please note the UiAutomation APK bundled with Workload Automation requires
+ Android 9 (API level 28) to work correctly, otherwise it will fail to
+ install.
+ '''
+
+ default_test_strings = [
+ 'PortraitVerticalTest',
+ 'PortraitHorizontalTest',
+ 'LandscapeVerticalTest',
+ ]
+
+ # List of jank tests to invoke for this workload.
+ jetnews_jank_tests = ['test1']
+
+ parameters = [
+ Parameter('tests', kind=list_of_strs,
+ description="""
+ List of tests to be executed. The available
+ tests are PortraitVerticalTest, LandscapeVerticalTest and
+ PortraitHorizontalTest. If none are specified, the default
+ is to run all of them.
+ """, default=default_test_strings,
+ constraint=lambda x: all(v in ['PortraitVerticalTest', 'PortraitHorizontalTest', 'LandscapeVerticalTest'] for v in x)),
+ Parameter('flingspeed', kind=int,
+ description="""
+ Default fling speed for the tests. The default is 15000 and
+ the minimum value is 1000. If the value is too small, it will
+ take longer for the test to run.
+ """, default=15000, constraint=lambda x: x >= 1000),
+ Parameter('repeat', kind=int,
+ description="""
+ The number of times the tests should be repeated. The default
+ is 1.
+ """, default=1, constraint=lambda x: x > 0)
+ ]
+
+ def __init__(self, target, **kwargs):
+ super(Jetnews, self).__init__(target, **kwargs)
+ self.gui.jank_tests = self.jetnews_jank_tests
+ self.gui.uiauto_params['tests'] = self.tests
+ self.gui.uiauto_params['flingspeed'] = self.flingspeed
+ self.gui.uiauto_params['repeat'] = self.repeat
+
+ def run(self, context):
+ # Run the jank tests.
+ self.gui.run()
+
+ def update_output(self, context):
+ super(Jetnews, self).update_output(context)
+ # Parse the frame metrics and output the results file.
+ self.gui.parse_metrics(context)
diff --git a/wa/workloads/jetnews/com.arm.wa.uiauto.jetnews.apk b/wa/workloads/jetnews/com.arm.wa.uiauto.jetnews.apk
new file mode 100644
index 000000000..f00fe5434
Binary files /dev/null and b/wa/workloads/jetnews/com.arm.wa.uiauto.jetnews.apk differ
diff --git a/wa/workloads/jetnews/uiauto/app/build.gradle b/wa/workloads/jetnews/uiauto/app/build.gradle
new file mode 100644
index 000000000..4be0393b5
--- /dev/null
+++ b/wa/workloads/jetnews/uiauto/app/build.gradle
@@ -0,0 +1,50 @@
+plugins {
+ id 'com.android.application'
+ id "org.jetbrains.kotlin.android" version "2.0.20-Beta1"
+}
+
+kotlin {
+ // Standardize on the same jvm version for compatibility reasons.
+ jvmToolchain(17)
+}
+
+def packageName = "com.arm.wa.uiauto.jetnews"
+
+android {
+ namespace = "com.arm.wa.uiauto.jetnews"
+
+ compileSdkVersion 34
+ defaultConfig {
+ applicationId "${packageName}"
+ minSdkVersion 28
+ targetSdkVersion 34
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+ buildTypes {
+ applicationVariants.all { variant ->
+ variant.outputs.each { output ->
+ output.outputFileName = "${packageName}.apk"
+ }
+ }
+ }
+ useLibrary 'android.test.base'
+}
+
+dependencies {
+ implementation fileTree(include: ['*.jar'], dir: 'libs')
+ implementation 'androidx.test.uiautomator:uiautomator:2.4.0-alpha01'
+ implementation 'androidx.test.janktesthelper:janktesthelper:1.0.1'
+ implementation 'androidx.test.espresso:espresso-core:3.5.1'
+
+ implementation(name: 'uiauto', ext: 'aar')
+}
+
+repositories {
+ flatDir {
+ dirs 'libs'
+ }
+}
+
+tasks.withType(JavaCompile) {
+ options.compilerArgs += ['-Xlint:deprecation']
+}
diff --git a/wa/workloads/jetnews/uiauto/app/src/main/AndroidManifest.xml b/wa/workloads/jetnews/uiauto/app/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..1d89fe34f
--- /dev/null
+++ b/wa/workloads/jetnews/uiauto/app/src/main/AndroidManifest.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
diff --git a/wa/workloads/jetnews/uiauto/app/src/main/java/com/arm/wa/uiauto/jetnews/UiAutomation.java b/wa/workloads/jetnews/uiauto/app/src/main/java/com/arm/wa/uiauto/jetnews/UiAutomation.java
new file mode 100644
index 000000000..ae0f28e38
--- /dev/null
+++ b/wa/workloads/jetnews/uiauto/app/src/main/java/com/arm/wa/uiauto/jetnews/UiAutomation.java
@@ -0,0 +1,81 @@
+/* Copyright 2024 ARM Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.arm.wa.uiauto.jetnews;
+
+import androidx.test.uiautomator.UiObject;
+import androidx.test.uiautomator.UiSelector;
+
+import android.os.Bundle;
+
+import com.arm.wa.uiauto.ApplaunchInterface;
+import com.arm.wa.uiauto.BaseUiAutomation;
+import com.arm.wa.uiauto.UiAutoUtils;
+
+import org.junit.Test;
+
+// Dummy workload for jetnews. We need to use the JankTestBase class but we
+// can't inherit from that class as we already inherit BaseUiAutomation.
+// Therefore we have another class (UiAutomationJankTests) that uses
+// this class instead.
+
+public class UiAutomation extends BaseUiAutomation implements ApplaunchInterface {
+
+ protected Bundle parameters;
+ protected String packageID;
+
+ public void initialize() {
+ parameters = getParams();
+ packageID = getPackageID(parameters);
+ }
+
+ @Test
+ public void setup() throws Exception {
+ setScreenOrientation(ScreenOrientation.NATURAL);
+ }
+
+ @Test
+ public void runWorkload() {
+ // Intentionally empty, not used.
+ }
+
+ @Test
+ public void teardown() throws Exception {
+ unsetScreenOrientation();
+ }
+
+ public void runApplicationSetup() throws Exception {
+ // Intentionally empty, not used.
+ }
+
+ // Sets the UiObject that marks the end of the application launch.
+ public UiObject getLaunchEndObject() {
+ // Intentionally empty, not used.
+ return null;
+ }
+
+ // Returns the launch command for the application.
+ public String getLaunchCommand() {
+ // Intentionally empty, not used.
+ return "";
+ }
+
+ // Pass the workload parameters, used for applaunch
+ public void setWorkloadParameters(Bundle workload_parameters) {
+ // Intentionally empty, not used.
+ }
+}
+
+
diff --git a/wa/workloads/jetnews/uiauto/app/src/main/java/com/arm/wa/uiauto/jetnews/UiAutomationJankTests.java b/wa/workloads/jetnews/uiauto/app/src/main/java/com/arm/wa/uiauto/jetnews/UiAutomationJankTests.java
new file mode 100644
index 000000000..cd559dbc1
--- /dev/null
+++ b/wa/workloads/jetnews/uiauto/app/src/main/java/com/arm/wa/uiauto/jetnews/UiAutomationJankTests.java
@@ -0,0 +1,386 @@
+/* Copyright 2024 ARM Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.arm.wa.uiauto.jetnews;
+
+import androidx.test.jank.GfxMonitor;
+import androidx.test.jank.JankTest;
+import androidx.test.jank.JankTestBase;
+import android.os.Bundle;
+
+// UiAutomator 1 imports.
+import androidx.test.uiautomator.UiScrollable;
+import androidx.test.uiautomator.UiSelector;
+import androidx.test.uiautomator.UiObject;
+
+// UiAutomator 2 imports.
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.Direction;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.UiObject2;
+import androidx.test.uiautomator.Until;
+
+import androidx.test.espresso.matcher.ViewMatchers;
+import org.hamcrest.CoreMatchers;
+
+import android.util.Log;
+
+import java.util.Arrays;
+
+import com.arm.wa.uiauto.ActionLogger;
+
+// This class is responsible for actually running the UiAutomation
+// tests and measuring the frame metrics. It will be invoked directly
+// by workload-automation. We use an instance of UiAutomation so
+// things get setup properly (parameters, screen orientation etc).
+
+public class UiAutomationJankTests extends JankTestBase {
+ private static final int DEFAULT_BENCHMARK_REPEAT_COUNT = 1;
+ private static final int DEFAULT_TIMEOUT = 1000;
+ private static final int DEFAULT_BENCHMARK_FLING_SPEED = 5000;
+ private static final String[] DEFAULT_BENCHMARK_TESTS
+ = {"PortraitVerticalTest",
+ "PortraitHorizontalTest",
+ "LandscapeVerticalTest"};
+ private static final String PACKAGE_NAME = "com.example.jetnews";
+ private static final String LOG_TAG = "JetNewsJankTests: ";
+
+ private static final String MAIN_VIEW = "ArticlesMainScrollView";
+ private static final String TOP_ARTICLE = "TopStoriesForYou";
+ private static final String FIRST_POST = "PostCardSimple0";
+ private static final String FIRST_POST_CONTENT = "PostContent0";
+ private static final String ARTICLE_VIEW = "ArticleView";
+ private static final String BOTTOM_ARTICLE = "PostCardHistory19";
+ private static final String POPULAR_LIST = "PopularOnJetnewsRow";
+ private static final String ARTICLE_PREVIEW = "ArticleHomeScreenPreview0";
+ private static final String FIRST_POPULAR_CARD = "PostCardPopular0";
+ private static final String LAST_POPULAR_CARD = "PostCardPopular10";
+
+ private UiAutomation mUiAutomation;
+ private int repeat;
+ private int fling_speed;
+ private String[] tests;
+ private UiDevice mDevice;
+ private boolean testPortraitVertical;
+ private boolean testPortraitHorizontal;
+ private boolean testLandscapeVertical;
+
+ @JankTest(
+ expectedFrames = 100,
+ defaultIterationCount = 1
+ )
+
+ @GfxMonitor(processName = PACKAGE_NAME)
+ public void test1() throws Exception {
+ for (int i = 0; i < repeat; i++) {
+ if (testPortraitVertical) {
+ ActionLogger logger
+ = new ActionLogger("PortraitVerticalTest",
+ mUiAutomation.getParams());
+ logger.start();
+ resetAppState();
+ runPortraitVerticalTests();
+ logger.stop();
+ }
+
+ if (testPortraitHorizontal) {
+ ActionLogger logger
+ = new ActionLogger("PortraitHorizontalTest",
+ mUiAutomation.getParams());
+ logger.start();
+ resetAppState();
+ runPortraitHorizontalTests();
+ logger.stop();
+ }
+
+ if (testLandscapeVertical) {
+ ActionLogger logger
+ = new ActionLogger("LandscapeVerticalTest",
+ mUiAutomation.getParams());
+ logger.start();
+ resetAppState();
+ runLandscapeVerticalTests();
+ logger.stop();
+ }
+ }
+ }
+
+ // Returns true if the main view is in focus. False otherwise.
+ private boolean findMainView() throws Exception {
+ return mDevice.wait(Until.findObject(By.res(MAIN_VIEW)), DEFAULT_TIMEOUT) != null;
+ }
+
+ // Scroll the object with resource id ARTICLES_ID to the bottom end and
+ // back to the top end. It is also used to scroll sideways if the controls
+ // allow such movement.
+ private void scrollList(String articles_id, String top_id, String bottom_id,
+ boolean sideways) throws Exception {
+ // Scroll down and up in the articles list.
+ assert(scrollTo(articles_id, bottom_id, true, top_id,
+ bottom_id, sideways, true, fling_speed));
+ assert(scrollTo(articles_id, top_id, false, top_id,
+ bottom_id, sideways, true, fling_speed));
+ }
+
+ // Scroll the object with resource id ARTICLES_ID down until the object with
+ // resource id ARTICLE_NAME is visible and return true. If the object is not
+ // visible, return false.
+ private boolean scrollToArticle(String articles_id,
+ String article_name) throws Exception {
+ // Scroll downwards until we find the item named ARTICLE_NAME on screen.
+ // We reduce the fling speed so we don't skip past it on devices with
+ // screens that are too small (less area to display things) or too
+ // big (fast scrolling).
+ scrollTo(articles_id, article_name, true, TOP_ARTICLE, BOTTOM_ARTICLE,
+ false, true, 1000);
+
+ return mDevice.findObject(By.res(article_name)) != null;
+ }
+
+ // Assuming an object with resource id ARTICLE_ID is in view, click it,
+ // wait for the article to open, fling downwards and upwards.
+ //
+ // This is shared with landscape mode as well, so we don't try to back out
+ // from the opened article, since landscape mode presents a split view of
+ // the scroll list and the article's contents.
+ private void interactWithArticle(String article_id) throws Exception {
+ UiObject2 article
+ = mDevice.wait(Until.findObject(By.res(article_id)),
+ DEFAULT_TIMEOUT);
+
+ ViewMatchers.assertThat(article, CoreMatchers.notNullValue());
+
+ article.click();
+
+ // Wait for the clicked article to appear.
+ UiObject2 article_view
+ = mDevice.wait(Until.findObject(By.res(ARTICLE_PREVIEW)),
+ DEFAULT_TIMEOUT);
+
+ // If it is a small screen device or portrait mode, we may not have a
+ // preview window, so look for a fullscreen article view.
+ if (article_view == null) {
+
+ article_view
+ = mDevice.wait(Until.findObject(By.res(FIRST_POST_CONTENT)),
+ DEFAULT_TIMEOUT);
+ }
+
+ // Interact with the opened article by flinging up and down once.
+ ViewMatchers.assertThat(article_view, CoreMatchers.notNullValue());
+ article_view.setGestureMarginPercentage(0.2f);
+ article_view.fling(Direction.DOWN, fling_speed);
+ article_view.fling(Direction.UP, fling_speed);
+
+ UiObject2 refresh_button
+ = mDevice.wait(Until.findObject(By.text("Retry")),
+ DEFAULT_TIMEOUT);
+
+ if (refresh_button != null)
+ refresh_button.click();
+ }
+
+ // Reset the app state for a new test.
+ private void resetAppState() throws Exception {
+ mDevice.setOrientationPortrait();
+
+ // FIXUP for differences between tablets and small phones.
+ // Sometimes, when flipping back from landscape to portrait, the app
+ // will switch to a view of the article, and we might need to back out
+ // to the main view.
+ UiObject2 article_view
+ = mDevice.wait(Until.findObject(By.res(FIRST_POST_CONTENT)),
+ DEFAULT_TIMEOUT);
+
+ // If we see the article view, back out from it.
+ if (article_view != null)
+ mDevice.pressBack();
+
+ mDevice.setOrientationNatural();
+
+ // Now make sure the main view is visible.
+ while (mDevice.wait(Until.findObject(By.res(MAIN_VIEW)),
+ DEFAULT_TIMEOUT) == null) {};
+
+ // Scroll up to the top of the articles list.
+ scrollTo(MAIN_VIEW, TOP_ARTICLE, false, TOP_ARTICLE,
+ BOTTOM_ARTICLE, false, true, fling_speed);
+ }
+
+ private void runPortraitVerticalTests() throws Exception {
+ mDevice.setOrientationPortrait();
+
+ assert(findMainView());
+ scrollList(MAIN_VIEW, TOP_ARTICLE, BOTTOM_ARTICLE, false);
+ assert(scrollToArticle(MAIN_VIEW, FIRST_POST));
+ interactWithArticle(FIRST_POST);
+ }
+
+ private void runPortraitHorizontalTests() throws Exception {
+ mDevice.setOrientationPortrait();
+
+ assert(findMainView());
+ scrollList(MAIN_VIEW, TOP_ARTICLE, BOTTOM_ARTICLE, false);
+ assert(scrollToArticle(MAIN_VIEW, "PostCardHistory0"));
+ assert(scrollToArticle(MAIN_VIEW, POPULAR_LIST));
+
+ // Scroll the horizontal list to the end and back.
+ scrollList(POPULAR_LIST, FIRST_POPULAR_CARD, LAST_POPULAR_CARD, true);
+ // Fetch the first article on the horizontal scroll list.
+ interactWithArticle(FIRST_POPULAR_CARD);
+ }
+
+ private void runLandscapeVerticalTests() throws Exception {
+ // Flip the screen sideways to exercise the other layout
+ // of the Jetnews app.
+ mDevice.setOrientationLandscape();
+
+ assert(findMainView());
+ scrollList(MAIN_VIEW, TOP_ARTICLE, BOTTOM_ARTICLE, false);
+ assert(scrollToArticle(MAIN_VIEW, FIRST_POST));
+ interactWithArticle(FIRST_POST);
+ }
+
+ private boolean scrollTo(String element_id,
+ String resourceId, boolean downFirst,
+ String beginningId, String endId, boolean sideways,
+ boolean fling, int swipeSpeed) {
+ // First check if the resource is in view. If it is, then just return.
+ if (mDevice.wait(Until.findObject(By.res(resourceId)),
+ DEFAULT_TIMEOUT) != null) {
+ Log.d(LOG_TAG, "Object " + resourceId + " was already in view.");
+ return true;
+ }
+
+ Direction direction;
+ String markerId = downFirst? endId:beginningId;
+
+ if (sideways) {
+ direction = downFirst? Direction.RIGHT:Direction.LEFT;
+ }
+ else {
+ direction = downFirst? Direction.DOWN:Direction.UP;
+ }
+
+ for (int i = 0; i < 2; i++) {
+ // Scroll to find the object.
+ Log.d(LOG_TAG,
+ "Object " + resourceId + " is not in view. Scrolling.");
+ do {
+ UiObject2 element = mDevice.wait(Until.findObject(By.res(element_id)),
+ DEFAULT_TIMEOUT);
+ element.setGestureMarginPercentage(0.2f);
+
+ if (fling)
+ element.fling(direction, swipeSpeed);
+ else
+ element.scroll(direction, 0.3f);
+
+ UiObject2 refresh_button
+ = mDevice.wait(Until.findObject(By.text("Retry")),
+ DEFAULT_TIMEOUT);
+
+ if (refresh_button != null) {
+ refresh_button.click();
+ }
+
+ // If we found it, just return. Otherwise keep going.
+ if (mDevice.wait(Until.findObject(By.res(resourceId)),
+ DEFAULT_TIMEOUT) != null) {
+ Log.d(LOG_TAG,
+ "Object " + resourceId + " found while scrolling.");
+ return true;
+ }
+
+ } while (mDevice.wait(Until.findObject(By.res(markerId)),
+ DEFAULT_TIMEOUT) == null);
+
+ if (direction == Direction.DOWN)
+ direction = Direction.UP;
+ else if (direction == Direction.UP)
+ direction = Direction.DOWN;
+ else if (direction == Direction.RIGHT)
+ direction = Direction.LEFT;
+ else
+ direction = Direction.RIGHT;
+
+ Log.d(LOG_TAG, "Reached the limit at " + markerId + ".");
+
+ if (markerId == beginningId)
+ markerId = endId;
+ else
+ markerId = beginningId;
+ }
+ // We should've found it. If it is not here, it doesn't exist.
+ return false;
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ Log.d(LOG_TAG, "Initializing UiAutomation object.");
+ mUiAutomation = new UiAutomation ();
+ mUiAutomation.initialize_instrumentation();
+ mUiAutomation.initialize();
+ mUiAutomation.setup();
+
+ // Check the parameters and set sane defaults.
+ mDevice = mUiAutomation.mDevice;
+ Bundle parameters = mUiAutomation.getParams();
+
+ repeat = parameters.getInt("repeat");
+ Log.d(LOG_TAG,"Argument \"repeat\": " + String.valueOf (repeat));
+ if (repeat <= 0) {
+ repeat = DEFAULT_BENCHMARK_REPEAT_COUNT;
+ Log.d(LOG_TAG, "Argument \"repeat\" initialized to default: " +
+ String.valueOf (DEFAULT_BENCHMARK_REPEAT_COUNT));
+ }
+
+ fling_speed = parameters.getInt("flingspeed");
+ Log.d(LOG_TAG,
+ "Argument \"flingspeed\": " + String.valueOf (fling_speed));
+ if (fling_speed <= 1000 || fling_speed >= 30000) {
+ fling_speed = DEFAULT_BENCHMARK_FLING_SPEED;
+ Log.d(LOG_TAG, "Argument \"flingspeed\" initialized to default: " +
+ String.valueOf (DEFAULT_BENCHMARK_FLING_SPEED));
+ }
+
+ String[] tests = parameters.getStringArray("tests");
+ if (tests == null) {
+ tests = DEFAULT_BENCHMARK_TESTS;
+ Log.d(LOG_TAG, "Argument \"tests\" initialized to default: " +
+ String.valueOf (DEFAULT_BENCHMARK_TESTS));
+ }
+
+ Arrays.sort (tests);
+ testPortraitVertical
+ = Arrays.binarySearch(tests,
+ "PortraitVerticalTest") >= 0? true : false;
+ testPortraitHorizontal
+ = Arrays.binarySearch(tests,
+ "PortraitHorizontalTest") >= 0? true : false;
+ testLandscapeVertical
+ = Arrays.binarySearch(tests,
+ "LandscapeVerticalTest") >= 0? true : false;
+
+ if (testPortraitVertical)
+ Log.d(LOG_TAG, "Found PortraitVerticalTest");
+ if (testPortraitHorizontal)
+ Log.d(LOG_TAG, "Found PortraitHorizontalTest");
+ if (testLandscapeVertical)
+ Log.d(LOG_TAG, "Found LandscapeVerticalTest");
+ }
+}
diff --git a/wa/workloads/jetnews/uiauto/build.gradle b/wa/workloads/jetnews/uiauto/build.gradle
new file mode 100644
index 000000000..204815382
--- /dev/null
+++ b/wa/workloads/jetnews/uiauto/build.gradle
@@ -0,0 +1,26 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:8.5.0'
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+tasks.register('clean', Delete) {
+ delete rootProject.buildDir
+}
diff --git a/wa/workloads/jetnews/uiauto/build.sh b/wa/workloads/jetnews/uiauto/build.sh
new file mode 100755
index 000000000..2dc3ac8fc
--- /dev/null
+++ b/wa/workloads/jetnews/uiauto/build.sh
@@ -0,0 +1,55 @@
+#!/bin/bash
+# Copyright 2013-2024 ARM Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+set -e
+
+# CD into build dir if possible - allows building from any directory
+script_path='.'
+if `readlink -f $0 &>/dev/null`; then
+ script_path=`readlink -f $0 2>/dev/null`
+fi
+script_dir=`dirname $script_path`
+cd $script_dir
+
+# Ensure gradelw exists before starting
+if [[ ! -f gradlew ]]; then
+ echo 'gradlew file not found! Check that you are in the right directory.'
+ exit 9
+fi
+
+# Copy base class library from wa dist
+libs_dir=app/libs
+base_class=`python3 -c "import os, wa; print(os.path.join(os.path.dirname(wa.__file__), 'framework', 'uiauto-androidx', 'uiauto.aar'))"`
+mkdir -p $libs_dir
+cp $base_class $libs_dir
+
+# Build and return appropriate exit code if failed
+# gradle build
+./gradlew clean :app:assembleDebug
+exit_code=$?
+if [[ $exit_code -ne 0 ]]; then
+ echo "ERROR: 'gradle build' exited with code $exit_code"
+ exit $exit_code
+fi
+
+# If successful move APK file to workload folder (overwrite previous)
+package=com.arm.wa.uiauto.jetnews
+rm -f ../$package
+if [[ -f app/build/outputs/apk/debug/$package.apk ]]; then
+ cp app/build/outputs/apk/debug/$package.apk ../$package.apk
+else
+ echo 'ERROR: UiAutomator apk could not be found!'
+ exit 9
+fi
diff --git a/wa/workloads/jetnews/uiauto/gradle.properties b/wa/workloads/jetnews/uiauto/gradle.properties
new file mode 100644
index 000000000..495f5d377
--- /dev/null
+++ b/wa/workloads/jetnews/uiauto/gradle.properties
@@ -0,0 +1,42 @@
+#
+# Copyright 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m
+
+# Turn on parallel compilation, caching and on-demand configuration
+org.gradle.configureondemand=true
+org.gradle.caching=true
+org.gradle.parallel=true
+
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+
+# Enable R8 full mode.
+android.enableR8.fullMode=true
diff --git a/wa/workloads/jetnews/uiauto/gradle/wrapper/gradle-wrapper.jar b/wa/workloads/jetnews/uiauto/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..13372aef5
Binary files /dev/null and b/wa/workloads/jetnews/uiauto/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/wa/workloads/jetnews/uiauto/gradle/wrapper/gradle-wrapper.properties b/wa/workloads/jetnews/uiauto/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..ec6e4d93c
--- /dev/null
+++ b/wa/workloads/jetnews/uiauto/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Tue 27 Aug 2024 12:24:19 PM BST
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-all.zip
diff --git a/wa/workloads/jetnews/uiauto/gradlew b/wa/workloads/jetnews/uiauto/gradlew
new file mode 100755
index 000000000..9d82f7891
--- /dev/null
+++ b/wa/workloads/jetnews/uiauto/gradlew
@@ -0,0 +1,160 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/wa/workloads/jetnews/uiauto/gradlew.bat b/wa/workloads/jetnews/uiauto/gradlew.bat
new file mode 100644
index 000000000..8a0b282aa
--- /dev/null
+++ b/wa/workloads/jetnews/uiauto/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/wa/workloads/jetnews/uiauto/settings.gradle b/wa/workloads/jetnews/uiauto/settings.gradle
new file mode 100644
index 000000000..63275f219
--- /dev/null
+++ b/wa/workloads/jetnews/uiauto/settings.gradle
@@ -0,0 +1,5 @@
+plugins {
+ id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0'
+}
+
+include ':app'