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