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