1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.platform.helpers; 18 19 import static android.platform.helpers.ui.UiAutomatorUtils.getInstrumentation; 20 import static android.platform.helpers.ui.UiAutomatorUtils.getUiDevice; 21 import static android.platform.helpers.ui.UiSearch.search; 22 import static android.platform.uiautomator_helpers.DeviceHelpers.getContext; 23 24 import static com.google.common.truth.Truth.assertWithMessage; 25 26 import static java.lang.String.format; 27 28 import android.app.Instrumentation; 29 import android.content.ComponentName; 30 import android.content.Intent; 31 import android.content.pm.PackageManager; 32 import android.content.res.Configuration; 33 import android.content.res.Resources; 34 import android.graphics.Point; 35 import android.graphics.Rect; 36 import android.os.Bundle; 37 import android.os.ParcelFileDescriptor; 38 import android.os.RemoteException; 39 import android.platform.helpers.features.common.HomeLockscreenPage; 40 import android.platform.test.util.HealthTestingUtils; 41 import android.util.Log; 42 43 import androidx.test.platform.app.InstrumentationRegistry; 44 import androidx.test.uiautomator.BySelector; 45 import androidx.test.uiautomator.UiObject; 46 import androidx.test.uiautomator.UiObjectNotFoundException; 47 48 import java.io.FileInputStream; 49 import java.io.IOException; 50 import java.util.ArrayList; 51 import java.util.Arrays; 52 import java.util.List; 53 import java.util.regex.Matcher; 54 import java.util.regex.Pattern; 55 56 /** 57 * Helper class for writing System UI ui tests. It consists of common utils required while writing 58 * UI tests. 59 */ 60 public class CommonUtils { 61 62 private static final int LARGE_SCREEN_DP_THRESHOLD = 600; 63 private static final String TAG = "CommonUtils"; 64 private static final int SWIPE_STEPS = 100; 65 private static final int DEFAULT_MARGIN = 5; 66 private static final String LIST_ALL_USERS_COMMAND = "cmd user list -v --all"; 67 private static final String GET_MAIN_USER_COMMAND = "cmd user get-main-user"; 68 private static final String SYSTEM_UI_PACKAGE = "com.android.systemui"; 69 private static final String SPLIT_SHADE_RES_NAME = "config_use_split_notification_shade"; 70 CommonUtils()71 private CommonUtils() { 72 } 73 74 /** 75 * Prints a message to standard output during an instrumentation test. 76 * 77 * Message will be printed to terminal if test is run using {@code am instrument}. This is 78 * useful for debugging. 79 */ println(String msg)80 public static void println(String msg) { 81 final Bundle streamResult = new Bundle(); 82 streamResult.putString(Instrumentation.REPORT_KEY_STREAMRESULT, msg + "\n"); 83 InstrumentationRegistry.getInstrumentation().sendStatus(0, streamResult); 84 } 85 86 /** 87 * This method help you execute you shell command. 88 * Example: adb shell pm list packages -f 89 * Here you just need to provide executeShellCommand("pm list packages -f") 90 * 91 * @param command command need to executed. 92 * @return output in String format. 93 */ executeShellCommand(String command)94 public static String executeShellCommand(String command) { 95 Log.d(TAG, format("Executing Shell Command: %s", command)); 96 try { 97 String out = getUiDevice().executeShellCommand(command); 98 return out; 99 } catch (IOException e) { 100 Log.d(TAG, format("IOException Occurred: %s", e)); 101 throw new RuntimeException(e); 102 } 103 } 104 105 /** Returns PIDs of all System UI processes */ getSystemUiPids()106 private static String[] getSystemUiPids() { 107 String output = executeShellCommand("pidof com.android.systemui"); 108 if (output.isEmpty()) { 109 // explicit check empty string, and return 0-length array. 110 // "".split("\\s") returns 1-length array [""], which invalidates 111 // allSysUiProcessesRestarted check. 112 return new String[0]; 113 } 114 return output.split("\\s"); 115 } 116 allSysUiProcessesRestarted(List<String> initialPidsList)117 private static boolean allSysUiProcessesRestarted(List<String> initialPidsList) { 118 final String[] currentPids = getSystemUiPids(); 119 Log.d(TAG, "restartSystemUI: Current PIDs=" + Arrays.toString(currentPids)); 120 if (currentPids.length < initialPidsList.size()) { 121 return false; // No all processes restarted. 122 } 123 for (String pid : currentPids) { 124 if (initialPidsList.contains(pid)) { 125 return false; // Old process still running. 126 } 127 } 128 return true; 129 } 130 131 /** 132 * Restart System UI by running {@code am crash com.android.systemui}. 133 * 134 * <p>This is sometimes necessary after changing flags, configs, or settings ensure that 135 * systemui is properly initialized with the new changes. This method will wait until the home 136 * screen is visible, then it will optionally dismiss the home screen via swipe. 137 * 138 * @param swipeUp whether to call {@link HomeLockscreenPage#swipeUp()} after restarting System 139 * UI 140 * @deprecated Use {@link SysuiRestarter} instead. It has been moved out from here to use 141 * androidx uiautomator version (this class depends on the old version, and there are many 142 * deps that don't allow to easily switch to the new androidx one) 143 */ 144 @Deprecated restartSystemUI(boolean swipeUp)145 public static void restartSystemUI(boolean swipeUp) { 146 SysuiRestarter.restartSystemUI(swipeUp); 147 } 148 149 /** Asserts that the screen is on. */ assertScreenOn(String errorMessage)150 public static void assertScreenOn(String errorMessage) { 151 try { 152 assertWithMessage(errorMessage) 153 .that(getUiDevice().isScreenOn()) 154 .isTrue(); 155 } catch (RemoteException e) { 156 throw new RuntimeException(e); 157 } 158 } 159 160 /** 161 * Helper method to swipe the given object. 162 * 163 * @param gestureType direction which to swipe. 164 * @param obj object which needs to be swiped. 165 */ swipe(GestureType gestureType, UiObject obj)166 public static void swipe(GestureType gestureType, UiObject obj) { 167 Log.d(TAG, format("Swiping Object[%s] %s", obj.getSelector(), gestureType)); 168 try { 169 Rect boundary = obj.getBounds(); 170 final int displayHeight = getUiDevice().getDisplayHeight() - DEFAULT_MARGIN; 171 final int displayWidth = getUiDevice().getDisplayWidth() - DEFAULT_MARGIN; 172 final int objHeight = boundary.height(); 173 final int objWidth = boundary.width(); 174 final int marginHeight = (Math.abs(displayHeight - objHeight)) / 2; 175 final int marginWidth = (Math.abs(displayWidth - objWidth)) / 2; 176 switch (gestureType) { 177 case DOWN: 178 getUiDevice().swipe( 179 marginWidth + (objWidth / 2), 180 marginHeight, 181 marginWidth + (objWidth / 2), 182 displayHeight, 183 SWIPE_STEPS 184 ); 185 break; 186 case UP: 187 getUiDevice().swipe( 188 marginWidth + (objWidth / 2), 189 displayHeight, 190 marginWidth + (objWidth / 2), 191 marginHeight, 192 SWIPE_STEPS 193 ); 194 break; 195 case RIGHT: 196 getUiDevice().swipe( 197 marginWidth, 198 marginHeight + (objHeight / 2), 199 displayWidth, 200 marginHeight + (objHeight / 2), 201 SWIPE_STEPS 202 ); 203 break; 204 case LEFT: 205 getUiDevice().swipe( 206 displayWidth, 207 marginHeight + (objHeight / 2), 208 marginWidth, 209 marginHeight + (objHeight / 2), 210 SWIPE_STEPS 211 ); 212 break; 213 } 214 } catch (UiObjectNotFoundException e) { 215 Log.e(TAG, 216 format("Given object was not found. Hence failed to swipe. Exception %s", e)); 217 throw new RuntimeException(e); 218 } 219 } 220 221 /** 222 * Launching an app with different ways. 223 * 224 * @param launchAppWith options used to launching an app. 225 * @param packageActivityOrComponentName required package or activity or component name to 226 * launch the given app 227 * @param appName name of the app 228 */ launchApp(LaunchAppWith launchAppWith, String packageActivityOrComponentName, String appName)229 public static void launchApp(LaunchAppWith launchAppWith, String packageActivityOrComponentName, 230 String appName) { 231 Log.d(TAG, String.format("Opening app %s using their %s [%s]", 232 appName, launchAppWith, packageActivityOrComponentName)); 233 Intent appIntent = null; 234 switch (launchAppWith) { 235 case PACKAGE_NAME: 236 PackageManager packageManager = getContext().getPackageManager(); 237 appIntent = packageManager.getLaunchIntentForPackage( 238 packageActivityOrComponentName); 239 appIntent.addCategory(Intent.CATEGORY_LAUNCHER); 240 break; 241 case ACTIVITY: 242 appIntent = new Intent(packageActivityOrComponentName); 243 break; 244 case COMPONENT_NAME: 245 ComponentName componentName = ComponentName.unflattenFromString( 246 packageActivityOrComponentName); 247 appIntent = new Intent(); 248 appIntent.setComponent(componentName); 249 break; 250 default: 251 throw new AssertionError("Non-supported Launch App with: " + launchAppWith); 252 } 253 // Ensure the app is completely restarted so that none of the test app's state 254 // leaks between tests. 255 appIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); 256 getContext().startActivity(appIntent); 257 } 258 259 /** 260 * Asserts that a given page is visible. 261 * 262 * @param pageSelector selector helped to verify the page 263 * @param pageName name of the page to be verified 264 * @param maxTimeoutSeconds max time in seconds to verify the page 265 */ assertPageVisible(BySelector pageSelector, String pageName, int maxTimeoutSeconds)266 public static void assertPageVisible(BySelector pageSelector, String pageName, 267 int maxTimeoutSeconds) { 268 assertWithMessage(format("Page[%s] not visible; selector: %s", pageName, pageSelector)) 269 .that(search(null, pageSelector, format("Page[%s]", pageName), maxTimeoutSeconds)) 270 .isTrue(); 271 } 272 273 /** 274 * Asserts that a given page is not visible. 275 * 276 * @param pageSelector selector helped to verify the page 277 * @param pageName name of the page to be verified 278 */ assertPageNotVisible(BySelector pageSelector, String pageName)279 public static void assertPageNotVisible(BySelector pageSelector, String pageName) { 280 HealthTestingUtils.waitForCondition( 281 () -> "Page is still visible", 282 () -> !search(null, pageSelector, format("Page[%s]", pageName), 0)); 283 } 284 285 /** 286 * Execute the given shell command and get the detailed output 287 * 288 * @param shellCommand shell command to be executed 289 * @return the detailed output as an arraylist. 290 */ executeShellCommandWithDetailedOutput(String shellCommand)291 public static ArrayList<String> executeShellCommandWithDetailedOutput(String shellCommand) { 292 try { 293 ParcelFileDescriptor fileDescriptor = 294 getInstrumentation().getUiAutomation().executeShellCommand(shellCommand); 295 byte[] buf = new byte[512]; 296 int bytesRead; 297 FileInputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream( 298 fileDescriptor); 299 ArrayList<String> output = new ArrayList<>(); 300 while ((bytesRead = inputStream.read(buf)) != -1) { 301 output.add(new String(buf, 0, bytesRead)); 302 } 303 inputStream.close(); 304 return output; 305 } catch (IOException e) { 306 throw new RuntimeException(e); 307 } 308 } 309 310 /** 311 * Returns the current user user ID. NOTE: UserID = 0 is for System 312 * 313 * @return a current user ID 314 */ getCurrentUserId()315 public static int getCurrentUserId() { 316 Log.d(TAG, "Getting the Current User ID"); 317 318 // Example terminal output of the list all users command: 319 // 320 // $ adb shell cmd user list -v --all 321 // 2 users: 322 // 323 // 0: id=0, name=Owner, type=full.SYSTEM, flags=FULL|INITIALIZED|PRIMARY|SYSTEM (running) 324 // 1: id=10, name=Guest, type=full.GUEST, flags=FULL|GUEST|INITIALIZED (running) (current) 325 ArrayList<String> output = executeShellCommandWithDetailedOutput(LIST_ALL_USERS_COMMAND); 326 String getCurrentUser = null; 327 for (String line : output) { 328 if (line.contains("(current)")) { 329 getCurrentUser = line; 330 break; 331 } 332 } 333 Pattern userRegex = Pattern.compile("[\\d]+:.*id=([\\d]+).*\\(current\\)"); 334 Matcher matcher = userRegex.matcher(getCurrentUser); 335 while (matcher.find()) { 336 return Integer.parseInt(matcher.group(1)); 337 } 338 339 Log.d(TAG, "Failed to find current user ID. dumpsys activity follows:"); 340 for (String line : output) { 341 Log.d(TAG, line); 342 } 343 throw new RuntimeException("Failed to find current user ID."); 344 } 345 346 /** 347 * Returns the main user ID. Main user is the main human user on the device. 348 * Returns 0 by default, if there is no main user. Android Auto is example of HSUM without 349 * main user. 350 * 351 * NOTE: For headless system main user it is NOT 0. Therefore Main user should be used in 352 * test cases rather than owner or deprecated primary user. 353 */ getMainUserId()354 public static int getMainUserId() { 355 ArrayList<String> output = executeShellCommandWithDetailedOutput(GET_MAIN_USER_COMMAND); 356 try { 357 return Integer.parseInt(output.get(0).trim()); 358 } catch (NumberFormatException e) { 359 return 0; 360 } 361 } 362 isSplitShade()363 public static boolean isSplitShade() { 364 try { 365 Resources sysUiResources = 366 getContext().getPackageManager().getResourcesForApplication(SYSTEM_UI_PACKAGE); 367 int resourceId = 368 sysUiResources.getIdentifier(SPLIT_SHADE_RES_NAME, "bool", SYSTEM_UI_PACKAGE); 369 return sysUiResources.getBoolean(resourceId); 370 } catch (PackageManager.NameNotFoundException e) { 371 Log.e(TAG, "Couldn't get SysUI resources"); 372 } 373 374 // Fallback check, accurate for most but not necessarily all devices 375 int orientation = getContext().getResources().getConfiguration().orientation; 376 return isLargeScreen() && (orientation == Configuration.ORIENTATION_LANDSCAPE); 377 } 378 isLargeScreen()379 public static boolean isLargeScreen() { 380 Point sizeDp = getUiDevice().getDisplaySizeDp(); 381 return sizeDp.x >= LARGE_SCREEN_DP_THRESHOLD && sizeDp.y >= LARGE_SCREEN_DP_THRESHOLD; 382 } 383 384 /** 385 * Gesture for swipe 386 */ 387 public enum GestureType { 388 RIGHT, 389 LEFT, 390 UP, 391 DOWN 392 } 393 394 /** 395 * Different options used for launching an app. 396 */ 397 public enum LaunchAppWith { 398 PACKAGE_NAME, 399 ACTIVITY, 400 COMPONENT_NAME 401 } 402 } 403