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'