/* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.platform.helpers; import static android.platform.helpers.ui.UiAutomatorUtils.getInstrumentation; import static android.platform.helpers.ui.UiAutomatorUtils.getUiDevice; import static android.platform.helpers.ui.UiSearch.search; import static android.platform.uiautomator_helpers.DeviceHelpers.getContext; import static com.google.common.truth.Truth.assertWithMessage; import static java.lang.String.format; import android.app.Instrumentation; import android.content.ComponentName; import android.content.Intent; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Point; import android.graphics.Rect; import android.os.Bundle; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.platform.helpers.features.common.HomeLockscreenPage; import android.platform.test.util.HealthTestingUtils; import android.util.Log; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.uiautomator.BySelector; import androidx.test.uiautomator.UiObject; import androidx.test.uiautomator.UiObjectNotFoundException; import java.io.FileInputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Helper class for writing System UI ui tests. It consists of common utils required while writing * UI tests. */ public class CommonUtils { private static final int LARGE_SCREEN_DP_THRESHOLD = 600; private static final String TAG = "CommonUtils"; private static final int SWIPE_STEPS = 100; private static final int DEFAULT_MARGIN = 5; private static final String LIST_ALL_USERS_COMMAND = "cmd user list -v --all"; private static final String GET_MAIN_USER_COMMAND = "cmd user get-main-user"; private static final String SYSTEM_UI_PACKAGE = "com.android.systemui"; private static final String SPLIT_SHADE_RES_NAME = "config_use_split_notification_shade"; private CommonUtils() { } /** * Prints a message to standard output during an instrumentation test. * * Message will be printed to terminal if test is run using {@code am instrument}. This is * useful for debugging. */ public static void println(String msg) { final Bundle streamResult = new Bundle(); streamResult.putString(Instrumentation.REPORT_KEY_STREAMRESULT, msg + "\n"); InstrumentationRegistry.getInstrumentation().sendStatus(0, streamResult); } /** * This method help you execute you shell command. * Example: adb shell pm list packages -f * Here you just need to provide executeShellCommand("pm list packages -f") * * @param command command need to executed. * @return output in String format. */ public static String executeShellCommand(String command) { Log.d(TAG, format("Executing Shell Command: %s", command)); try { String out = getUiDevice().executeShellCommand(command); return out; } catch (IOException e) { Log.d(TAG, format("IOException Occurred: %s", e)); throw new RuntimeException(e); } } /** Returns PIDs of all System UI processes */ private static String[] getSystemUiPids() { String output = executeShellCommand("pidof com.android.systemui"); if (output.isEmpty()) { // explicit check empty string, and return 0-length array. // "".split("\\s") returns 1-length array [""], which invalidates // allSysUiProcessesRestarted check. return new String[0]; } return output.split("\\s"); } private static boolean allSysUiProcessesRestarted(List initialPidsList) { final String[] currentPids = getSystemUiPids(); Log.d(TAG, "restartSystemUI: Current PIDs=" + Arrays.toString(currentPids)); if (currentPids.length < initialPidsList.size()) { return false; // No all processes restarted. } for (String pid : currentPids) { if (initialPidsList.contains(pid)) { return false; // Old process still running. } } return true; } /** * Restart System UI by running {@code am crash com.android.systemui}. * *

This is sometimes necessary after changing flags, configs, or settings ensure that * systemui is properly initialized with the new changes. This method will wait until the home * screen is visible, then it will optionally dismiss the home screen via swipe. * * @param swipeUp whether to call {@link HomeLockscreenPage#swipeUp()} after restarting System * UI * @deprecated Use {@link SysuiRestarter} instead. It has been moved out from here to use * androidx uiautomator version (this class depends on the old version, and there are many * deps that don't allow to easily switch to the new androidx one) */ @Deprecated public static void restartSystemUI(boolean swipeUp) { SysuiRestarter.restartSystemUI(swipeUp); } /** Asserts that the screen is on. */ public static void assertScreenOn(String errorMessage) { try { assertWithMessage(errorMessage) .that(getUiDevice().isScreenOn()) .isTrue(); } catch (RemoteException e) { throw new RuntimeException(e); } } /** * Helper method to swipe the given object. * * @param gestureType direction which to swipe. * @param obj object which needs to be swiped. */ public static void swipe(GestureType gestureType, UiObject obj) { Log.d(TAG, format("Swiping Object[%s] %s", obj.getSelector(), gestureType)); try { Rect boundary = obj.getBounds(); final int displayHeight = getUiDevice().getDisplayHeight() - DEFAULT_MARGIN; final int displayWidth = getUiDevice().getDisplayWidth() - DEFAULT_MARGIN; final int objHeight = boundary.height(); final int objWidth = boundary.width(); final int marginHeight = (Math.abs(displayHeight - objHeight)) / 2; final int marginWidth = (Math.abs(displayWidth - objWidth)) / 2; switch (gestureType) { case DOWN: getUiDevice().swipe( marginWidth + (objWidth / 2), marginHeight, marginWidth + (objWidth / 2), displayHeight, SWIPE_STEPS ); break; case UP: getUiDevice().swipe( marginWidth + (objWidth / 2), displayHeight, marginWidth + (objWidth / 2), marginHeight, SWIPE_STEPS ); break; case RIGHT: getUiDevice().swipe( marginWidth, marginHeight + (objHeight / 2), displayWidth, marginHeight + (objHeight / 2), SWIPE_STEPS ); break; case LEFT: getUiDevice().swipe( displayWidth, marginHeight + (objHeight / 2), marginWidth, marginHeight + (objHeight / 2), SWIPE_STEPS ); break; } } catch (UiObjectNotFoundException e) { Log.e(TAG, format("Given object was not found. Hence failed to swipe. Exception %s", e)); throw new RuntimeException(e); } } /** * Launching an app with different ways. * * @param launchAppWith options used to launching an app. * @param packageActivityOrComponentName required package or activity or component name to * launch the given app * @param appName name of the app */ public static void launchApp(LaunchAppWith launchAppWith, String packageActivityOrComponentName, String appName) { Log.d(TAG, String.format("Opening app %s using their %s [%s]", appName, launchAppWith, packageActivityOrComponentName)); Intent appIntent = null; switch (launchAppWith) { case PACKAGE_NAME: PackageManager packageManager = getContext().getPackageManager(); appIntent = packageManager.getLaunchIntentForPackage( packageActivityOrComponentName); appIntent.addCategory(Intent.CATEGORY_LAUNCHER); break; case ACTIVITY: appIntent = new Intent(packageActivityOrComponentName); break; case COMPONENT_NAME: ComponentName componentName = ComponentName.unflattenFromString( packageActivityOrComponentName); appIntent = new Intent(); appIntent.setComponent(componentName); break; default: throw new AssertionError("Non-supported Launch App with: " + launchAppWith); } // Ensure the app is completely restarted so that none of the test app's state // leaks between tests. appIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); getContext().startActivity(appIntent); } /** * Asserts that a given page is visible. * * @param pageSelector selector helped to verify the page * @param pageName name of the page to be verified * @param maxTimeoutSeconds max time in seconds to verify the page */ public static void assertPageVisible(BySelector pageSelector, String pageName, int maxTimeoutSeconds) { assertWithMessage(format("Page[%s] not visible; selector: %s", pageName, pageSelector)) .that(search(null, pageSelector, format("Page[%s]", pageName), maxTimeoutSeconds)) .isTrue(); } /** * Asserts that a given page is not visible. * * @param pageSelector selector helped to verify the page * @param pageName name of the page to be verified */ public static void assertPageNotVisible(BySelector pageSelector, String pageName) { HealthTestingUtils.waitForCondition( () -> "Page is still visible", () -> !search(null, pageSelector, format("Page[%s]", pageName), 0)); } /** * Execute the given shell command and get the detailed output * * @param shellCommand shell command to be executed * @return the detailed output as an arraylist. */ public static ArrayList executeShellCommandWithDetailedOutput(String shellCommand) { try { ParcelFileDescriptor fileDescriptor = getInstrumentation().getUiAutomation().executeShellCommand(shellCommand); byte[] buf = new byte[512]; int bytesRead; FileInputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream( fileDescriptor); ArrayList output = new ArrayList<>(); while ((bytesRead = inputStream.read(buf)) != -1) { output.add(new String(buf, 0, bytesRead)); } inputStream.close(); return output; } catch (IOException e) { throw new RuntimeException(e); } } /** * Returns the current user user ID. NOTE: UserID = 0 is for System * * @return a current user ID */ public static int getCurrentUserId() { Log.d(TAG, "Getting the Current User ID"); // Example terminal output of the list all users command: // // $ adb shell cmd user list -v --all // 2 users: // // 0: id=0, name=Owner, type=full.SYSTEM, flags=FULL|INITIALIZED|PRIMARY|SYSTEM (running) // 1: id=10, name=Guest, type=full.GUEST, flags=FULL|GUEST|INITIALIZED (running) (current) ArrayList output = executeShellCommandWithDetailedOutput(LIST_ALL_USERS_COMMAND); String getCurrentUser = null; for (String line : output) { if (line.contains("(current)")) { getCurrentUser = line; break; } } Pattern userRegex = Pattern.compile("[\\d]+:.*id=([\\d]+).*\\(current\\)"); Matcher matcher = userRegex.matcher(getCurrentUser); while (matcher.find()) { return Integer.parseInt(matcher.group(1)); } Log.d(TAG, "Failed to find current user ID. dumpsys activity follows:"); for (String line : output) { Log.d(TAG, line); } throw new RuntimeException("Failed to find current user ID."); } /** * Returns the main user ID. Main user is the main human user on the device. * Returns 0 by default, if there is no main user. Android Auto is example of HSUM without * main user. * * NOTE: For headless system main user it is NOT 0. Therefore Main user should be used in * test cases rather than owner or deprecated primary user. */ public static int getMainUserId() { ArrayList output = executeShellCommandWithDetailedOutput(GET_MAIN_USER_COMMAND); try { return Integer.parseInt(output.get(0).trim()); } catch (NumberFormatException e) { return 0; } } public static boolean isSplitShade() { try { Resources sysUiResources = getContext().getPackageManager().getResourcesForApplication(SYSTEM_UI_PACKAGE); int resourceId = sysUiResources.getIdentifier(SPLIT_SHADE_RES_NAME, "bool", SYSTEM_UI_PACKAGE); return sysUiResources.getBoolean(resourceId); } catch (PackageManager.NameNotFoundException e) { Log.e(TAG, "Couldn't get SysUI resources"); } // Fallback check, accurate for most but not necessarily all devices int orientation = getContext().getResources().getConfiguration().orientation; return isLargeScreen() && (orientation == Configuration.ORIENTATION_LANDSCAPE); } public static boolean isLargeScreen() { Point sizeDp = getUiDevice().getDisplaySizeDp(); return sizeDp.x >= LARGE_SCREEN_DP_THRESHOLD && sizeDp.y >= LARGE_SCREEN_DP_THRESHOLD; } /** * Gesture for swipe */ public enum GestureType { RIGHT, LEFT, UP, DOWN } /** * Different options used for launching an app. */ public enum LaunchAppWith { PACKAGE_NAME, ACTIVITY, COMPONENT_NAME } }