1 /* 2 * Copyright (C) 2010 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 package com.android.tradefed.device; 17 18 import com.android.ddmlib.AdbCommandRejectedException; 19 import com.android.ddmlib.IDevice; 20 import com.android.ddmlib.InstallException; 21 import com.android.ddmlib.InstallReceiver; 22 import com.android.ddmlib.RawImage; 23 import com.android.ddmlib.ShellCommandUnresponsiveException; 24 import com.android.ddmlib.SyncException; 25 import com.android.ddmlib.TimeoutException; 26 import com.android.tradefed.config.GlobalConfiguration; 27 import com.android.tradefed.device.IDeviceSelection.BaseDeviceType; 28 import com.android.tradefed.invoker.logger.InvocationMetricLogger; 29 import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey; 30 import com.android.tradefed.invoker.tracing.CloseableTraceScope; 31 import com.android.tradefed.log.ITestLogger; 32 import com.android.tradefed.log.LogUtil.CLog; 33 import com.android.tradefed.result.ByteArrayInputStreamSource; 34 import com.android.tradefed.result.FileInputStreamSource; 35 import com.android.tradefed.result.InputStreamSource; 36 import com.android.tradefed.result.LogDataType; 37 import com.android.tradefed.result.error.DeviceErrorIdentifier; 38 import com.android.tradefed.result.error.InfraErrorIdentifier; 39 import com.android.tradefed.targetprep.TargetSetupError; 40 import com.android.tradefed.util.AaptParser; 41 import com.android.tradefed.util.Bugreport; 42 import com.android.tradefed.util.CommandResult; 43 import com.android.tradefed.util.CommandStatus; 44 import com.android.tradefed.util.FileUtil; 45 import com.android.tradefed.util.KeyguardControllerState; 46 import com.android.tradefed.util.RunUtil; 47 import com.android.tradefed.util.StreamUtil; 48 import com.android.tradefed.util.TimeUtil; 49 import com.android.tradefed.util.ZipUtil2; 50 51 import com.google.common.annotations.VisibleForTesting; 52 import com.google.common.base.Strings; 53 54 import org.apache.commons.compress.archivers.zip.ZipFile; 55 56 import java.awt.Image; 57 import java.awt.image.BufferedImage; 58 import java.io.BufferedReader; 59 import java.io.ByteArrayOutputStream; 60 import java.io.File; 61 import java.io.IOException; 62 import java.io.InputStreamReader; 63 import java.io.PipedInputStream; 64 import java.io.PipedOutputStream; 65 import java.io.Reader; 66 import java.net.ServerSocket; 67 import java.util.ArrayList; 68 import java.util.Arrays; 69 import java.util.Collections; 70 import java.util.HashMap; 71 import java.util.HashSet; 72 import java.util.LinkedHashMap; 73 import java.util.LinkedHashSet; 74 import java.util.List; 75 import java.util.Map; 76 import java.util.Set; 77 import java.util.concurrent.ExecutorService; 78 import java.util.concurrent.Executors; 79 import java.util.concurrent.TimeUnit; 80 import java.util.regex.Matcher; 81 import java.util.regex.Pattern; 82 import java.util.stream.Collectors; 83 84 import javax.annotation.Nonnull; 85 import javax.annotation.Nullable; 86 import javax.imageio.ImageIO; 87 88 /** 89 * Implementation of a {@link ITestDevice} for a full stack android device 90 */ 91 public class TestDevice extends NativeDevice { 92 93 /** number of attempts made to clear dialogs */ 94 private static final int NUM_CLEAR_ATTEMPTS = 5; 95 /** the command used to dismiss a error dialog. Currently sends a DPAD_CENTER key event */ 96 static final String DISMISS_DIALOG_CMD = "input keyevent 23"; 97 98 static final String DISMISS_DIALOG_BROADCAST = 99 "am broadcast -a android.intent.action.CLOSE_SYSTEM_DIALOGS"; 100 // Collapse notifications 101 private static final String COLLAPSE_STATUS_BAR = "cmd statusbar collapse"; 102 103 /** Commands that can be used to dismiss the keyguard. */ 104 public static final String DISMISS_KEYGUARD_CMD = "input keyevent 82"; 105 106 /** 107 * Alternative command to dismiss the keyguard by requesting the Window Manager service to do 108 * it. Api 23 and after. 109 */ 110 static final String DISMISS_KEYGUARD_WM_CMD = "wm dismiss-keyguard"; 111 112 /** Maximum time to wait for keyguard to be dismissed. */ 113 private static final long DISMISS_KEYGUARD_TIMEOUT = 3 * 1000; 114 115 /** Command to construct KeyguardControllerState. */ 116 static final String KEYGUARD_CONTROLLER_CMD = 117 "dumpsys activity activities | grep -A3 KeyguardController:"; 118 119 /** Timeout to wait for input dispatch to become ready **/ 120 private static final long INPUT_DISPATCH_READY_TIMEOUT = 5 * 1000; 121 /** command to test input dispatch readiness **/ 122 private static final String TEST_INPUT_CMD = "dumpsys input"; 123 124 private static final long AM_COMMAND_TIMEOUT = 10 * 1000; 125 private static final long CHECK_NEW_USER = 1000; 126 127 static final String LIST_PACKAGES_CMD = "pm list packages -f"; 128 private static final Pattern PACKAGE_REGEX = Pattern.compile("package:(.*)=(.*)"); 129 130 static final String LIST_APEXES_CMD = "pm list packages --apex-only --show-versioncode -f"; 131 private static final Pattern APEXES_WITH_PATH_REGEX = 132 Pattern.compile("package:(.*)=(.*) versionCode:(.*)"); 133 134 static final String GET_MODULEINFOS_CMD = "pm get-moduleinfo --all"; 135 private static final Pattern MODULEINFO_REGEX = 136 Pattern.compile("ModuleInfo\\{(.*)\\} packageName: (.*)"); 137 138 /** 139 * Regexp to match on old versions of platform (before R), where {@code -f} flag for the {@code 140 * pm list packages apex-only} command wasn't supported. 141 */ 142 private static final Pattern APEXES_WITHOUT_PATH_REGEX = 143 Pattern.compile("package:(.*) versionCode:(.*)"); 144 145 private static final int FLAG_PRIMARY = 1; // From the UserInfo class 146 147 private static final int FLAG_MAIN = 0x00004000; // From the UserInfo class 148 149 private static final String[] SETTINGS_NAMESPACE = {"system", "secure", "global"}; 150 151 /** user pattern in the output of "pm list users" = TEXT{<id>:<name>:<flags>} TEXT * */ 152 private static final String USER_PATTERN = "(.*?\\{)(\\d+)(:)(.*)(:)(\\w+)(\\}.*)"; 153 /** Pattern to find the display ids of "dumpsys SurfaceFlinger" */ 154 private static final String DISPLAY_ID_PATTERN = "(Display )(?<id>\\d+)( color modes:)"; 155 156 private static final int API_LEVEL_GET_CURRENT_USER = 24; 157 /** Timeout to wait for a screenshot before giving up to avoid hanging forever */ 158 private static final long MAX_SCREENSHOT_TIMEOUT = 5 * 60 * 1000; // 5 min 159 160 /** adb shell am dumpheap <service pid> <dump file path> */ 161 private static final String DUMPHEAP_CMD = "am dumpheap %s %s"; 162 /** Time given to a file to be dumped on device side */ 163 private static final long DUMPHEAP_TIME = 5000L; 164 165 /** Timeout in minutes for the package installation */ 166 static final long INSTALL_TIMEOUT_MINUTES = 4; 167 /** Max timeout to output for package installation */ 168 static final long INSTALL_TIMEOUT_TO_OUTPUT_MINUTES = 3; 169 170 private boolean mWasWifiHelperInstalled = false; 171 172 private static final String APEX_SUFFIX = ".apex"; 173 private static final String APEX_ARG = "--apex"; 174 175 /** Contains a set of Microdroid instances running in this TestDevice, and their resources. */ 176 private Map<Process, MicrodroidTracker> mStartedMicrodroids = new HashMap<>(); 177 178 private static final String TEST_ROOT = "/data/local/tmp/virt/tradefed/"; 179 private static final String VIRT_APEX = "/apex/com.android.virt/"; 180 private static final String INSTANCE_ID_FILE = "instance_id"; 181 private static final String INSTANCE_IMG = "instance.img"; 182 183 // This is really slow on GCE (2m 40s) but fast on localhost or actual Android phones (< 10s). 184 // Then there is time to run the actual task. Set the maximum timeout value big enough. 185 private static final long MICRODROID_MAX_LIFETIME_MINUTES = 20; 186 187 private static final long MICRODROID_DEFAULT_ADB_CONNECT_TIMEOUT_MINUTES = 5; 188 189 private static final String EARLY_REBOOT = "Too early to call shutdown() or reboot()"; 190 191 /** 192 * Allow pauses of up to 2 minutes while receiving bugreport. 193 * 194 * <p>Note that dumpsys may pause up to a minute while waiting for unresponsive components. It 195 * still should bail after that minute, if it will ever terminate on its own. 196 */ 197 private static final int BUGREPORT_TIMEOUT = 2 * 60 * 1000; 198 199 private static final String BUGREPORT_CMD = "bugreport"; 200 private static final String BUGREPORTZ_CMD = "bugreportz"; 201 private static final Pattern BUGREPORTZ_RESPONSE_PATTERN = Pattern.compile("(OK:)(.*)"); 202 203 /** Number of attempts made to get user info. */ 204 private static final int NUM_USER_INFO_ATTEMPTS = 3; 205 206 /** Track microdroid and its resources */ 207 private class MicrodroidTracker { 208 ExecutorService executor; 209 String cid; 210 } 211 212 private boolean mWaitForSnapuserd = false; 213 private SnapuserdWaitPhase mWaitPhase = null; 214 private long mSnapuserNotificationTimestamp = 0L; 215 216 /** 217 * @param device 218 * @param stateMonitor 219 * @param allocationMonitor 220 */ TestDevice(IDevice device, IDeviceStateMonitor stateMonitor, IDeviceMonitor allocationMonitor)221 public TestDevice(IDevice device, IDeviceStateMonitor stateMonitor, 222 IDeviceMonitor allocationMonitor) { 223 super(device, stateMonitor, allocationMonitor); 224 } 225 226 @Override isAppEnumerationSupported()227 public boolean isAppEnumerationSupported() throws DeviceNotAvailableException { 228 if (!checkApiLevelAgainstNextRelease(30)) { 229 return false; 230 } 231 return hasFeature("android.software.app_enumeration"); 232 } 233 234 /** 235 * Core implementation of package installation, with retries around 236 * {@link IDevice#installPackage(String, boolean, String...)} 237 * @param packageFile 238 * @param reinstall 239 * @param extraArgs 240 * @return the response from the installation 241 * @throws DeviceNotAvailableException 242 */ internalInstallPackage( final File packageFile, final boolean reinstall, final List<String> extraArgs)243 private String internalInstallPackage( 244 final File packageFile, final boolean reinstall, final List<String> extraArgs) 245 throws DeviceNotAvailableException { 246 long startTime = System.currentTimeMillis(); 247 try { 248 List<String> args = new ArrayList<>(extraArgs); 249 if (packageFile.getName().endsWith(APEX_SUFFIX)) { 250 args.add(APEX_ARG); 251 } 252 // use array to store response, so it can be returned to caller 253 final String[] response = new String[1]; 254 DeviceAction installAction = 255 new DeviceAction() { 256 @Override 257 public boolean run() throws InstallException { 258 try { 259 InstallReceiver receiver = createInstallReceiver(); 260 getIDevice() 261 .installPackage( 262 packageFile.getAbsolutePath(), 263 reinstall, 264 receiver, 265 INSTALL_TIMEOUT_MINUTES, 266 INSTALL_TIMEOUT_TO_OUTPUT_MINUTES, 267 TimeUnit.MINUTES, 268 args.toArray(new String[] {})); 269 response[0] = handleInstallReceiver(receiver, packageFile); 270 } catch (InstallException e) { 271 response[0] = handleInstallationError(e); 272 } 273 return response[0] == null; 274 } 275 }; 276 CLog.v( 277 "Installing package file %s with args %s on %s", 278 packageFile.getAbsolutePath(), extraArgs.toString(), getSerialNumber()); 279 performDeviceAction( 280 String.format("install %s", packageFile.getAbsolutePath()), 281 installAction, 282 MAX_RETRY_ATTEMPTS); 283 List<File> packageFiles = new ArrayList<>(); 284 packageFiles.add(packageFile); 285 allowLegacyStorageForApps(packageFiles); 286 return response[0]; 287 } finally { 288 InvocationMetricLogger.addInvocationMetrics( 289 InvocationMetricKey.PACKAGE_INSTALL_COUNT, 1); 290 InvocationMetricLogger.addInvocationMetrics( 291 InvocationMetricKey.PACKAGE_INSTALL_TIME, 292 System.currentTimeMillis() - startTime); 293 } 294 } 295 296 /** 297 * Creates and return an {@link InstallReceiver} for {@link #internalInstallPackage(File, 298 * boolean, List)} and {@link #installPackage(File, File, boolean, String...)} testing. 299 */ 300 @VisibleForTesting createInstallReceiver()301 InstallReceiver createInstallReceiver() { 302 return new InstallReceiver(); 303 } 304 305 /** {@inheritDoc} */ 306 @Override getBugreport()307 public InputStreamSource getBugreport() { 308 if (getApiLevelSafe() < 24) { 309 InputStreamSource bugreport = getBugreportInternal(); 310 if (bugreport == null) { 311 // Safe call so we don't return null but an empty resource. 312 return new ByteArrayInputStreamSource("".getBytes()); 313 } 314 return bugreport; 315 } 316 CLog.d("Api level above 24, using bugreportz instead."); 317 File mainEntry = null; 318 File bugreportzFile = null; 319 long startTime = System.currentTimeMillis(); 320 try { 321 bugreportzFile = getBugreportzInternal(); 322 if (bugreportzFile == null) { 323 // return empty buffer 324 return new ByteArrayInputStreamSource("".getBytes()); 325 } 326 try (ZipFile zip = new ZipFile(bugreportzFile)) { 327 // We get the main_entry.txt that contains the bugreport name. 328 mainEntry = ZipUtil2.extractFileFromZip(zip, "main_entry.txt"); 329 String bugreportName = FileUtil.readStringFromFile(mainEntry).trim(); 330 CLog.d("bugreport name: '%s'", bugreportName); 331 File bugreport = ZipUtil2.extractFileFromZip(zip, bugreportName); 332 return new FileInputStreamSource(bugreport, true); 333 } 334 } catch (IOException e) { 335 CLog.e("Error while unzipping bugreportz"); 336 CLog.e(e); 337 return new ByteArrayInputStreamSource("corrupted bugreport.".getBytes()); 338 } finally { 339 InvocationMetricLogger.addInvocationMetrics( 340 InvocationMetricKey.BUGREPORT_TIME, System.currentTimeMillis() - startTime); 341 InvocationMetricLogger.addInvocationMetrics(InvocationMetricKey.BUGREPORT_COUNT, 1); 342 FileUtil.deleteFile(bugreportzFile); 343 FileUtil.deleteFile(mainEntry); 344 } 345 } 346 347 /** {@inheritDoc} */ 348 @Override logBugreport(String dataName, ITestLogger listener)349 public boolean logBugreport(String dataName, ITestLogger listener) { 350 InputStreamSource bugreport = null; 351 LogDataType type = null; 352 try { 353 bugreport = getBugreportz(); 354 type = LogDataType.BUGREPORTZ; 355 // log what we managed to capture. 356 if (bugreport != null && bugreport.size() > 0L) { 357 listener.testLog(dataName, type, bugreport); 358 return true; 359 } 360 } finally { 361 StreamUtil.cancel(bugreport); 362 } 363 CLog.d( 364 "logBugreport() was not successful in collecting and logging the bugreport " 365 + "for device %s", 366 getSerialNumber()); 367 return false; 368 } 369 370 /** {@inheritDoc} */ 371 @Override takeBugreport()372 public Bugreport takeBugreport() { 373 File bugreportFile = null; 374 int apiLevel = getApiLevelSafe(); 375 if (apiLevel == UNKNOWN_API_LEVEL) { 376 return null; 377 } 378 long startTime = System.currentTimeMillis(); 379 try { 380 if (apiLevel >= 24) { 381 CLog.d("Api level above 24, using bugreportz."); 382 bugreportFile = getBugreportzInternal(); 383 if (bugreportFile != null) { 384 return new Bugreport(bugreportFile, true); 385 } 386 return null; 387 } 388 // fall back to regular bugreport 389 InputStreamSource bugreport = getBugreportInternal(); 390 if (bugreport == null) { 391 CLog.e("Error when collecting the bugreport."); 392 return null; 393 } 394 try { 395 bugreportFile = FileUtil.createTempFile("bugreport", ".txt"); 396 FileUtil.writeToFile(bugreport.createInputStream(), bugreportFile); 397 return new Bugreport(bugreportFile, false); 398 } catch (IOException e) { 399 CLog.e("Error when writing the bugreport file"); 400 CLog.e(e); 401 } 402 return null; 403 } finally { 404 InvocationMetricLogger.addInvocationMetrics( 405 InvocationMetricKey.BUGREPORT_TIME, System.currentTimeMillis() - startTime); 406 InvocationMetricLogger.addInvocationMetrics(InvocationMetricKey.BUGREPORT_COUNT, 1); 407 } 408 } 409 410 /** {@inheritDoc} */ 411 @Override getBugreportz()412 public InputStreamSource getBugreportz() { 413 if (getApiLevelSafe() < 24) { 414 return null; 415 } 416 CLog.d("Start getBugreportz()"); 417 long startTime = System.currentTimeMillis(); 418 try { 419 File bugreportZip = getBugreportzInternal(); 420 if (bugreportZip != null) { 421 return new FileInputStreamSource(bugreportZip, true); 422 } 423 return null; 424 } finally { 425 CLog.d("Done with getBugreportz()"); 426 InvocationMetricLogger.addInvocationMetrics( 427 InvocationMetricKey.BUGREPORT_TIME, System.currentTimeMillis() - startTime); 428 InvocationMetricLogger.addInvocationMetrics(InvocationMetricKey.BUGREPORT_COUNT, 1); 429 } 430 } 431 432 /** Internal Helper method to get the bugreportz zip file as a {@link File}. */ 433 @VisibleForTesting getBugreportzInternal()434 protected File getBugreportzInternal() { 435 CollectingOutputReceiver receiver = new CollectingOutputReceiver(); 436 // Does not rely on {@link ITestDevice#executeAdbCommand(String...)} because it does not 437 // provide a timeout. 438 try { 439 executeShellCommand( 440 BUGREPORTZ_CMD, 441 receiver, 442 getOptions().getBugreportzTimeout(), 443 TimeUnit.MILLISECONDS, 444 0 /* don't retry */); 445 String output = receiver.getOutput().trim(); 446 Matcher match = BUGREPORTZ_RESPONSE_PATTERN.matcher(output); 447 if (!match.find()) { 448 CLog.e("Something went went wrong during bugreportz collection: '%s'", output); 449 return null; 450 } else { 451 String remoteFilePath = match.group(2); 452 if (Strings.isNullOrEmpty(remoteFilePath)) { 453 CLog.e("Invalid bugreportz path found from output: %s", output); 454 return null; 455 } 456 File zipFile = null; 457 try { 458 if (!doesFileExist(remoteFilePath)) { 459 CLog.e("Did not find bugreportz at: '%s'", remoteFilePath); 460 return null; 461 } 462 // Create a placeholder to replace the file 463 zipFile = FileUtil.createTempFile("bugreportz", ".zip"); 464 // pull 465 pullFile(remoteFilePath, zipFile); 466 String bugreportDir = 467 remoteFilePath.substring(0, remoteFilePath.lastIndexOf('/')); 468 if (!bugreportDir.isEmpty()) { 469 // clean bugreport files directory on device 470 deleteFile(String.format("%s/*", bugreportDir)); 471 } 472 473 return zipFile; 474 } catch (IOException e) { 475 CLog.e("Failed to create the temporary file."); 476 return null; 477 } 478 } 479 } catch (DeviceNotAvailableException e) { 480 CLog.e("Device %s became unresponsive while retrieving bugreportz", getSerialNumber()); 481 CLog.e(e); 482 } 483 return null; 484 } 485 getBugreportInternal()486 protected InputStreamSource getBugreportInternal() { 487 CollectingByteOutputReceiver receiver = new CollectingByteOutputReceiver(); 488 try { 489 executeShellCommand( 490 BUGREPORT_CMD, 491 receiver, 492 BUGREPORT_TIMEOUT, 493 TimeUnit.MILLISECONDS, 494 0 /* don't retry */); 495 } catch (DeviceNotAvailableException e) { 496 // Log, but don't throw, so the caller can get the bugreport contents even 497 // if the device goes away 498 CLog.e("Device %s became unresponsive while retrieving bugreport", getSerialNumber()); 499 return null; 500 } 501 return new ByteArrayInputStreamSource(receiver.getOutput()); 502 } 503 504 /** 505 * {@inheritDoc} 506 */ 507 @Override installPackage(final File packageFile, final boolean reinstall, final String... extraArgs)508 public String installPackage(final File packageFile, final boolean reinstall, 509 final String... extraArgs) throws DeviceNotAvailableException { 510 boolean runtimePermissionSupported = isRuntimePermissionSupported(); 511 List<String> args = new ArrayList<>(Arrays.asList(extraArgs)); 512 // grant all permissions by default if feature is supported 513 if (runtimePermissionSupported) { 514 args.add("-g"); 515 } 516 return internalInstallPackage(packageFile, reinstall, args); 517 } 518 519 /** 520 * {@inheritDoc} 521 */ 522 @Override installPackage(File packageFile, boolean reinstall, boolean grantPermissions, String... extraArgs)523 public String installPackage(File packageFile, boolean reinstall, boolean grantPermissions, 524 String... extraArgs) throws DeviceNotAvailableException { 525 ensureRuntimePermissionSupported(); 526 List<String> args = new ArrayList<>(Arrays.asList(extraArgs)); 527 if (grantPermissions) { 528 args.add("-g"); 529 } 530 return internalInstallPackage(packageFile, reinstall, args); 531 } 532 installPackage(final File packageFile, final File certFile, final boolean reinstall, final String... extraArgs)533 public String installPackage(final File packageFile, final File certFile, 534 final boolean reinstall, final String... extraArgs) throws DeviceNotAvailableException { 535 long startTime = System.currentTimeMillis(); 536 try { 537 // use array to store response, so it can be returned to caller 538 final String[] response = new String[1]; 539 DeviceAction installAction = 540 new DeviceAction() { 541 @Override 542 public boolean run() 543 throws InstallException, SyncException, IOException, 544 TimeoutException, AdbCommandRejectedException { 545 // TODO: create a getIDevice().installPackage(File, File...) method when 546 // the 547 // dist cert functionality is ready to be open sourced 548 String remotePackagePath = 549 getIDevice().syncPackageToDevice(packageFile.getAbsolutePath()); 550 String remoteCertPath = 551 getIDevice().syncPackageToDevice(certFile.getAbsolutePath()); 552 // trick installRemotePackage into issuing a 'pm install <apk> <cert>' 553 // command, by adding apk path to extraArgs, and using cert as the 554 // 'apk file'. 555 String[] newExtraArgs = new String[extraArgs.length + 1]; 556 System.arraycopy(extraArgs, 0, newExtraArgs, 0, extraArgs.length); 557 newExtraArgs[newExtraArgs.length - 1] = 558 String.format("\"%s\"", remotePackagePath); 559 try { 560 InstallReceiver receiver = createInstallReceiver(); 561 getIDevice() 562 .installRemotePackage( 563 remoteCertPath, 564 reinstall, 565 receiver, 566 INSTALL_TIMEOUT_MINUTES, 567 INSTALL_TIMEOUT_TO_OUTPUT_MINUTES, 568 TimeUnit.MINUTES, 569 newExtraArgs); 570 response[0] = handleInstallReceiver(receiver, packageFile); 571 } catch (InstallException e) { 572 response[0] = handleInstallationError(e); 573 } finally { 574 getIDevice().removeRemotePackage(remotePackagePath); 575 getIDevice().removeRemotePackage(remoteCertPath); 576 } 577 return true; 578 } 579 }; 580 performDeviceAction( 581 String.format("install %s", packageFile.getAbsolutePath()), 582 installAction, 583 MAX_RETRY_ATTEMPTS); 584 List<File> packageFiles = new ArrayList<>(); 585 packageFiles.add(packageFile); 586 allowLegacyStorageForApps(packageFiles); 587 return response[0]; 588 } finally { 589 InvocationMetricLogger.addInvocationMetrics( 590 InvocationMetricKey.PACKAGE_INSTALL_COUNT, 1); 591 InvocationMetricLogger.addInvocationMetrics( 592 InvocationMetricKey.PACKAGE_INSTALL_TIME, 593 System.currentTimeMillis() - startTime); 594 } 595 } 596 597 /** 598 * {@inheritDoc} 599 */ 600 @Override installPackageForUser(File packageFile, boolean reinstall, int userId, String... extraArgs)601 public String installPackageForUser(File packageFile, boolean reinstall, int userId, 602 String... extraArgs) throws DeviceNotAvailableException { 603 boolean runtimePermissionSupported = isRuntimePermissionSupported(); 604 List<String> args = new ArrayList<>(Arrays.asList(extraArgs)); 605 // grant all permissions by default if feature is supported 606 if (runtimePermissionSupported) { 607 args.add("-g"); 608 } 609 args.add("--user"); 610 args.add(Integer.toString(userId)); 611 return internalInstallPackage(packageFile, reinstall, args); 612 } 613 614 /** 615 * {@inheritDoc} 616 */ 617 @Override installPackageForUser(File packageFile, boolean reinstall, boolean grantPermissions, int userId, String... extraArgs)618 public String installPackageForUser(File packageFile, boolean reinstall, 619 boolean grantPermissions, int userId, String... extraArgs) 620 throws DeviceNotAvailableException { 621 ensureRuntimePermissionSupported(); 622 List<String> args = new ArrayList<>(Arrays.asList(extraArgs)); 623 if (grantPermissions) { 624 args.add("-g"); 625 } 626 args.add("--user"); 627 args.add(Integer.toString(userId)); 628 return internalInstallPackage(packageFile, reinstall, args); 629 } 630 631 /** 632 * {@inheritDoc} 633 */ 634 @Override uninstallPackage(final String packageName)635 public String uninstallPackage(final String packageName) throws DeviceNotAvailableException { 636 // use array to store response, so it can be returned to caller 637 return uninstallPackage(packageName, /* extraArgs= */ null); 638 } 639 uninstallPackage(String packageName, @Nullable String extraArgs)640 private String uninstallPackage(String packageName, @Nullable String extraArgs) 641 throws DeviceNotAvailableException { 642 final String finalExtraArgs = (extraArgs == null) ? "" : extraArgs; 643 644 // use array to store response, so it can be returned to caller 645 final String[] response = new String[1]; 646 DeviceAction uninstallAction = 647 () -> { 648 CLog.d("Uninstalling %s with extra args %s", packageName, finalExtraArgs); 649 650 String result = getIDevice().uninstallApp(packageName, finalExtraArgs); 651 response[0] = result; 652 return result == null; 653 }; 654 655 performDeviceAction( 656 String.format("uninstall %s with extra args %s", packageName, finalExtraArgs), 657 uninstallAction, 658 MAX_RETRY_ATTEMPTS); 659 return response[0]; 660 } 661 662 /** {@inheritDoc} */ 663 @Override uninstallPackageForUser(final String packageName, int userId)664 public String uninstallPackageForUser(final String packageName, int userId) 665 throws DeviceNotAvailableException { 666 return uninstallPackage(packageName, "--user " + userId); 667 } 668 669 /** 670 * Core implementation for installing application with split apk files {@link 671 * IDevice#installPackages(List, boolean, List)} See 672 * "https://developer.android.com/studio/build/configure-apk-splits" on how to split apk to 673 * several files. 674 * 675 * @param packageFiles the local apk files 676 * @param reinstall <code>true</code> if a reinstall should be performed 677 * @param extraArgs optional extra arguments to pass. See 'adb shell pm -h' for available 678 * options. 679 * @return the response from the installation <code>null</code> if installation succeeds. 680 * @throws DeviceNotAvailableException 681 */ internalInstallPackages( final List<File> packageFiles, final boolean reinstall, final List<String> extraArgs)682 private String internalInstallPackages( 683 final List<File> packageFiles, final boolean reinstall, final List<String> extraArgs) 684 throws DeviceNotAvailableException { 685 long startTime = System.currentTimeMillis(); 686 try { 687 // use array to store response, so it can be returned to caller 688 final String[] response = new String[1]; 689 DeviceAction installAction = 690 new DeviceAction() { 691 @Override 692 public boolean run() throws InstallException { 693 try { 694 getIDevice() 695 .installPackages( 696 packageFiles, 697 reinstall, 698 extraArgs, 699 INSTALL_TIMEOUT_MINUTES, 700 TimeUnit.MINUTES); 701 response[0] = null; 702 return true; 703 } catch (InstallException e) { 704 response[0] = handleInstallationError(e); 705 return false; 706 } 707 } 708 }; 709 performDeviceAction( 710 String.format("install %s", packageFiles.toString()), 711 installAction, 712 MAX_RETRY_ATTEMPTS); 713 allowLegacyStorageForApps(packageFiles); 714 return response[0]; 715 } finally { 716 InvocationMetricLogger.addInvocationMetrics( 717 InvocationMetricKey.PACKAGE_INSTALL_COUNT, 1); 718 InvocationMetricLogger.addInvocationMetrics( 719 InvocationMetricKey.PACKAGE_INSTALL_TIME, 720 System.currentTimeMillis() - startTime); 721 } 722 } 723 724 /** 725 * Allows Legacy External Storage access for apps that request for it. 726 * 727 * <p>Apps that request for legacy external storage access are granted the access by setting 728 * MANAGE_EXTERNAL_STORAGE App Op. This gives the app File manager privileges, File managers 729 * have legacy external storage access. 730 * 731 * @param appFiles List of Files. Apk Files of the apps that are installed. 732 */ allowLegacyStorageForApps(List<File> appFiles)733 private void allowLegacyStorageForApps(List<File> appFiles) throws DeviceNotAvailableException { 734 for (File appFile : appFiles) { 735 AaptParser aaptParser = createParser(appFile); 736 if (aaptParser != null 737 && aaptParser.getTargetSdkVersion() > 29 738 && aaptParser.isRequestingLegacyStorage()) { 739 if (!aaptParser.isUsingPermissionManageExternalStorage()) { 740 CLog.w( 741 "App is requesting legacy storage and targets R or above, but didn't" 742 + " request the MANAGE_EXTERNAL_STORAGE permission so the" 743 + " associated app op cannot be automatically granted and the" 744 + " app won't have legacy external storage access: " 745 + aaptParser.getPackageName()); 746 continue; 747 } 748 // Set the MANAGE_EXTERNAL_STORAGE App Op to MODE_ALLOWED (Code = 0) 749 // for all users. 750 ArrayList<Integer> userIds = listUsers(); 751 for (int userId : userIds) { 752 CommandResult setFileManagerAppOpResult = 753 executeShellV2Command( 754 "appops set --user " 755 + userId 756 + " --uid " 757 + aaptParser.getPackageName() 758 + " MANAGE_EXTERNAL_STORAGE 0"); 759 if (!CommandStatus.SUCCESS.equals(setFileManagerAppOpResult.getStatus())) { 760 CLog.e( 761 "Failed to set MANAGE_EXTERNAL_STORAGE App Op to" 762 + " allow legacy external storage for: %s ; stderr: %s", 763 aaptParser.getPackageName(), setFileManagerAppOpResult.getStderr()); 764 } 765 } 766 } 767 } 768 CommandResult persistFileManagerAppOpResult = 769 executeShellV2Command("appops write-settings"); 770 if (!CommandStatus.SUCCESS.equals(persistFileManagerAppOpResult.getStatus())) { 771 CLog.e( 772 "Failed to persist MANAGE_EXTERNAL_STORAGE App Op over `adb reboot`: %s", 773 persistFileManagerAppOpResult.getStderr()); 774 } 775 } 776 777 @VisibleForTesting createParser(File appFile)778 protected AaptParser createParser(File appFile) { 779 return AaptParser.parse(appFile); 780 } 781 782 /** {@inheritDoc} */ 783 @Override installPackages( final List<File> packageFiles, final boolean reinstall, final String... extraArgs)784 public String installPackages( 785 final List<File> packageFiles, final boolean reinstall, final String... extraArgs) 786 throws DeviceNotAvailableException { 787 // Grant all permissions by default if feature is supported 788 return installPackages(packageFiles, reinstall, isRuntimePermissionSupported(), extraArgs); 789 } 790 791 /** {@inheritDoc} */ 792 @Override installPackages( List<File> packageFiles, boolean reinstall, boolean grantPermissions, String... extraArgs)793 public String installPackages( 794 List<File> packageFiles, 795 boolean reinstall, 796 boolean grantPermissions, 797 String... extraArgs) 798 throws DeviceNotAvailableException { 799 List<String> args = new ArrayList<>(Arrays.asList(extraArgs)); 800 if (grantPermissions) { 801 ensureRuntimePermissionSupported(); 802 args.add("-g"); 803 } 804 return internalInstallPackages(packageFiles, reinstall, args); 805 } 806 807 /** {@inheritDoc} */ 808 @Override installPackagesForUser( List<File> packageFiles, boolean reinstall, int userId, String... extraArgs)809 public String installPackagesForUser( 810 List<File> packageFiles, boolean reinstall, int userId, String... extraArgs) 811 throws DeviceNotAvailableException { 812 // Grant all permissions by default if feature is supported 813 return installPackagesForUser( 814 packageFiles, reinstall, isRuntimePermissionSupported(), userId, extraArgs); 815 } 816 817 /** {@inheritDoc} */ 818 @Override installPackagesForUser( List<File> packageFiles, boolean reinstall, boolean grantPermissions, int userId, String... extraArgs)819 public String installPackagesForUser( 820 List<File> packageFiles, 821 boolean reinstall, 822 boolean grantPermissions, 823 int userId, 824 String... extraArgs) 825 throws DeviceNotAvailableException { 826 List<String> args = new ArrayList<>(Arrays.asList(extraArgs)); 827 if (grantPermissions) { 828 ensureRuntimePermissionSupported(); 829 args.add("-g"); 830 } 831 args.add("--user"); 832 args.add(Integer.toString(userId)); 833 return internalInstallPackages(packageFiles, reinstall, args); 834 } 835 836 /** 837 * Core implementation for split apk remote installation {@link IDevice#installPackage(String, 838 * boolean, String...)} See "https://developer.android.com/studio/build/configure-apk-splits" on 839 * how to split apk to several files. 840 * 841 * @param remoteApkPaths the remote apk file paths 842 * @param reinstall <code>true</code> if a reinstall should be performed 843 * @param extraArgs optional extra arguments to pass. See 'adb shell pm -h' for available 844 * options. 845 * @return the response from the installation <code>null</code> if installation succeeds. 846 * @throws DeviceNotAvailableException 847 */ internalInstallRemotePackages( final List<String> remoteApkPaths, final boolean reinstall, final List<String> extraArgs)848 private String internalInstallRemotePackages( 849 final List<String> remoteApkPaths, 850 final boolean reinstall, 851 final List<String> extraArgs) 852 throws DeviceNotAvailableException { 853 // use array to store response, so it can be returned to caller 854 final String[] response = new String[1]; 855 DeviceAction installAction = 856 new DeviceAction() { 857 @Override 858 public boolean run() throws InstallException { 859 try { 860 getIDevice() 861 .installRemotePackages( 862 remoteApkPaths, 863 reinstall, 864 extraArgs, 865 INSTALL_TIMEOUT_MINUTES, 866 TimeUnit.MINUTES); 867 response[0] = null; 868 return true; 869 } catch (InstallException e) { 870 response[0] = handleInstallationError(e); 871 return false; 872 } 873 } 874 }; 875 performDeviceAction( 876 String.format("install %s", remoteApkPaths.toString()), 877 installAction, 878 MAX_RETRY_ATTEMPTS); 879 return response[0]; 880 } 881 882 /** {@inheritDoc} */ 883 @Override installRemotePackages( final List<String> remoteApkPaths, final boolean reinstall, final String... extraArgs)884 public String installRemotePackages( 885 final List<String> remoteApkPaths, final boolean reinstall, final String... extraArgs) 886 throws DeviceNotAvailableException { 887 // Grant all permissions by default if feature is supported 888 return installRemotePackages( 889 remoteApkPaths, reinstall, isRuntimePermissionSupported(), extraArgs); 890 } 891 892 /** {@inheritDoc} */ 893 @Override installRemotePackages( List<String> remoteApkPaths, boolean reinstall, boolean grantPermissions, String... extraArgs)894 public String installRemotePackages( 895 List<String> remoteApkPaths, 896 boolean reinstall, 897 boolean grantPermissions, 898 String... extraArgs) 899 throws DeviceNotAvailableException { 900 List<String> args = new ArrayList<>(Arrays.asList(extraArgs)); 901 if (grantPermissions) { 902 ensureRuntimePermissionSupported(); 903 args.add("-g"); 904 } 905 return internalInstallRemotePackages(remoteApkPaths, reinstall, args); 906 } 907 908 /** {@inheritDoc} */ 909 @Override getScreenshot()910 public InputStreamSource getScreenshot() throws DeviceNotAvailableException { 911 return getScreenshot("PNG"); 912 } 913 914 /** 915 * {@inheritDoc} 916 */ 917 @Override getScreenshot(String format)918 public InputStreamSource getScreenshot(String format) throws DeviceNotAvailableException { 919 return getScreenshot(format, true); 920 } 921 922 /** {@inheritDoc} */ 923 @Override getScreenshot(String format, boolean rescale)924 public InputStreamSource getScreenshot(String format, boolean rescale) 925 throws DeviceNotAvailableException { 926 if (!format.equalsIgnoreCase("PNG") && !format.equalsIgnoreCase("JPEG")){ 927 CLog.e("Screenshot: Format %s is not supported, defaulting to PNG.", format); 928 format = "PNG"; 929 } 930 ScreenshotAction action = new ScreenshotAction(); 931 if (performDeviceAction("screenshot", action, MAX_RETRY_ATTEMPTS)) { 932 byte[] imageData = 933 compressRawImage(action.mRawScreenshot, format.toUpperCase(), rescale); 934 if (imageData != null) { 935 return new ByteArrayInputStreamSource(imageData); 936 } 937 } 938 // Return an error in the buffer 939 return new ByteArrayInputStreamSource( 940 "Error: device reported null for screenshot.".getBytes()); 941 } 942 943 /** {@inheritDoc} */ 944 @Override getScreenshot(long displayId)945 public InputStreamSource getScreenshot(long displayId) throws DeviceNotAvailableException { 946 final String tmpDevicePath = String.format("/data/local/tmp/display_%s.png", displayId); 947 CommandResult result = 948 executeShellV2Command( 949 String.format("screencap -p -d %s %s", displayId, tmpDevicePath)); 950 if (!CommandStatus.SUCCESS.equals(result.getStatus())) { 951 // Return an error in the buffer 952 CLog.e("Error: device reported error for screenshot:"); 953 CLog.e("stdout: %s\nstderr: %s", result.getStdout(), result.getStderr()); 954 return null; 955 } 956 try { 957 File tmpScreenshot = pullFile(tmpDevicePath); 958 if (tmpScreenshot == null) { 959 return null; 960 } 961 return new FileInputStreamSource(tmpScreenshot, true); 962 } finally { 963 deleteFile(tmpDevicePath); 964 } 965 } 966 967 private class ScreenshotAction implements DeviceAction { 968 969 RawImage mRawScreenshot; 970 971 /** 972 * {@inheritDoc} 973 */ 974 @Override run()975 public boolean run() throws IOException, TimeoutException, AdbCommandRejectedException, 976 ShellCommandUnresponsiveException, InstallException, SyncException { 977 mRawScreenshot = 978 getIDevice().getScreenshot(MAX_SCREENSHOT_TIMEOUT, TimeUnit.MILLISECONDS); 979 return mRawScreenshot != null; 980 } 981 } 982 983 /** 984 * Helper to compress a rawImage obtained from the screen. 985 * 986 * @param rawImage {@link RawImage} to compress. 987 * @param format resulting format of compressed image. PNG and JPEG are supported. 988 * @param rescale if rescaling should be done to further reduce size of compressed image. 989 * @return compressed image. 990 */ 991 @VisibleForTesting compressRawImage(RawImage rawImage, String format, boolean rescale)992 byte[] compressRawImage(RawImage rawImage, String format, boolean rescale) { 993 BufferedImage image = rawImageToBufferedImage(rawImage, format); 994 995 // Rescale to reduce size if needed 996 // Screenshot default format is 1080 x 1920, 8-bit/color RGBA 997 // By cutting in half we can easily keep good quality and smaller size 998 if (rescale) { 999 image = rescaleImage(image); 1000 } 1001 1002 return getImageData(image, format); 1003 } 1004 1005 /** 1006 * Converts {@link RawImage} to {@link BufferedImage} in specified format. 1007 * 1008 * @param rawImage {@link RawImage} to convert. 1009 * @param format resulting format of image. PNG and JPEG are supported. 1010 * @return converted image. 1011 */ 1012 @VisibleForTesting rawImageToBufferedImage(RawImage rawImage, String format)1013 BufferedImage rawImageToBufferedImage(RawImage rawImage, String format) { 1014 BufferedImage image = null; 1015 1016 if ("JPEG".equalsIgnoreCase(format)) { 1017 //JPEG does not support ARGB without a special encoder 1018 image = 1019 new BufferedImage( 1020 rawImage.width, rawImage.height, BufferedImage.TYPE_3BYTE_BGR); 1021 } 1022 else { 1023 image = new BufferedImage(rawImage.width, rawImage.height, BufferedImage.TYPE_INT_ARGB); 1024 } 1025 1026 // borrowed conversion logic from platform/sdk/screenshot/.../Screenshot.java 1027 int index = 0; 1028 int IndexInc = rawImage.bpp >> 3; 1029 for (int y = 0 ; y < rawImage.height ; y++) { 1030 for (int x = 0 ; x < rawImage.width ; x++) { 1031 int value = rawImage.getARGB(index); 1032 index += IndexInc; 1033 image.setRGB(x, y, value); 1034 } 1035 } 1036 1037 return image; 1038 } 1039 1040 /** 1041 * Rescales image cutting it in half. 1042 * 1043 * @param image source {@link BufferedImage}. 1044 * @return resulting scaled image. 1045 */ 1046 @VisibleForTesting rescaleImage(BufferedImage image)1047 BufferedImage rescaleImage(BufferedImage image) { 1048 int shortEdge = Math.min(image.getHeight(), image.getWidth()); 1049 if (shortEdge > 720) { 1050 Image resized = 1051 image.getScaledInstance( 1052 image.getWidth() / 2, image.getHeight() / 2, Image.SCALE_SMOOTH); 1053 image = 1054 new BufferedImage( 1055 image.getWidth() / 2, image.getHeight() / 2, Image.SCALE_REPLICATE); 1056 image.getGraphics().drawImage(resized, 0, 0, null); 1057 } 1058 return image; 1059 } 1060 1061 /** 1062 * Gets byte array representation of {@link BufferedImage}. 1063 * 1064 * @param image source {@link BufferedImage}. 1065 * @param format resulting format of image. PNG and JPEG are supported. 1066 * @return byte array representation of the image. 1067 */ 1068 @VisibleForTesting getImageData(BufferedImage image, String format)1069 byte[] getImageData(BufferedImage image, String format) { 1070 // store compressed image in memory, and let callers write to persistent storage 1071 // use initial buffer size of 128K 1072 byte[] imageData = null; 1073 ByteArrayOutputStream imageOut = new ByteArrayOutputStream(128*1024); 1074 try { 1075 if (ImageIO.write(image, format, imageOut)) { 1076 imageData = imageOut.toByteArray(); 1077 } else { 1078 CLog.e("Failed to compress screenshot to png"); 1079 } 1080 } catch (IOException e) { 1081 CLog.e("Failed to compress screenshot to png"); 1082 CLog.e(e); 1083 } 1084 StreamUtil.close(imageOut); 1085 return imageData; 1086 } 1087 1088 /** 1089 * {@inheritDoc} 1090 */ 1091 @Override clearErrorDialogs()1092 public boolean clearErrorDialogs() throws DeviceNotAvailableException { 1093 CommandResult dismissResult = 1094 executeShellV2Command( 1095 DISMISS_DIALOG_BROADCAST, 60000L, TimeUnit.MILLISECONDS, 0 /*retry*/); 1096 if (!CommandStatus.SUCCESS.equals(dismissResult.getStatus())) { 1097 CLog.w("Issue with dimissing dialog broadcast. %s", dismissResult); 1098 } 1099 executeShellCommand(COLLAPSE_STATUS_BAR); 1100 // attempt to clear error dialogs multiple times 1101 for (int i = 0; i < NUM_CLEAR_ATTEMPTS; i++) { 1102 int numErrorDialogs = getErrorDialogCount(); 1103 if (numErrorDialogs == 0) { 1104 return true; 1105 } 1106 doClearDialogs(numErrorDialogs); 1107 } 1108 if (getErrorDialogCount() > 0) { 1109 // at this point, all attempts to clear error dialogs completely have failed 1110 // it might be the case that the process keeps showing new dialogs immediately after 1111 // clearing. There's really no workaround, but to dump an error 1112 CLog.e("error dialogs still exist on %s.", getSerialNumber()); 1113 return false; 1114 } 1115 return true; 1116 } 1117 1118 /** 1119 * Detects the number of crash or ANR dialogs currently displayed. 1120 * <p/> 1121 * Parses output of 'dump activity processes' 1122 * 1123 * @return count of dialogs displayed 1124 * @throws DeviceNotAvailableException 1125 */ getErrorDialogCount()1126 private int getErrorDialogCount() throws DeviceNotAvailableException { 1127 int errorDialogCount = 0; 1128 Pattern crashPattern = Pattern.compile(".*crashing=true.*AppErrorDialog.*"); 1129 Pattern anrPattern = Pattern.compile(".*notResponding=true.*AppNotRespondingDialog.*"); 1130 String systemStatusOutput = 1131 executeShellCommand( 1132 "dumpsys activity processes | grep -e .*crashing=true.*AppErrorDialog.* -e" 1133 + " .*notResponding=true.*AppNotRespondingDialog.*"); 1134 Matcher crashMatcher = crashPattern.matcher(systemStatusOutput); 1135 while (crashMatcher.find()) { 1136 errorDialogCount++; 1137 } 1138 Matcher anrMatcher = anrPattern.matcher(systemStatusOutput); 1139 while (anrMatcher.find()) { 1140 errorDialogCount++; 1141 } 1142 1143 return errorDialogCount; 1144 } 1145 doClearDialogs(int numDialogs)1146 private void doClearDialogs(int numDialogs) throws DeviceNotAvailableException { 1147 CLog.i("Attempted to clear %d dialogs on %s", numDialogs, getSerialNumber()); 1148 for (int i=0; i < numDialogs; i++) { 1149 // send DPAD_CENTER 1150 executeShellCommand(DISMISS_DIALOG_CMD); 1151 } 1152 } 1153 1154 /** 1155 * {@inheritDoc} 1156 */ 1157 @Override disableKeyguard()1158 public void disableKeyguard() throws DeviceNotAvailableException { 1159 long start = System.currentTimeMillis(); 1160 while (true) { 1161 Boolean ready = isDeviceInputReady(); 1162 if (ready == null) { 1163 // unsupported API level, bail 1164 break; 1165 } 1166 if (ready) { 1167 // input dispatch is ready, bail 1168 break; 1169 } 1170 long timeSpent = System.currentTimeMillis() - start; 1171 if (timeSpent > INPUT_DISPATCH_READY_TIMEOUT) { 1172 CLog.w("Timeout after waiting %dms on enabling of input dispatch", timeSpent); 1173 // break & proceed anyway 1174 break; 1175 } else { 1176 getRunUtil().sleep(1000); 1177 } 1178 } 1179 if (getApiLevel() >= 23) { 1180 CLog.i( 1181 "Attempting to disable keyguard on %s using %s", 1182 getSerialNumber(), DISMISS_KEYGUARD_WM_CMD); 1183 String output = executeShellCommand(DISMISS_KEYGUARD_WM_CMD); 1184 CLog.i("output of %s: %s", DISMISS_KEYGUARD_WM_CMD, output); 1185 } else { 1186 CLog.i("Command: %s, is not supported, falling back to %s", DISMISS_KEYGUARD_WM_CMD, 1187 DISMISS_KEYGUARD_CMD); 1188 executeShellCommand(DISMISS_KEYGUARD_CMD); 1189 } 1190 verifyKeyguardDismissed(); 1191 } 1192 verifyKeyguardDismissed()1193 private void verifyKeyguardDismissed() throws DeviceNotAvailableException { 1194 long start = System.currentTimeMillis(); 1195 while (true) { 1196 KeyguardControllerState state = getKeyguardState(); 1197 if (state == null) { 1198 return; // unsupported 1199 } 1200 if (!state.isKeyguardShowing()) { 1201 return; // keyguard dismissed successfully 1202 } 1203 long timeSpent = System.currentTimeMillis() - start; 1204 if (timeSpent > DISMISS_KEYGUARD_TIMEOUT) { 1205 if (state.isKeyguardGoingAway()) { 1206 CLog.w("Keyguard still going away %dms after being dismissed", timeSpent); 1207 } else { 1208 CLog.w("No response from keyguard %dms after being dismissed", timeSpent); 1209 } 1210 return; // proceed anyway, may be dismissed in a later step 1211 } 1212 getRunUtil().sleep(500); 1213 } 1214 } 1215 1216 /** {@inheritDoc} */ 1217 @Override getKeyguardState()1218 public KeyguardControllerState getKeyguardState() throws DeviceNotAvailableException { 1219 String output = executeShellCommand(KEYGUARD_CONTROLLER_CMD); 1220 CLog.d("Output from KeyguardController: %s", output); 1221 KeyguardControllerState state = 1222 KeyguardControllerState.create(Arrays.asList(output.trim().split("\n"))); 1223 return state; 1224 } 1225 1226 /** 1227 * Tests the device to see if input dispatcher is ready 1228 * 1229 * @return <code>null</code> if not supported by platform, or the actual readiness state 1230 * @throws DeviceNotAvailableException 1231 */ isDeviceInputReady()1232 Boolean isDeviceInputReady() throws DeviceNotAvailableException { 1233 CollectingOutputReceiver receiver = new CollectingOutputReceiver(); 1234 executeShellCommand(TEST_INPUT_CMD, receiver); 1235 String output = receiver.getOutput(); 1236 Matcher m = INPUT_DISPATCH_STATE_REGEX.matcher(output); 1237 if (!m.find()) { 1238 // output does not contain the line at all, implying unsupported API level, bail 1239 return null; 1240 } 1241 return "1".equals(m.group(1)); 1242 } 1243 1244 /** 1245 * {@inheritDoc} 1246 */ 1247 @Override prePostBootSetup()1248 protected void prePostBootSetup() throws DeviceNotAvailableException { 1249 if (mOptions.isDisableKeyguard()) { 1250 disableKeyguard(); 1251 } 1252 } 1253 1254 /** 1255 * Performs an reboot via framework power manager 1256 * 1257 * <p>Must have root access, device must be API Level 18 or above 1258 * 1259 * @param rebootMode a mode of this reboot. 1260 * @param reason for this reboot. 1261 * @return <code>true</code> if the device rebooted, <code>false</code> if not successful or 1262 * unsupported 1263 * @throws DeviceNotAvailableException 1264 */ doAdbFrameworkReboot(RebootMode rebootMode, @Nullable final String reason)1265 private boolean doAdbFrameworkReboot(RebootMode rebootMode, @Nullable final String reason) 1266 throws DeviceNotAvailableException { 1267 // use framework reboot when: 1268 // 1. device API level >= 18 1269 // 2. has adb root 1270 // 3. framework is running 1271 if (!isEnableAdbRoot()) { 1272 CLog.i("framework reboot is not supported; when enable root is disabled"); 1273 return false; 1274 } 1275 boolean isRoot = enableAdbRoot(); 1276 if (getApiLevel() >= 18 && isRoot) { 1277 try { 1278 // check framework running 1279 String output = executeShellCommand("pm path android"); 1280 if (output == null || !output.contains("package:")) { 1281 CLog.v("framework reboot: can't detect framework running"); 1282 return false; 1283 } 1284 notifyRebootStarted(); 1285 String command = "svc power reboot " + rebootMode.formatRebootCommand(reason); 1286 CommandResult result = executeShellV2Command(command); 1287 if (result.getStdout().contains(EARLY_REBOOT) 1288 || result.getStderr().contains(EARLY_REBOOT)) { 1289 CLog.e( 1290 "Reboot was called too early: stdout: %s.\nstderr: %s.", 1291 result.getStdout(), result.getStderr()); 1292 // notify of this reboot end, since reboot will be retried again at later stage. 1293 notifyRebootEnded(); 1294 return false; 1295 } 1296 } catch (DeviceUnresponsiveException due) { 1297 CLog.v("framework reboot: device unresponsive to shell command, using fallback"); 1298 return false; 1299 } 1300 postAdbReboot(); 1301 return true; 1302 } else { 1303 CLog.v("framework reboot: not supported"); 1304 return false; 1305 } 1306 } 1307 1308 /** 1309 * Perform a adb reboot. 1310 * 1311 * @param rebootMode a mode of this reboot. 1312 * @param reason for this reboot. 1313 * @throws DeviceNotAvailableException 1314 */ 1315 @Override doAdbReboot(RebootMode rebootMode, @Nullable final String reason)1316 protected void doAdbReboot(RebootMode rebootMode, @Nullable final String reason) 1317 throws DeviceNotAvailableException { 1318 getConnection().notifyAdbRebootCalled(); 1319 if (!TestDeviceState.ONLINE.equals(getDeviceState()) 1320 || !doAdbFrameworkReboot(rebootMode, reason)) { 1321 super.doAdbReboot(rebootMode, reason); 1322 } 1323 } 1324 1325 /** 1326 * {@inheritDoc} 1327 */ 1328 @Override getInstalledPackageNames()1329 public Set<String> getInstalledPackageNames() throws DeviceNotAvailableException { 1330 return getInstalledPackageNames(null, null); 1331 } 1332 1333 // TODO: convert this to use DumpPkgAction getInstalledPackageNames(String packageNameSearched, String userId)1334 private Set<String> getInstalledPackageNames(String packageNameSearched, String userId) 1335 throws DeviceNotAvailableException { 1336 Set<String> packages= new HashSet<String>(); 1337 String command = LIST_PACKAGES_CMD; 1338 if (userId != null) { 1339 command += String.format(" --user %s", userId); 1340 } 1341 if (packageNameSearched != null) { 1342 command += (" | grep " + packageNameSearched); 1343 } 1344 String output = executeShellCommand(command); 1345 if (output != null) { 1346 Matcher m = PACKAGE_REGEX.matcher(output); 1347 while (m.find()) { 1348 String packageName = m.group(2); 1349 if (packageNameSearched != null && packageName.equals(packageNameSearched)) { 1350 packages.add(packageName); 1351 } else if (packageNameSearched == null) { 1352 packages.add(packageName); 1353 } 1354 } 1355 } 1356 return packages; 1357 } 1358 1359 /** {@inheritDoc} */ 1360 @Override isPackageInstalled(String packageName)1361 public boolean isPackageInstalled(String packageName) throws DeviceNotAvailableException { 1362 return getInstalledPackageNames(packageName, null).contains(packageName); 1363 } 1364 1365 /** {@inheritDoc} */ 1366 @Override isPackageInstalled(String packageName, String userId)1367 public boolean isPackageInstalled(String packageName, String userId) 1368 throws DeviceNotAvailableException { 1369 return getInstalledPackageNames(packageName, userId).contains(packageName); 1370 } 1371 1372 /** {@inheritDoc} */ 1373 @Override getActiveApexes()1374 public Set<ApexInfo> getActiveApexes() throws DeviceNotAvailableException { 1375 String output = executeShellCommand(LIST_APEXES_CMD); 1376 // Optimistically parse expecting platform to return paths. If it doesn't, empty set will 1377 // be returned. 1378 Set<ApexInfo> ret = parseApexesFromOutput(output, true /* withPath */); 1379 if (ret.isEmpty()) { 1380 ret = parseApexesFromOutput(output, false /* withPath */); 1381 } 1382 return ret; 1383 } 1384 1385 /** {@inheritDoc} */ 1386 @Override getMainlineModuleInfo()1387 public Set<String> getMainlineModuleInfo() throws DeviceNotAvailableException { 1388 checkApiLevelAgainstNextRelease(GET_MODULEINFOS_CMD, 29); 1389 Set<String> ret = new HashSet<>(); 1390 String output = executeShellCommand(GET_MODULEINFOS_CMD); 1391 if (output != null) { 1392 Matcher m = MODULEINFO_REGEX.matcher(output); 1393 while (m.find()) { 1394 String packageName = m.group(2); 1395 ret.add(packageName); 1396 } 1397 } 1398 return ret; 1399 } 1400 parseApexesFromOutput(final String output, boolean withPath)1401 private Set<ApexInfo> parseApexesFromOutput(final String output, boolean withPath) { 1402 Set<ApexInfo> ret = new HashSet<>(); 1403 Matcher matcher = 1404 withPath 1405 ? APEXES_WITH_PATH_REGEX.matcher(output) 1406 : APEXES_WITHOUT_PATH_REGEX.matcher(output); 1407 while (matcher.find()) { 1408 if (withPath) { 1409 String sourceDir = matcher.group(1); 1410 String name = matcher.group(2); 1411 long version = Long.valueOf(matcher.group(3)); 1412 ret.add(new ApexInfo(name, version, sourceDir)); 1413 } else { 1414 String name = matcher.group(1); 1415 long version = Long.valueOf(matcher.group(2)); 1416 ret.add(new ApexInfo(name, version)); 1417 } 1418 } 1419 return ret; 1420 } 1421 1422 /** 1423 * A {@link com.android.tradefed.device.NativeDevice.DeviceAction} 1424 * for retrieving package system service info, and do retries on 1425 * failures. 1426 */ 1427 private class DumpPkgAction implements DeviceAction { 1428 1429 Map<String, PackageInfo> mPkgInfoMap; 1430 DumpPkgAction()1431 DumpPkgAction() { 1432 } 1433 1434 @Override run()1435 public boolean run() throws IOException, TimeoutException, AdbCommandRejectedException, 1436 ShellCommandUnresponsiveException, InstallException, SyncException { 1437 DumpsysPackageReceiver receiver = new DumpsysPackageReceiver(); 1438 getIDevice().executeShellCommand("dumpsys package p", receiver); 1439 mPkgInfoMap = receiver.getPackages(); 1440 if (mPkgInfoMap.size() == 0) { 1441 // Package parsing can fail if package manager is currently down. throw exception 1442 // to retry 1443 CLog.w("no packages found from dumpsys package p."); 1444 throw new IOException(); 1445 } 1446 return true; 1447 } 1448 } 1449 1450 /** 1451 * {@inheritDoc} 1452 */ 1453 @Override getUninstallablePackageNames()1454 public Set<String> getUninstallablePackageNames() throws DeviceNotAvailableException { 1455 DumpPkgAction action = new DumpPkgAction(); 1456 performDeviceAction("dumpsys package p", action, MAX_RETRY_ATTEMPTS); 1457 1458 Set<String> pkgs = new HashSet<String>(); 1459 for (PackageInfo pkgInfo : action.mPkgInfoMap.values()) { 1460 if (!pkgInfo.isSystemApp() || pkgInfo.isUpdatedSystemApp()) { 1461 CLog.d("Found uninstallable package %s", pkgInfo.getPackageName()); 1462 pkgs.add(pkgInfo.getPackageName()); 1463 } 1464 } 1465 return pkgs; 1466 } 1467 1468 /** 1469 * {@inheritDoc} 1470 */ 1471 @Override getAppPackageInfo(String packageName)1472 public PackageInfo getAppPackageInfo(String packageName) throws DeviceNotAvailableException { 1473 DumpPkgAction action = new DumpPkgAction(); 1474 performDeviceAction("dumpsys package", action, MAX_RETRY_ATTEMPTS); 1475 return action.mPkgInfoMap.get(packageName); 1476 } 1477 1478 /** {@inheritDoc} */ 1479 @Override getAppPackageInfos()1480 public List<PackageInfo> getAppPackageInfos() throws DeviceNotAvailableException { 1481 DumpPkgAction action = new DumpPkgAction(); 1482 performDeviceAction("dumpsys package", action, MAX_RETRY_ATTEMPTS); 1483 return new ArrayList<>(action.mPkgInfoMap.values()); 1484 } 1485 1486 /** {@inheritDoc} */ 1487 @Override doesFileExist(String deviceFilePath)1488 public boolean doesFileExist(String deviceFilePath) throws DeviceNotAvailableException { 1489 int currentUser = 0; 1490 if (deviceFilePath.startsWith(SD_CARD)) { 1491 if (getApiLevel() > 23) { 1492 // Don't trigger the current logic if unsupported 1493 currentUser = getCurrentUser(); 1494 } 1495 } 1496 return doesFileExist(deviceFilePath, currentUser); 1497 } 1498 1499 /** {@inheritDoc} */ 1500 @Override doesFileExist(String deviceFilePath, int userId)1501 public boolean doesFileExist(String deviceFilePath, int userId) 1502 throws DeviceNotAvailableException { 1503 if (deviceFilePath.startsWith(SD_CARD)) { 1504 deviceFilePath = 1505 deviceFilePath.replaceFirst( 1506 SD_CARD, String.format("/storage/emulated/%s/", userId)); 1507 } 1508 return super.doesFileExist(deviceFilePath, userId); 1509 } 1510 1511 /** 1512 * {@inheritDoc} 1513 */ 1514 @Override listUsers()1515 public ArrayList<Integer> listUsers() throws DeviceNotAvailableException { 1516 ArrayList<String[]> users = tokenizeListUsers(); 1517 ArrayList<Integer> userIds = new ArrayList<Integer>(users.size()); 1518 for (String[] user : users) { 1519 userIds.add(Integer.parseInt(user[1])); 1520 } 1521 return userIds; 1522 } 1523 1524 /** {@inheritDoc} */ 1525 @Override getUserInfos()1526 public Map<Integer, UserInfo> getUserInfos() throws DeviceNotAvailableException { 1527 ArrayList<String[]> lines = tokenizeListUsers(); 1528 Map<Integer, UserInfo> result = new HashMap<Integer, UserInfo>(lines.size()); 1529 for (String[] tokens : lines) { 1530 if (getApiLevel() < 33) { 1531 UserInfo userInfo = 1532 new UserInfo( 1533 /* userId= */ Integer.parseInt(tokens[1]), 1534 /* userName= */ tokens[2], 1535 /* flag= */ Integer.parseInt(tokens[3], 16), 1536 /* isRunning= */ tokens.length >= 5 1537 ? tokens[4].contains("running") 1538 : false); 1539 result.put(userInfo.userId(), userInfo); 1540 } else { 1541 UserInfo userInfo = 1542 new UserInfo( 1543 /* userId= */ Integer.parseInt(tokens[1]), 1544 /* userName= */ tokens[2], 1545 /* flag= */ Integer.parseInt(tokens[3], 16), 1546 /* isRunning= */ tokens.length >= 5 1547 ? tokens[4].contains("running") 1548 : false, 1549 /* userType= */ tokens[5]); 1550 result.put(userInfo.userId(), userInfo); 1551 } 1552 } 1553 return result; 1554 } 1555 1556 /** 1557 * Tokenizes the output of 'pm list users' pre-T and 'cmd user list -v' post-T. 1558 * 1559 * <p>Pre-T: The returned tokens for each user have the form: {"\tUserInfo", 1560 * Integer.toString(id), name, Integer.toHexString(flag), "[running]"}; (the last one being 1561 * optional) 1562 * 1563 * <p>Post-T: The returned tokens for each user have the form: {"\tUserInfo", Integer 1564 * .toString(id), name, Integer.toHexString(flag), "[running]", type}; (the last two being 1565 * optional) 1566 * 1567 * @return a list of arrays of strings, each element of the list representing the tokens for a 1568 * user, or {@code null} if there was an error while tokenizing the adb command output. 1569 */ tokenizeListUsers()1570 private ArrayList<String[]> tokenizeListUsers() throws DeviceNotAvailableException { 1571 if (getApiLevel() < 33) { // Android-T 1572 return tokenizeListUsersPreT(); 1573 } else { 1574 return tokenizeListUserPostT(); 1575 } 1576 } 1577 tokenizeListUserPostT()1578 private ArrayList<String[]> tokenizeListUserPostT() throws DeviceNotAvailableException { 1579 String command = "cmd user list -v"; 1580 String commandOutput = null; 1581 for (int i = 0; i < NUM_USER_INFO_ATTEMPTS; i++) { 1582 commandOutput = executeShellCommand(command); 1583 if (commandOutput == null || commandOutput.trim().isEmpty()) { 1584 CLog.d("Command `%s` executed with no output. (attempt %d)", command, i); 1585 // Throw exception if the last attempt failed too. 1586 if (i == NUM_USER_INFO_ATTEMPTS - 1) { 1587 throw new DeviceRuntimeException( 1588 String.format( 1589 "Command `%s` executed with no output. Attempts made = %d.", 1590 command, NUM_USER_INFO_ATTEMPTS), 1591 DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE); 1592 } 1593 } else { 1594 break; 1595 } 1596 // wait before retrying 1597 RunUtil.getDefault().sleep(1000); 1598 } 1599 // Extract the id of all existing users. 1600 List<String> lines = 1601 Arrays.stream(commandOutput.split("\\r?\\n")) 1602 .filter(line -> line != null && line.trim().length() != 0) 1603 .collect(Collectors.toList()); 1604 1605 if (!lines.get(0).contains("users:")) { 1606 if (commandOutput.contains("cmd: Can't find service: package")) { 1607 throw new DeviceNotAvailableException( 1608 String.format( 1609 "'%s' in not a valid output for 'user list -v'", commandOutput), 1610 getSerialNumber()); 1611 } 1612 throw new DeviceRuntimeException( 1613 String.format("'%s' in not a valid output for 'user list -v'", commandOutput), 1614 DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE); 1615 } 1616 ArrayList<String[]> users = new ArrayList<String[]>(lines.size() - 1); 1617 1618 String pattern = ".id=(.*), name=(.*), type=(.*), flags=(.*)"; 1619 Pattern r = Pattern.compile(pattern); 1620 for (int i = 1; i < lines.size(); i++) { 1621 // Individual user is printed out like this: 1622 // idx: id=$id, name=$name, type=$type, flags=AAA|BBB|XXX (running) (current) (visible) 1623 Matcher m = r.matcher(lines.get(i)); 1624 if (m.find()) { 1625 String id = m.group(1); 1626 String name = m.group(2); 1627 // example: full.SYSTEM, profile.XXX 1628 String type = m.group(3); 1629 // AAA|BBB|XXX (running) (current) (visible) 1630 String flags_and_status = m.group(4); 1631 1632 String flags = ""; 1633 String status = ""; 1634 if (flags_and_status != null) { 1635 // Split flags and convert to hex 1636 // output: [AAA, BBB, XXX (running) (current) (visible)] 1637 String[] flagsArr = flags_and_status.split("\\|"); 1638 // XXX (running) (current) (visible) 1639 String last_flag_and_status = 1640 flagsArr.length > 0 ? flagsArr[flagsArr.length - 1] : ""; 1641 String[] arr = last_flag_and_status.split("\\s", 2); 1642 if (arr.length > 0) { 1643 flags = Integer.toHexString(convertToHex(flagsArr, arr[0])); 1644 } 1645 if (arr.length > 1) { 1646 status = arr[1] != null ? arr[1] : ""; 1647 } 1648 } 1649 // Maintain same sequence as per-Q output, add type at the end. 1650 users.add(new String[] {"", id, name, flags, status, type}); 1651 } 1652 } 1653 return users; 1654 } 1655 tokenizeListUsersPreT()1656 private ArrayList<String[]> tokenizeListUsersPreT() throws DeviceNotAvailableException { 1657 String command = "pm list users"; 1658 String commandOutput = null; 1659 for (int i = 0; i < NUM_USER_INFO_ATTEMPTS; i++) { 1660 commandOutput = executeShellCommand(command); 1661 if (commandOutput == null || commandOutput.trim().isEmpty()) { 1662 CLog.d("Command `%s` executed with no output. (attempt %d)", command, i); 1663 // Throw exception if the last attempt failed too. 1664 if (i == NUM_USER_INFO_ATTEMPTS - 1) { 1665 throw new DeviceRuntimeException( 1666 String.format( 1667 "Command `%s` executed with no output. Attempts made = %d.", 1668 command, NUM_USER_INFO_ATTEMPTS), 1669 DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE); 1670 } 1671 } else { 1672 break; 1673 } 1674 // wait before retrying 1675 RunUtil.getDefault().sleep(1000); 1676 } 1677 // Extract the id of all existing users. 1678 String[] lines = commandOutput.split("\\r?\\n"); 1679 if (!lines[0].equals("Users:")) { 1680 throw new DeviceRuntimeException( 1681 String.format("'%s' in not a valid output for 'pm list users'", commandOutput), 1682 DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE); 1683 } 1684 ArrayList<String[]> users = new ArrayList<String[]>(lines.length - 1); 1685 for (int i = 1; i < lines.length; i++) { 1686 // Individual user is printed out like this: 1687 // \tUserInfo{$id$:$name$:$Integer.toHexString(flags)$} [running] 1688 String[] tokens = lines[i].split("\\{|\\}|:"); 1689 if (tokens.length != 4 && tokens.length != 5) { 1690 throw new DeviceRuntimeException( 1691 String.format( 1692 "device output: '%s' \nline: '%s' was not in the expected " 1693 + "format for user info.", 1694 commandOutput, lines[i]), 1695 DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE); 1696 } 1697 users.add(tokens); 1698 } 1699 return users; 1700 } 1701 convertToHex(String[] arr, String str)1702 private int convertToHex(String[] arr, String str) { 1703 int res = 0; 1704 1705 for (int i = 0; i < arr.length - 1; i++) { 1706 res |= getHexaDecimalValue(arr[i]); 1707 } 1708 res |= getHexaDecimalValue(str); 1709 1710 return res; 1711 } 1712 getHexaDecimalValue(String flag)1713 private int getHexaDecimalValue(String flag) { 1714 switch (flag) { 1715 case "PRIMARY": 1716 return 0x00000001; 1717 case "ADMIN": 1718 return 0x00000002; 1719 case "GUEST": 1720 return 0x00000004; 1721 case "RESTRICTED": 1722 return 0x00000008; 1723 case "INITIALIZED": 1724 return 0x00000010; 1725 case "MANAGED_PROFILE": 1726 return 0x00000020; 1727 case "DISABLED": 1728 return 0x00000040; 1729 case "QUIET_MODE": 1730 return 0x00000080; 1731 case "EPHEMERAL": 1732 return 0x00000100; 1733 case "DEMO": 1734 return 0x00000200; 1735 case "FULL": 1736 return 0x00000400; 1737 case "SYSTEM": 1738 return 0x00000800; 1739 case "PROFILE": 1740 return 0x00001000; 1741 case "EPHEMERAL_ON_CREATE": 1742 return 0x00002000; 1743 case "MAIN": 1744 return 0x00004000; 1745 case "FOR_TESTING": 1746 return 0x00008000; 1747 default: 1748 CLog.e("Flag %s not found.", flag); 1749 return 0; 1750 } 1751 } 1752 1753 /** 1754 * {@inheritDoc} 1755 */ 1756 @Override getMaxNumberOfUsersSupported()1757 public int getMaxNumberOfUsersSupported() throws DeviceNotAvailableException { 1758 String command = "pm get-max-users"; 1759 String commandOutput = executeShellCommand(command); 1760 try { 1761 return Integer.parseInt(commandOutput.substring(commandOutput.lastIndexOf(" ")).trim()); 1762 } catch (NumberFormatException e) { 1763 CLog.e("Failed to parse result: %s", commandOutput); 1764 } 1765 return 0; 1766 } 1767 1768 /** {@inheritDoc} */ 1769 @Override getMaxNumberOfRunningUsersSupported()1770 public int getMaxNumberOfRunningUsersSupported() throws DeviceNotAvailableException { 1771 checkApiLevelAgainstNextRelease("get-max-running-users", 28); 1772 String command = "pm get-max-running-users"; 1773 String commandOutput = executeShellCommand(command); 1774 try { 1775 return Integer.parseInt(commandOutput.substring(commandOutput.lastIndexOf(" ")).trim()); 1776 } catch (NumberFormatException e) { 1777 CLog.e("Failed to parse result: %s", commandOutput); 1778 } 1779 return 0; 1780 } 1781 1782 /** 1783 * {@inheritDoc} 1784 */ 1785 @Override isMultiUserSupported()1786 public boolean isMultiUserSupported() throws DeviceNotAvailableException { 1787 checkApiLevelAgainstNextRelease("get-max-running-users", 28); 1788 final int apiLevel = getApiLevel(); 1789 if (apiLevel > 33) { 1790 String command = "pm supports-multiple-users"; 1791 String commandOutput = executeShellCommand(command).trim(); 1792 try { 1793 String parsedOutput = 1794 commandOutput.substring(commandOutput.lastIndexOf(" ")).trim(); 1795 Boolean retValue = Boolean.valueOf(parsedOutput); 1796 return retValue; 1797 } catch (NumberFormatException e) { 1798 CLog.e("Failed to parse result: %s", commandOutput); 1799 return false; 1800 } 1801 } 1802 return getMaxNumberOfUsersSupported() > 1; 1803 } 1804 1805 @Override isHeadlessSystemUserMode()1806 public boolean isHeadlessSystemUserMode() throws DeviceNotAvailableException { 1807 checkApiLevelAgainst("isHeadlessSystemUserMode", 29); 1808 return checkApiLevelAgainstNextRelease(34) 1809 ? executeShellV2CommandThatReturnsBooleanSafe( 1810 "cmd user is-headless-system-user-mode") 1811 : getBooleanProperty("ro.fw.mu.headless_system_user", false); 1812 } 1813 1814 /** {@inheritDoc} */ 1815 @Override canSwitchToHeadlessSystemUser()1816 public boolean canSwitchToHeadlessSystemUser() throws DeviceNotAvailableException { 1817 checkApiLevelAgainst("canSwitchToHeadlessSystemUser", 34); 1818 return executeShellV2CommandThatReturnsBooleanSafe( 1819 "cmd user can-switch-to-headless-system-user"); 1820 } 1821 1822 /** {@inheritDoc} */ 1823 @Override isMainUserPermanentAdmin()1824 public boolean isMainUserPermanentAdmin() throws DeviceNotAvailableException { 1825 checkApiLevelAgainst("isMainUserPermanentAdmin", 34); 1826 return executeShellV2CommandThatReturnsBooleanSafe("cmd user is-main-user-permanent-admin"); 1827 } 1828 1829 /** 1830 * {@inheritDoc} 1831 */ 1832 @Override createUser(String name)1833 public int createUser(String name) throws DeviceNotAvailableException, IllegalStateException { 1834 return createUser(name, false, false); 1835 } 1836 1837 /** 1838 * {@inheritDoc} 1839 */ 1840 @Override createUser(String name, boolean guest, boolean ephemeral)1841 public int createUser(String name, boolean guest, boolean ephemeral) 1842 throws DeviceNotAvailableException, IllegalStateException { 1843 return createUser(name, guest, ephemeral, /* forTesting= */ false); 1844 } 1845 1846 /** {@inheritDoc} */ 1847 @Override createUser(String name, boolean guest, boolean ephemeral, boolean forTesting)1848 public int createUser(String name, boolean guest, boolean ephemeral, boolean forTesting) 1849 throws DeviceNotAvailableException, IllegalStateException { 1850 String command = 1851 "pm create-user " 1852 + (guest ? "--guest " : "") 1853 + (ephemeral ? "--ephemeral " : "") 1854 + (forTesting && getApiLevel() >= 34 ? "--for-testing " : "") 1855 + name; 1856 final String output = executeShellCommand(command); 1857 if (output.startsWith("Success")) { 1858 try { 1859 resetContentProviderSetup(); 1860 return Integer.parseInt(output.substring(output.lastIndexOf(" ")).trim()); 1861 } catch (NumberFormatException e) { 1862 CLog.e("Failed to parse result: %s", output); 1863 } 1864 } 1865 throw new IllegalStateException(String.format("Failed to create user: %s", output)); 1866 } 1867 1868 /** {@inheritDoc} */ 1869 @Override createUserNoThrow(String name)1870 public int createUserNoThrow(String name) throws DeviceNotAvailableException { 1871 try { 1872 return createUser(name); 1873 } catch (IllegalStateException e) { 1874 CLog.e("Error creating user: " + e.toString()); 1875 return -1; 1876 } 1877 } 1878 1879 /** 1880 * {@inheritDoc} 1881 */ 1882 @Override removeUser(int userId)1883 public boolean removeUser(int userId) throws DeviceNotAvailableException { 1884 final String output = executeShellCommand(String.format("pm remove-user %s", userId)); 1885 if (output.startsWith("Error")) { 1886 CLog.e("Failed to remove user %d on device %s: %s", userId, getSerialNumber(), output); 1887 return false; 1888 } 1889 return true; 1890 } 1891 1892 /** 1893 * {@inheritDoc} 1894 */ 1895 @Override startUser(int userId)1896 public boolean startUser(int userId) throws DeviceNotAvailableException { 1897 return startUser(userId, false); 1898 } 1899 1900 /** {@inheritDoc} */ 1901 @Override startUser(int userId, boolean waitFlag)1902 public boolean startUser(int userId, boolean waitFlag) throws DeviceNotAvailableException { 1903 if (waitFlag) { 1904 checkApiLevelAgainstNextRelease("start-user -w", 29); 1905 } 1906 String cmd = "am start-user " + (waitFlag ? "-w " : "") + userId; 1907 1908 CLog.d("Starting user with command: %s", cmd); 1909 final String output = executeShellCommand(cmd); 1910 if (output.startsWith("Error")) { 1911 CLog.e("Failed to start user: %s", output); 1912 return false; 1913 } 1914 if (waitFlag) { 1915 String state = executeShellCommand("am get-started-user-state " + userId); 1916 if (!state.contains("RUNNING_UNLOCKED")) { 1917 CLog.w("User %s is not RUNNING_UNLOCKED after start-user -w. (%s).", userId, state); 1918 return false; 1919 } 1920 } 1921 return true; 1922 } 1923 1924 @Override startVisibleBackgroundUser(int userId, int displayId, boolean waitFlag)1925 public boolean startVisibleBackgroundUser(int userId, int displayId, boolean waitFlag) 1926 throws DeviceNotAvailableException { 1927 checkApiLevelAgainstNextRelease("startVisibleBackgroundUser", 34); 1928 1929 String cmd = 1930 String.format( 1931 "am start-user%s --display %d %d", 1932 (waitFlag ? " -w" : ""), displayId, userId); 1933 CommandResult res = executeShellV2Command(cmd); 1934 if (!CommandStatus.SUCCESS.equals(res.getStatus())) { 1935 throw new DeviceRuntimeException( 1936 "Command '" + cmd + "' failed: " + res, 1937 DeviceErrorIdentifier.SHELL_COMMAND_ERROR); 1938 } 1939 return res.getStdout().trim().startsWith("Success"); 1940 } 1941 1942 /** 1943 * {@inheritDoc} 1944 */ 1945 @Override stopUser(int userId)1946 public boolean stopUser(int userId) throws DeviceNotAvailableException { 1947 // No error or status code is returned. 1948 return stopUser(userId, false, false); 1949 } 1950 1951 /** 1952 * {@inheritDoc} 1953 */ 1954 @Override stopUser(int userId, boolean waitFlag, boolean forceFlag)1955 public boolean stopUser(int userId, boolean waitFlag, boolean forceFlag) 1956 throws DeviceNotAvailableException { 1957 final int apiLevel = getApiLevel(); 1958 if (waitFlag && apiLevel < 23) { 1959 throw new IllegalArgumentException("stop-user -w requires API level >= 23"); 1960 } 1961 if (forceFlag && apiLevel < 24) { 1962 throw new IllegalArgumentException("stop-user -f requires API level >= 24"); 1963 } 1964 StringBuilder cmd = new StringBuilder("am stop-user "); 1965 if (waitFlag) { 1966 cmd.append("-w "); 1967 } 1968 if (forceFlag) { 1969 cmd.append("-f "); 1970 } 1971 cmd.append(userId); 1972 1973 CLog.d("stopping user with command: %s", cmd.toString()); 1974 final String output = executeShellCommand(cmd.toString()); 1975 if (output.contains("Error: Can't stop system user")) { 1976 CLog.e("Cannot stop System user."); 1977 return false; 1978 } 1979 if (output.contains("Can't stop current user")) { 1980 CLog.e("Cannot stop current user."); 1981 return false; 1982 } 1983 if (isUserRunning(userId)) { 1984 CLog.w("User Id: %s is still running after the stop-user command.", userId); 1985 return false; 1986 } 1987 return true; 1988 } 1989 1990 @Override isVisibleBackgroundUsersSupported()1991 public boolean isVisibleBackgroundUsersSupported() throws DeviceNotAvailableException { 1992 checkApiLevelAgainstNextRelease("isHeadlessSystemUserMode", 34); 1993 1994 return executeShellV2CommandThatReturnsBoolean( 1995 "cmd user is-visible-background-users-supported"); 1996 } 1997 1998 @Override isVisibleBackgroundUsersOnDefaultDisplaySupported()1999 public boolean isVisibleBackgroundUsersOnDefaultDisplaySupported() 2000 throws DeviceNotAvailableException { 2001 checkApiLevelAgainstNextRelease("isVisibleBackgroundUsersOnDefaultDisplaySupported", 34); 2002 2003 return executeShellV2CommandThatReturnsBoolean( 2004 "cmd user is-visible-background-users-on-default-display-supported"); 2005 } 2006 2007 /** 2008 * {@inheritDoc} 2009 */ 2010 @Override getPrimaryUserId()2011 public Integer getPrimaryUserId() throws DeviceNotAvailableException { 2012 return getUserIdByFlag(FLAG_PRIMARY); 2013 } 2014 2015 /** {@inheritDoc} */ 2016 @Override getMainUserId()2017 public Integer getMainUserId() throws DeviceNotAvailableException { 2018 return getUserIdByFlag(FLAG_MAIN); 2019 } 2020 getUserIdByFlag(int requiredFlag)2021 private Integer getUserIdByFlag(int requiredFlag) throws DeviceNotAvailableException { 2022 ArrayList<String[]> users = tokenizeListUsers(); 2023 for (String[] user : users) { 2024 int flag = Integer.parseInt(user[3], 16); 2025 if ((flag & requiredFlag) != 0) { 2026 return Integer.parseInt(user[1]); 2027 } 2028 } 2029 return null; 2030 } 2031 2032 /** {@inheritDoc} */ 2033 @Override getCurrentUser()2034 public int getCurrentUser() throws DeviceNotAvailableException { 2035 checkApiLevelAgainstNextRelease("get-current-user", API_LEVEL_GET_CURRENT_USER); 2036 final String output = executeShellCommand("am get-current-user"); 2037 try { 2038 int userId = Integer.parseInt(output.trim()); 2039 if (userId >= 0) { 2040 return userId; 2041 } 2042 CLog.e("Invalid user id '%s' was returned for get-current-user", userId); 2043 } catch (NumberFormatException e) { 2044 CLog.e("Invalid string was returned for get-current-user: %s.", output); 2045 } 2046 return INVALID_USER_ID; 2047 } 2048 2049 @Override isUserVisible(int userId)2050 public boolean isUserVisible(int userId) throws DeviceNotAvailableException { 2051 checkApiLevelAgainstNextRelease("isUserVisible", 34); 2052 2053 return executeShellV2CommandThatReturnsBoolean("cmd user is-user-visible %d", userId); 2054 } 2055 2056 @Override isUserVisibleOnDisplay(int userId, int displayId)2057 public boolean isUserVisibleOnDisplay(int userId, int displayId) 2058 throws DeviceNotAvailableException { 2059 checkApiLevelAgainstNextRelease("isUserVisibleOnDisplay", 34); 2060 2061 return executeShellV2CommandThatReturnsBoolean( 2062 "cmd user is-user-visible --display %d %d", displayId, userId); 2063 } 2064 findUserInfo(String pmListUsersOutput)2065 private Matcher findUserInfo(String pmListUsersOutput) { 2066 Pattern pattern = Pattern.compile(USER_PATTERN); 2067 Matcher matcher = pattern.matcher(pmListUsersOutput); 2068 return matcher; 2069 } 2070 2071 /** 2072 * {@inheritDoc} 2073 */ 2074 @Override getUserFlags(int userId)2075 public int getUserFlags(int userId) throws DeviceNotAvailableException { 2076 checkApiLevelAgainst("getUserFlags", 22); 2077 final String commandOutput = executeShellCommand("pm list users"); 2078 Matcher matcher = findUserInfo(commandOutput); 2079 while(matcher.find()) { 2080 if (Integer.parseInt(matcher.group(2)) == userId) { 2081 return Integer.parseInt(matcher.group(6), 16); 2082 } 2083 } 2084 CLog.w("Could not find any flags for userId: %d in output: %s", userId, commandOutput); 2085 return INVALID_USER_ID; 2086 } 2087 2088 /** {@inheritDoc} */ 2089 @Override isUserSecondary(int userId)2090 public boolean isUserSecondary(int userId) throws DeviceNotAvailableException { 2091 if (userId == UserInfo.USER_SYSTEM) { 2092 return false; 2093 } 2094 int flags = getUserFlags(userId); 2095 if (flags == INVALID_USER_ID) { 2096 return false; 2097 } 2098 return (flags & UserInfo.FLAGS_NOT_SECONDARY) == 0; 2099 } 2100 2101 /** 2102 * {@inheritDoc} 2103 */ 2104 @Override isUserRunning(int userId)2105 public boolean isUserRunning(int userId) throws DeviceNotAvailableException { 2106 checkApiLevelAgainst("isUserIdRunning", 22); 2107 final String commandOutput = executeShellCommand("pm list users"); 2108 Matcher matcher = findUserInfo(commandOutput); 2109 while(matcher.find()) { 2110 if (Integer.parseInt(matcher.group(2)) == userId) { 2111 if (matcher.group(7).contains("running")) { 2112 return true; 2113 } 2114 } 2115 } 2116 return false; 2117 } 2118 2119 /** 2120 * {@inheritDoc} 2121 */ 2122 @Override getUserSerialNumber(int userId)2123 public int getUserSerialNumber(int userId) throws DeviceNotAvailableException { 2124 checkApiLevelAgainst("getUserSerialNumber", 22); 2125 final String commandOutput = executeShellCommand("dumpsys user"); 2126 // example: UserInfo{0:Test:13} serialNo=0 2127 String userSerialPatter = "(.*\\{)(\\d+)(.*\\})(.*=)(\\d+)"; 2128 Pattern pattern = Pattern.compile(userSerialPatter); 2129 Matcher matcher = pattern.matcher(commandOutput); 2130 while(matcher.find()) { 2131 if (Integer.parseInt(matcher.group(2)) == userId) { 2132 return Integer.parseInt(matcher.group(5)); 2133 } 2134 } 2135 CLog.w("Could not find user serial number for userId: %d, in output: %s", 2136 userId, commandOutput); 2137 return INVALID_USER_ID; 2138 } 2139 2140 /** 2141 * {@inheritDoc} 2142 */ 2143 @Override switchUser(int userId)2144 public boolean switchUser(int userId) throws DeviceNotAvailableException { 2145 return switchUser(userId, AM_COMMAND_TIMEOUT); 2146 } 2147 2148 /** 2149 * {@inheritDoc} 2150 */ 2151 @Override switchUser(int userId, long timeout)2152 public boolean switchUser(int userId, long timeout) throws DeviceNotAvailableException { 2153 checkApiLevelAgainstNextRelease("switchUser", API_LEVEL_GET_CURRENT_USER); 2154 if (userId == getCurrentUser()) { 2155 CLog.w("Already running as user id: %s. Nothing to be done.", userId); 2156 return true; 2157 } 2158 2159 String switchCommand = 2160 checkApiLevelAgainstNextRelease(30) 2161 ? String.format("am switch-user -w %d", userId) 2162 : String.format("am switch-user %d", userId); 2163 2164 resetContentProviderSetup(); 2165 long initialTime = getHostCurrentTime(); 2166 String output = executeShellCommand(switchCommand); 2167 boolean success = userId == getCurrentUser(); 2168 2169 while (!success && (getHostCurrentTime() - initialTime <= timeout)) { 2170 // retry 2171 RunUtil.getDefault().sleep(getCheckNewUserSleep()); 2172 output = executeShellCommand(String.format(switchCommand)); 2173 success = userId == getCurrentUser(); 2174 } 2175 2176 CLog.d("switchUser took %d ms", getHostCurrentTime() - initialTime); 2177 if (success) { 2178 prePostBootSetup(); 2179 return true; 2180 } else { 2181 CLog.e("User did not switch in the given %d timeout: %s", timeout, output); 2182 return false; 2183 } 2184 } 2185 2186 /** 2187 * Exposed for testing. 2188 */ getCheckNewUserSleep()2189 protected long getCheckNewUserSleep() { 2190 return CHECK_NEW_USER; 2191 } 2192 2193 /** 2194 * Exposed for testing 2195 */ getHostCurrentTime()2196 protected long getHostCurrentTime() { 2197 return System.currentTimeMillis(); 2198 } 2199 2200 /** 2201 * {@inheritDoc} 2202 */ 2203 @Override hasFeature(String feature)2204 public boolean hasFeature(String feature) throws DeviceNotAvailableException { 2205 // Add support for directly checking a feature and match the pm output. 2206 if (!feature.startsWith("feature:")) { 2207 feature = "feature:" + feature; 2208 } 2209 final String versionedFeature = feature + "="; 2210 CommandResult commandResult = executeShellV2Command("pm list features"); 2211 if (!CommandStatus.SUCCESS.equals(commandResult.getStatus())) { 2212 throw new DeviceRuntimeException( 2213 String.format( 2214 "Failed to list features, command returned: stdout: %s, stderr: %s", 2215 commandResult.getStdout(), commandResult.getStderr()), 2216 DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE); 2217 } 2218 String commandOutput = commandResult.getStdout(); 2219 for (String line: commandOutput.split("\\s+")) { 2220 // Each line in the output of the command has the format 2221 // "feature:{FEATURE_VALUE}[={FEATURE_VERSION}]". 2222 if (line.equals(feature)) { 2223 return true; 2224 } 2225 if (line.startsWith(versionedFeature)) { 2226 return true; 2227 } 2228 } 2229 CLog.w("Feature: %s is not available on %s", feature, getSerialNumber()); 2230 return false; 2231 } 2232 2233 /** 2234 * {@inheritDoc} 2235 */ 2236 @Override getSetting(String namespace, String key)2237 public String getSetting(String namespace, String key) throws DeviceNotAvailableException { 2238 return getSettingInternal("", namespace.trim(), key.trim()); 2239 } 2240 2241 /** 2242 * {@inheritDoc} 2243 */ 2244 @Override getSetting(int userId, String namespace, String key)2245 public String getSetting(int userId, String namespace, String key) 2246 throws DeviceNotAvailableException { 2247 return getSettingInternal(String.format("--user %d", userId), namespace.trim(), key.trim()); 2248 } 2249 2250 /** 2251 * Internal Helper to get setting with or without a userId provided. 2252 */ getSettingInternal(String userFlag, String namespace, String key)2253 private String getSettingInternal(String userFlag, String namespace, String key) 2254 throws DeviceNotAvailableException { 2255 namespace = namespace.toLowerCase(); 2256 if (Arrays.asList(SETTINGS_NAMESPACE).contains(namespace)) { 2257 String cmd = String.format("settings %s get %s %s", userFlag, namespace, key); 2258 String output = executeShellCommand(cmd); 2259 if ("null".equals(output)) { 2260 CLog.w("settings returned null for command: %s. " 2261 + "please check if the namespace:key exists", cmd); 2262 return null; 2263 } 2264 return output.trim(); 2265 } 2266 CLog.e("Namespace requested: '%s' is not part of {system, secure, global}", namespace); 2267 return null; 2268 } 2269 2270 /** {@inheritDoc} */ 2271 @Override getAllSettings(String namespace)2272 public Map<String, String> getAllSettings(String namespace) throws DeviceNotAvailableException { 2273 return getAllSettingsInternal(namespace.trim()); 2274 } 2275 2276 /** Internal helper to get all settings */ getAllSettingsInternal(String namespace)2277 private Map<String, String> getAllSettingsInternal(String namespace) 2278 throws DeviceNotAvailableException { 2279 namespace = namespace.toLowerCase(); 2280 if (Arrays.asList(SETTINGS_NAMESPACE).contains(namespace)) { 2281 Map<String, String> map = new HashMap<>(); 2282 String cmd = String.format("settings list %s", namespace); 2283 String output = executeShellCommand(cmd); 2284 for (String line : output.split("\\n")) { 2285 // Setting's value could be empty 2286 String[] pair = line.trim().split("=", -1); 2287 if (pair.length > 1) { 2288 map.putIfAbsent(pair[0], pair[1]); 2289 } else { 2290 CLog.e("Unable to get setting from string: %s", line); 2291 } 2292 } 2293 return map; 2294 } 2295 CLog.e("Namespace requested: '%s' is not part of {system, secure, global}", namespace); 2296 return null; 2297 } 2298 2299 /** 2300 * {@inheritDoc} 2301 */ 2302 @Override setSetting(String namespace, String key, String value)2303 public void setSetting(String namespace, String key, String value) 2304 throws DeviceNotAvailableException { 2305 setSettingInternal("", namespace.trim(), key.trim(), value.trim()); 2306 } 2307 2308 /** 2309 * {@inheritDoc} 2310 */ 2311 @Override setSetting(int userId, String namespace, String key, String value)2312 public void setSetting(int userId, String namespace, String key, String value) 2313 throws DeviceNotAvailableException { 2314 setSettingInternal(String.format("--user %d", userId), namespace.trim(), key.trim(), 2315 value.trim()); 2316 } 2317 2318 /** 2319 * Internal helper to set a setting with or without a userId provided. 2320 */ setSettingInternal(String userFlag, String namespace, String key, String value)2321 private void setSettingInternal(String userFlag, String namespace, String key, String value) 2322 throws DeviceNotAvailableException { 2323 checkApiLevelAgainst("Changing settings", 22); 2324 if (Arrays.asList(SETTINGS_NAMESPACE).contains(namespace.toLowerCase())) { 2325 executeShellCommand(String.format("settings %s put %s %s %s", 2326 userFlag, namespace, key, value)); 2327 } else { 2328 throw new IllegalArgumentException("Namespace must be one of system, secure, global." 2329 + " You provided: " + namespace); 2330 } 2331 } 2332 2333 /** 2334 * {@inheritDoc} 2335 */ 2336 @Override getAndroidId(int userId)2337 public String getAndroidId(int userId) throws DeviceNotAvailableException { 2338 if (isAdbRoot()) { 2339 String cmd = String.format( 2340 "sqlite3 /data/user/%d/*/databases/gservices.db " 2341 + "'select value from main where name = \"android_id\"'", userId); 2342 String output = executeShellCommand(cmd).trim(); 2343 if (!output.contains("unable to open database")) { 2344 return output; 2345 } 2346 CLog.w("Couldn't find android-id, output: %s", output); 2347 } else { 2348 CLog.w("adb root is required."); 2349 } 2350 return null; 2351 } 2352 2353 /** 2354 * {@inheritDoc} 2355 */ 2356 @Override getAndroidIds()2357 public Map<Integer, String> getAndroidIds() throws DeviceNotAvailableException { 2358 ArrayList<Integer> userIds = listUsers(); 2359 if (userIds == null) { 2360 return null; 2361 } 2362 Map<Integer, String> androidIds = new HashMap<Integer, String>(); 2363 for (Integer id : userIds) { 2364 String androidId = getAndroidId(id); 2365 androidIds.put(id, androidId); 2366 } 2367 return androidIds; 2368 } 2369 2370 /** 2371 * {@inheritDoc} 2372 */ 2373 @Override createWifiHelper()2374 IWifiHelper createWifiHelper() throws DeviceNotAvailableException { 2375 return createWifiHelper(false); 2376 } 2377 2378 @Override createWifiHelper(boolean useV2)2379 IWifiHelper createWifiHelper(boolean useV2) throws DeviceNotAvailableException { 2380 if (useV2) { 2381 CLog.d("Using WifiHelper V2. WifiUtil apk installation skipped."); 2382 InvocationMetricLogger.addInvocationMetrics(InvocationMetricKey.WIFI_HELPER_V2, "true"); 2383 return createWifiHelper(useV2, false); 2384 } else { 2385 return createWifiHelper(useV2, true); 2386 } 2387 } 2388 2389 /** 2390 * Alternative to {@link #createWifiHelper()} where we can choose whether to do the wifi helper 2391 * setup or not. 2392 */ 2393 @VisibleForTesting createWifiHelper(boolean useV2, boolean doSetup)2394 IWifiHelper createWifiHelper(boolean useV2, boolean doSetup) 2395 throws DeviceNotAvailableException { 2396 if (doSetup) { 2397 mWasWifiHelperInstalled = true; 2398 // Ensure device is ready before attempting wifi setup 2399 waitForDeviceAvailable(); 2400 } 2401 return new WifiHelper(this, mOptions.getWifiUtilAPKPath(), doSetup, useV2); 2402 } 2403 2404 /** {@inheritDoc} */ 2405 @Override postInvocationTearDown(Throwable exception)2406 public void postInvocationTearDown(Throwable exception) { 2407 super.postInvocationTearDown(exception); 2408 // If wifi was installed and it's a real device, attempt to clean it. 2409 if (mWasWifiHelperInstalled) { 2410 mWasWifiHelperInstalled = false; 2411 if (getIDevice() instanceof StubDevice) { 2412 return; 2413 } 2414 if (!TestDeviceState.ONLINE.equals(getDeviceState())) { 2415 return; 2416 } 2417 if (exception instanceof DeviceNotAvailableException) { 2418 CLog.e("Skip WifiHelper teardown due to DeviceNotAvailableException."); 2419 return; 2420 } 2421 try { 2422 // Uninstall the wifi utility if it was installed. 2423 IWifiHelper wifi = createWifiHelper(false, false); 2424 wifi.cleanUp(); 2425 } catch (DeviceNotAvailableException e) { 2426 CLog.e("Device became unavailable while uninstalling wifi util."); 2427 CLog.e(e); 2428 } 2429 } 2430 } 2431 2432 /** {@inheritDoc} */ 2433 @Override setDeviceOwner(String componentName, int userId)2434 public boolean setDeviceOwner(String componentName, int userId) 2435 throws DeviceNotAvailableException { 2436 final String command = "dpm set-device-owner --user " + userId + " '" + componentName + "'"; 2437 final String commandOutput = executeShellCommand(command); 2438 return commandOutput.startsWith("Success:"); 2439 } 2440 2441 /** {@inheritDoc} */ 2442 @Override removeAdmin(String componentName, int userId)2443 public boolean removeAdmin(String componentName, int userId) 2444 throws DeviceNotAvailableException { 2445 final String command = 2446 "dpm remove-active-admin --user " + userId + " '" + componentName + "'"; 2447 final String commandOutput = executeShellCommand(command); 2448 return commandOutput.startsWith("Success:"); 2449 } 2450 2451 /** {@inheritDoc} */ 2452 @Override removeOwners()2453 public void removeOwners() throws DeviceNotAvailableException { 2454 String command = "dumpsys device_policy"; 2455 String commandOutput = executeShellCommand(command); 2456 String[] lines = commandOutput.split("\\r?\\n"); 2457 for (int i = 0; i < lines.length; ++i) { 2458 String line = lines[i].trim(); 2459 if (line.contains("Profile Owner")) { 2460 // Line is "Profile owner (User <id>): 2461 String[] tokens = line.split("\\(|\\)| "); 2462 int userId = Integer.parseInt(tokens[4]); 2463 2464 i = moveToNextIndexMatchingRegex(".*admin=.*", lines, i); 2465 line = lines[i].trim(); 2466 // Line is admin=ComponentInfo{<component>} 2467 tokens = line.split("\\{|\\}"); 2468 String componentName = tokens[1]; 2469 CLog.d("Cleaning up profile owner " + userId + " " + componentName); 2470 removeAdmin(componentName, userId); 2471 } else if (line.contains("Device Owner:")) { 2472 i = moveToNextIndexMatchingRegex(".*admin=.*", lines, i); 2473 line = lines[i].trim(); 2474 // Line is admin=ComponentInfo{<component>} 2475 String[] tokens = line.split("\\{|\\}"); 2476 String componentName = tokens[1]; 2477 2478 // Skip to user id line. 2479 i = moveToNextIndexMatchingRegex(".*User ID:.*", lines, i); 2480 line = lines[i].trim(); 2481 // Line is User ID: <N> 2482 tokens = line.split(":"); 2483 int userId = Integer.parseInt(tokens[1].trim()); 2484 CLog.d("Cleaning up device owner " + userId + " " + componentName); 2485 removeAdmin(componentName, userId); 2486 } 2487 } 2488 } 2489 2490 /** 2491 * Search forward from the current index to find a string matching the given regex. 2492 * 2493 * @param regex The regex to match each line against. 2494 * @param lines An array of strings to be searched. 2495 * @param currentIndex the index to start searching from. 2496 * @return The index of a string beginning with the regex. 2497 * @throws IllegalStateException if the line cannot be found. 2498 */ moveToNextIndexMatchingRegex(String regex, String[] lines, int currentIndex)2499 private int moveToNextIndexMatchingRegex(String regex, String[] lines, int currentIndex) { 2500 while (currentIndex < lines.length && !lines[currentIndex].matches(regex)) { 2501 currentIndex++; 2502 } 2503 2504 if (currentIndex >= lines.length) { 2505 throw new IllegalStateException( 2506 "The output of 'dumpsys device_policy' was not as expected. Owners have not " 2507 + "been removed. This will leave the device in an unstable state and " 2508 + "will lead to further test failures."); 2509 } 2510 2511 return currentIndex; 2512 } 2513 2514 /** 2515 * Helper for Api level checking of features in the new release before we incremented the api 2516 * number. 2517 */ checkApiLevelAgainstNextRelease(String feature, int strictMinLevel)2518 private void checkApiLevelAgainstNextRelease(String feature, int strictMinLevel) 2519 throws DeviceNotAvailableException { 2520 if (checkApiLevelAgainstNextRelease(strictMinLevel)) { 2521 return; 2522 } 2523 throw new IllegalArgumentException( 2524 String.format( 2525 "%s not supported on %s. Must be API %d.", 2526 feature, getSerialNumber(), strictMinLevel)); 2527 } 2528 2529 @Override dumpHeap(String process, String devicePath)2530 public File dumpHeap(String process, String devicePath) throws DeviceNotAvailableException { 2531 if (Strings.isNullOrEmpty(devicePath) || Strings.isNullOrEmpty(process)) { 2532 throw new IllegalArgumentException("devicePath or process cannot be null or empty."); 2533 } 2534 String pid = getProcessPid(process); 2535 if (pid == null) { 2536 return null; 2537 } 2538 File dump = dumpAndPullHeap(pid, devicePath); 2539 // Clean the device. 2540 deleteFile(devicePath); 2541 return dump; 2542 } 2543 2544 /** Dump the heap file and pull it from the device. */ dumpAndPullHeap(String pid, String devicePath)2545 private File dumpAndPullHeap(String pid, String devicePath) throws DeviceNotAvailableException { 2546 executeShellCommand(String.format(DUMPHEAP_CMD, pid, devicePath)); 2547 // Allow a little bit of time for the file to populate on device side. 2548 int attempt = 0; 2549 // TODO: add an API to check device file size 2550 while (!doesFileExist(devicePath) && attempt < 3) { 2551 getRunUtil().sleep(DUMPHEAP_TIME); 2552 attempt++; 2553 } 2554 File dumpFile = pullFile(devicePath); 2555 return dumpFile; 2556 } 2557 2558 /** {@inheritDoc} */ 2559 @Override listDisplayIds()2560 public Set<Long> listDisplayIds() throws DeviceNotAvailableException { 2561 Set<Long> displays = new HashSet<>(); 2562 CommandResult res = executeShellV2Command("dumpsys SurfaceFlinger | grep 'color modes:'"); 2563 if (!CommandStatus.SUCCESS.equals(res.getStatus())) { 2564 CLog.e("Something went wrong while listing displays: %s", res.getStderr()); 2565 return displays; 2566 } 2567 String output = res.getStdout(); 2568 Pattern p = Pattern.compile(DISPLAY_ID_PATTERN); 2569 for (String line : output.split("\n")) { 2570 Matcher m = p.matcher(line); 2571 if (m.matches()) { 2572 displays.add(Long.parseLong(m.group("id"))); 2573 } 2574 } 2575 2576 // If the device is older and did not report any displays 2577 // then add the default. 2578 // Note: this assumption breaks down if the device also has multiple displays 2579 if (displays.isEmpty()) { 2580 // Zero is the default display 2581 displays.add(0L); 2582 } 2583 2584 return displays; 2585 } 2586 2587 @Override listDisplayIdsForStartingVisibleBackgroundUsers()2588 public Set<Integer> listDisplayIdsForStartingVisibleBackgroundUsers() 2589 throws DeviceNotAvailableException { 2590 checkApiLevelAgainstNextRelease("getDisplayIdsForStartingVisibleBackgroundUsers", 34); 2591 2592 String cmd = "cmd activity list-displays-for-starting-users"; 2593 CommandResult res = executeShellV2Command(cmd); 2594 if (!CommandStatus.SUCCESS.equals(res.getStatus())) { 2595 throw new DeviceRuntimeException( 2596 "Command '" + cmd + "' failed: " + res, 2597 DeviceErrorIdentifier.SHELL_COMMAND_ERROR); 2598 } 2599 String output = res.getStdout().trim(); 2600 2601 if (output.equalsIgnoreCase("none")) { 2602 return Collections.emptySet(); 2603 } 2604 2605 // TODO: reuse some helper to parse the list 2606 if (!output.startsWith("[") || !output.endsWith("]")) { 2607 throw new DeviceRuntimeException( 2608 "Invalid output for command '" + cmd + "': " + output, 2609 DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE); 2610 } 2611 String contents = output.substring(1, output.length() - 1); 2612 try { 2613 String[] ids = contents.split(","); 2614 return Arrays.asList(ids).stream() 2615 .map(id -> Integer.parseInt(id.trim())) 2616 .collect(Collectors.toSet()); 2617 } catch (Exception e) { 2618 throw new DeviceRuntimeException( 2619 "Invalid output for command '" + cmd + "': " + output, 2620 DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE); 2621 } 2622 } 2623 2624 @Override getFoldableStates()2625 public Set<DeviceFoldableState> getFoldableStates() throws DeviceNotAvailableException { 2626 if (getIDevice() instanceof StubDevice) { 2627 return new HashSet<>(); 2628 } 2629 try (CloseableTraceScope foldable = new CloseableTraceScope("getFoldableStates")) { 2630 CommandResult result = executeShellV2Command("cmd device_state print-states"); 2631 if (!CommandStatus.SUCCESS.equals(result.getStatus())) { 2632 // Can't throw an exception since it would fail on non-supported version 2633 return new HashSet<>(); 2634 } 2635 Set<DeviceFoldableState> foldableStates = new LinkedHashSet<>(); 2636 Pattern deviceStatePattern = 2637 Pattern.compile( 2638 "DeviceState\\{identifier=(\\d+), name='(\\S+)'" 2639 + "(?:, app_accessible=)?(\\S+)?" 2640 + "(?:, cancel_when_requester_not_on_top=)?(\\S+)?" 2641 + "\\}\\S*"); 2642 for (String line : result.getStdout().split("\n")) { 2643 Matcher m = deviceStatePattern.matcher(line.trim()); 2644 if (m.matches()) { 2645 // Move onto the next state if the device state is not accessible by apps 2646 if (m.groupCount() > 2 2647 && m.group(3) != null 2648 && !Boolean.parseBoolean(m.group(3))) { 2649 continue; 2650 } 2651 // Move onto the next state if the device state is canceled when the requesting 2652 // app 2653 // is not on top. 2654 if (m.groupCount() > 3 2655 && m.group(4) != null 2656 && Boolean.parseBoolean(m.group(4))) { 2657 continue; 2658 } 2659 foldableStates.add( 2660 new DeviceFoldableState(Integer.parseInt(m.group(1)), m.group(2))); 2661 } 2662 } 2663 return foldableStates; 2664 } 2665 } 2666 2667 @Override notifySnapuserd(SnapuserdWaitPhase waitPhase)2668 public void notifySnapuserd(SnapuserdWaitPhase waitPhase) { 2669 mWaitForSnapuserd = true; 2670 mSnapuserNotificationTimestamp = System.currentTimeMillis(); 2671 mWaitPhase = waitPhase; 2672 CLog.d("Notified to wait for snapuserd at %s", waitPhase); 2673 } 2674 2675 @Override waitForSnapuserd(SnapuserdWaitPhase currentPhase)2676 public void waitForSnapuserd(SnapuserdWaitPhase currentPhase) 2677 throws DeviceNotAvailableException { 2678 if (!mWaitForSnapuserd) { 2679 CLog.d("No snapuserd notification in progress for %s", currentPhase); 2680 return; 2681 } 2682 // At releasing or at the reported phase, block for snapuserd. 2683 if (!SnapuserdWaitPhase.BLOCK_BEFORE_RELEASING.equals(currentPhase) 2684 && !currentPhase.equals(mWaitPhase)) { 2685 return; 2686 } 2687 long startTime = System.currentTimeMillis(); 2688 try (CloseableTraceScope ignored = new CloseableTraceScope("wait_for_snapuserd")) { 2689 long maxTimeout = getOptions().getSnapuserdTimeout(); 2690 while (System.currentTimeMillis() - startTime < maxTimeout) { 2691 CommandResult psOutput = executeShellV2Command("ps -ef | grep snapuserd"); 2692 CLog.d("stdout: %s, stderr: %s", psOutput.getStdout(), psOutput.getStderr()); 2693 if (psOutput.getStdout().contains("snapuserd -")) { 2694 RunUtil.getDefault().sleep(2500); 2695 CLog.d("waiting for snapuserd to complete."); 2696 } else { 2697 return; 2698 } 2699 } 2700 throw new DeviceRuntimeException( 2701 String.format( 2702 "snapuserd didn't complete in %s", 2703 TimeUtil.formatElapsedTime(maxTimeout)), 2704 InfraErrorIdentifier.INCREMENTAL_FLASHING_ERROR); 2705 } finally { 2706 InvocationMetricLogger.addInvocationMetrics( 2707 InvocationMetricKey.INCREMENTAL_SNAPUSERD_WRITE_BLOCKING_TIME, 2708 System.currentTimeMillis() - startTime); 2709 InvocationMetricLogger.addInvocationMetrics( 2710 InvocationMetricKey.INCREMENTAL_SNAPUSERD_WRITE_TIME, 2711 System.currentTimeMillis() - mSnapuserNotificationTimestamp); 2712 mWaitForSnapuserd = false; 2713 mSnapuserNotificationTimestamp = 0L; 2714 mWaitPhase = null; 2715 } 2716 } 2717 2718 @Override getCurrentFoldableState()2719 public DeviceFoldableState getCurrentFoldableState() throws DeviceNotAvailableException { 2720 if (getIDevice() instanceof StubDevice) { 2721 return null; 2722 } 2723 CommandResult result = executeShellV2Command("cmd device_state state"); 2724 Pattern deviceStatePattern = 2725 Pattern.compile( 2726 "Committed state: DeviceState\\{identifier=(\\d+), name='(\\S+)'" 2727 + "(?:, app_accessible=)?(\\S+)?" 2728 + "(?:, cancel_when_requester_not_on_top=)?(\\S+)?" 2729 + "\\}\\S*"); 2730 for (String line : result.getStdout().split("\n")) { 2731 Matcher m = deviceStatePattern.matcher(line.trim()); 2732 if (m.matches()) { 2733 return new DeviceFoldableState(Integer.parseInt(m.group(1)), m.group(2)); 2734 } 2735 } 2736 return null; 2737 } 2738 2739 /** 2740 * Checks the preconditions to run a microdroid. 2741 * 2742 * @param protectedVm true if microdroid is intended to run on protected VM. 2743 * @return returns true if the preconditions are satisfied, false otherwise. 2744 */ supportsMicrodroid(boolean protectedVm)2745 public boolean supportsMicrodroid(boolean protectedVm) throws Exception { 2746 CommandResult result = executeShellV2Command("getprop ro.product.cpu.abi"); 2747 if (result.getStatus() != CommandStatus.SUCCESS) { 2748 return false; 2749 } 2750 String abi = result.getStdout().trim(); 2751 2752 if (abi.isEmpty() || (!abi.startsWith("arm64") && !abi.startsWith("x86_64"))) { 2753 CLog.d("Unsupported ABI: " + abi); 2754 return false; 2755 } 2756 2757 if (protectedVm) { 2758 // check if device supports protected virtual machines. 2759 boolean pVMSupported = 2760 getBooleanProperty("ro.boot.hypervisor.protected_vm.supported", false); 2761 if (!pVMSupported) { 2762 CLog.i("Device does not support protected virtual machines."); 2763 return false; 2764 } 2765 } else { 2766 // check if device supports non protected virtual machines. 2767 boolean nonProtectedVMSupported = 2768 getBooleanProperty("ro.boot.hypervisor.vm.supported", false); 2769 if (!nonProtectedVMSupported) { 2770 CLog.i("Device does not support non protected virtual machines."); 2771 return false; 2772 } 2773 } 2774 2775 if (!doesFileExist("/apex/com.android.virt")) { 2776 CLog.i( 2777 "com.android.virt APEX was not pre-installed. Command Failed: 'ls" 2778 + " /apex/com.android.virt/bin/crosvm'"); 2779 return false; 2780 } 2781 return true; 2782 } 2783 2784 /** 2785 * Checks the preconditions to run a microdroid. 2786 * 2787 * @return returns true if the preconditions are satisfied, false otherwise. 2788 */ supportsMicrodroid()2789 public boolean supportsMicrodroid() throws Exception { 2790 // Micrdroid can run on protected and non-protected VMs 2791 return supportsMicrodroid(false) || supportsMicrodroid(true); 2792 } 2793 2794 /** 2795 * Forwards contents of a file to log. To be used when testing microdroid, to forward console 2796 * and log outputs to the host device's log. 2797 */ forwardFileToLog(String logPath, String tag)2798 private void forwardFileToLog(String logPath, String tag) { 2799 try (CloseableTraceScope ignored = new CloseableTraceScope("forward_to_log:" + tag)) { 2800 String logwrapperCmd = 2801 "logwrapper " 2802 + "sh " 2803 + "-c " 2804 + "\"$'tail -f -n +0 " 2805 + logPath 2806 + " | sed \\'s/^/" 2807 + tag 2808 + ": /g\\''\""; // add tags in front of lines 2809 getRunUtil().allowInterrupt(true); 2810 // Manually execute the adb action to avoid any kind of recovery 2811 // since it hard to interrupt the forwarding 2812 final String[] fullCmd = buildAdbShellCommand(logwrapperCmd, false); 2813 AdbShellAction adbActionV2 = 2814 new AdbShellAction( 2815 fullCmd, 2816 null, 2817 null, 2818 null, 2819 TimeUnit.MINUTES.toMillis(MICRODROID_MAX_LIFETIME_MINUTES)); 2820 adbActionV2.run(); 2821 } catch (Exception e) { 2822 // Consume 2823 } 2824 } 2825 isVirtFeatureEnabled(String feature)2826 private boolean isVirtFeatureEnabled(String feature) throws DeviceNotAvailableException { 2827 CommandResult result = 2828 executeShellV2Command(VIRT_APEX + "bin/vm check-feature-enabled " + feature); 2829 return result.getExitCode() == 0 && result.getStdout().contains("is enabled"); 2830 } 2831 2832 /** 2833 * Starts a Microdroid TestDevice. 2834 * 2835 * @param builder A {@link MicrodroidBuilder} with required properties to start a microdroid. 2836 * @return returns a ITestDevice for the microdroid, can return null. 2837 */ startMicrodroid(MicrodroidBuilder builder)2838 private ITestDevice startMicrodroid(MicrodroidBuilder builder) 2839 throws DeviceNotAvailableException { 2840 IDeviceManager deviceManager = GlobalConfiguration.getDeviceManagerInstance(); 2841 2842 if (!mStartedMicrodroids.isEmpty()) 2843 throw new IllegalStateException( 2844 String.format( 2845 "Microdroid with cid '%s' already exists in device. Cannot create" 2846 + " another one.", 2847 mStartedMicrodroids.values().iterator().next().cid)); 2848 2849 // remove any leftover files under test root 2850 executeShellV2Command("rm -rf " + TEST_ROOT + "*"); 2851 2852 CommandResult result = executeShellV2Command("mkdir -p " + TEST_ROOT); 2853 if (result.getStatus() != CommandStatus.SUCCESS) { 2854 throw new DeviceRuntimeException( 2855 "mkdir -p " + TEST_ROOT + " has failed: " + result, 2856 DeviceErrorIdentifier.SHELL_COMMAND_ERROR); 2857 } 2858 2859 for (File localFile : builder.mBootFiles.keySet()) { 2860 String remoteFileName = builder.mBootFiles.get(localFile); 2861 pushFile(localFile, TEST_ROOT + remoteFileName); 2862 } 2863 2864 // Push the apk file to the test directory 2865 if (builder.mApkFile != null) { 2866 pushFile(builder.mApkFile, TEST_ROOT + builder.mApkFile.getName()); 2867 builder.mApkPath = TEST_ROOT + builder.mApkFile.getName(); 2868 } else if (builder.mApkPath == null) { 2869 // if both apkFile and apkPath is null, we can not start a microdroid device 2870 throw new IllegalArgumentException( 2871 "apkFile and apkPath is both null. Can not start microdroid."); 2872 } 2873 2874 // This file is not what we provide. It will be created by the vm tool. 2875 final String outApkIdsigPath = 2876 TEST_ROOT 2877 + (builder.mApkFile != null ? builder.mApkFile.getName() : "NULL") 2878 + ".idsig"; 2879 final String consolePath = TEST_ROOT + "console.txt"; 2880 final String logPath = TEST_ROOT + "log.txt"; 2881 final String debugFlag = 2882 Strings.isNullOrEmpty(builder.mDebugLevel) ? "" : "--debug " + builder.mDebugLevel; 2883 final String cpuFlag = builder.mNumCpus == null ? "" : "--cpus " + builder.mNumCpus; 2884 final String cpuAffinityFlag = 2885 Strings.isNullOrEmpty(builder.mCpuAffinity) 2886 ? "" 2887 : "--cpu-affinity " + builder.mCpuAffinity; 2888 final String cpuTopologyFlag = 2889 Strings.isNullOrEmpty(builder.mCpuTopology) 2890 ? "" 2891 : "--cpu-topology " + builder.mCpuTopology; 2892 final String gkiFlag = Strings.isNullOrEmpty(builder.mGki) ? "" : "--gki " + builder.mGki; 2893 final String hugePagesFlag = builder.mHugePages ? "--hugepages" : ""; 2894 2895 List<String> args = 2896 new ArrayList<>( 2897 Arrays.asList( 2898 deviceManager.getAdbPath(), 2899 "-s", 2900 getSerialNumber(), 2901 "shell", 2902 VIRT_APEX + "bin/vm", 2903 "run-app", 2904 "--console " + consolePath, 2905 "--log " + logPath, 2906 "--mem " + builder.mMemoryMib, 2907 debugFlag, 2908 cpuFlag, 2909 cpuAffinityFlag, 2910 cpuTopologyFlag, 2911 gkiFlag, 2912 hugePagesFlag, 2913 builder.mApkPath, 2914 outApkIdsigPath, 2915 builder.mInstanceImg, 2916 "--config-path", 2917 builder.mConfigPath)); 2918 if (isVirtFeatureEnabled("com.android.kvm.LLPVM_CHANGES")) { 2919 args.add("--instance-id-file"); 2920 args.add(builder.mInstanceIdFile); 2921 } 2922 if (builder.mProtectedVm) { 2923 args.add("--protected"); 2924 } 2925 for (String path : builder.mExtraIdsigPaths) { 2926 args.add("--extra-idsig"); 2927 args.add(path); 2928 } 2929 for (String path : builder.mAssignedDevices) { 2930 args.add("--devices"); 2931 args.add(path); 2932 } 2933 2934 // Run the VM 2935 String cid = null; 2936 Process process = null; 2937 try { 2938 PipedInputStream pipe = new PipedInputStream(); 2939 process = getRunUtil().runCmdInBackground(args, new PipedOutputStream(pipe)); 2940 cid = getCidFromVmRunOutput(new InputStreamReader(pipe)); 2941 } catch (IOException ex) { 2942 throw new DeviceRuntimeException( 2943 "Exception trying to start a VM", 2944 ex, 2945 DeviceErrorIdentifier.SHELL_COMMAND_ERROR); 2946 } finally { 2947 if (cid == null) { 2948 // Don't leak the process on failure 2949 process.destroyForcibly(); 2950 } 2951 } 2952 2953 // Redirect log.txt to logd using logwrapper 2954 ExecutorService executor = Executors.newFixedThreadPool(2); 2955 executor.execute( 2956 () -> { 2957 forwardFileToLog(consolePath, "MicrodroidConsole"); 2958 }); 2959 executor.execute( 2960 () -> { 2961 forwardFileToLog(logPath, "MicrodroidLog"); 2962 }); 2963 2964 int vmAdbPort = forwardMicrodroidAdbPort(cid); 2965 String microdroidSerial = "localhost:" + vmAdbPort; 2966 2967 DeviceSelectionOptions microSelection = new DeviceSelectionOptions(); 2968 microSelection.setSerial(microdroidSerial); 2969 microSelection.setBaseDeviceTypeRequested(BaseDeviceType.NATIVE_DEVICE); 2970 2971 NativeDevice microdroid = (NativeDevice) deviceManager.allocateDevice(microSelection); 2972 if (microdroid == null) { 2973 process.destroy(); 2974 try { 2975 process.waitFor(); 2976 executor.shutdownNow(); 2977 executor.awaitTermination(2L, TimeUnit.MINUTES); 2978 } catch (InterruptedException ex) { 2979 } 2980 throw new DeviceRuntimeException( 2981 "Unable to force allocate the microdroid device", 2982 InfraErrorIdentifier.RUNNER_ALLOCATION_ERROR); 2983 } 2984 // microdroid can be slow to become unavailable after root. (b/259208275) 2985 microdroid.getOptions().setAdbRootUnavailableTimeout(4 * 1000); 2986 builder.mTestDeviceOptions.put("enable-device-connection", "true"); 2987 builder.mTestDeviceOptions.put( 2988 TestDeviceOptions.INSTANCE_TYPE_OPTION, getOptions().getInstanceType().toString()); 2989 microdroid.setTestDeviceOptions(builder.mTestDeviceOptions); 2990 ((IManagedTestDevice) microdroid).setIDevice(new RemoteAvdIDevice(microdroidSerial)); 2991 adbConnectToMicrodroid(cid, microdroidSerial, vmAdbPort, builder.mAdbConnectTimeoutMs); 2992 microdroid.setMicrodroidProcess(process); 2993 try { 2994 // TODO: Pass the build info 2995 microdroid.initializeConnection(null, null); 2996 } catch (DeviceNotAvailableException | TargetSetupError e) { 2997 CLog.e(e); 2998 } 2999 MicrodroidTracker tracker = new MicrodroidTracker(); 3000 tracker.executor = executor; 3001 tracker.cid = cid; 3002 mStartedMicrodroids.put(process, tracker); 3003 return microdroid; 3004 } 3005 getCidFromVmRunOutput(Reader outputReader)3006 private static String getCidFromVmRunOutput(Reader outputReader) { 3007 BufferedReader stdout = new BufferedReader(outputReader); 3008 3009 StringBuilder output = new StringBuilder(); 3010 3011 // Retrieve the CID from the vm tool output 3012 String cid = null; 3013 Pattern pattern = Pattern.compile("with CID (\\d+)"); 3014 String line; 3015 try { 3016 while ((line = stdout.readLine()) != null) { 3017 output.append(line); 3018 output.append(' '); 3019 3020 Matcher matcher = pattern.matcher(line); 3021 if (matcher.find()) { 3022 cid = matcher.group(1); 3023 break; 3024 } 3025 } 3026 } catch (IOException ex) { 3027 throw new DeviceRuntimeException( 3028 "Failed to find the CID of the VM: " + output, 3029 ex, 3030 DeviceErrorIdentifier.SHELL_COMMAND_ERROR); 3031 } 3032 if (cid == null) { 3033 throw new DeviceRuntimeException( 3034 "Failed to find the CID of the VM: " + output, 3035 DeviceErrorIdentifier.SHELL_COMMAND_ERROR); 3036 } 3037 return cid; 3038 } 3039 3040 /** Find an unused port and forward microdroid's adb connection. Returns the port number. */ forwardMicrodroidAdbPort(String cid)3041 private int forwardMicrodroidAdbPort(String cid) { 3042 IDeviceManager deviceManager = GlobalConfiguration.getDeviceManagerInstance(); 3043 for (int trial = 0; trial < 10; trial++) { 3044 int vmAdbPort; 3045 try (ServerSocket serverSocket = new ServerSocket(0)) { 3046 vmAdbPort = serverSocket.getLocalPort(); 3047 } catch (IOException e) { 3048 throw new DeviceRuntimeException( 3049 "Unable to get an unused port for Microdroid.", 3050 e, 3051 DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE); 3052 } 3053 String from = "tcp:" + vmAdbPort; 3054 String to = "vsock:" + cid + ":5555"; 3055 3056 CommandResult result = 3057 getRunUtil() 3058 .runTimedCmd( 3059 10000, 3060 deviceManager.getAdbPath(), 3061 "-s", 3062 getSerialNumber(), 3063 "forward", 3064 from, 3065 to); 3066 if (result.getStatus() == CommandStatus.SUCCESS) { 3067 return vmAdbPort; 3068 } 3069 3070 if (result.getStderr().contains("Address already in use")) { 3071 // retry with other ports 3072 continue; 3073 } else { 3074 throw new DeviceRuntimeException( 3075 "Unable to forward vsock:" + cid + ":5555: " + result.getStderr(), 3076 DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE); 3077 } 3078 } 3079 throw new DeviceRuntimeException( 3080 "Unable to get an unused port for Microdroid.", 3081 DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE); 3082 } 3083 3084 /** 3085 * Establish an adb connection to microdroid by letting Android forward the connection to 3086 * microdroid. Wait until the connection is established and microdroid is booted. 3087 */ adbConnectToMicrodroid( String cid, String microdroidSerial, int vmAdbPort, long adbConnectTimeoutMs)3088 private void adbConnectToMicrodroid( 3089 String cid, String microdroidSerial, int vmAdbPort, long adbConnectTimeoutMs) { 3090 MicrodroidHelper microdroidHelper = new MicrodroidHelper(); 3091 IDeviceManager deviceManager = GlobalConfiguration.getDeviceManagerInstance(); 3092 3093 long start = System.currentTimeMillis(); 3094 long timeoutMillis = adbConnectTimeoutMs; 3095 long elapsed = 0; 3096 3097 final String serial = getSerialNumber(); 3098 final String from = "tcp:" + vmAdbPort; 3099 final String to = "vsock:" + cid + ":5555"; 3100 getRunUtil() 3101 .runTimedCmd(10000, deviceManager.getAdbPath(), "-s", serial, "forward", from, to); 3102 3103 boolean disconnected = true; 3104 while (disconnected && timeoutMillis >= 0) { 3105 elapsed = System.currentTimeMillis() - start; 3106 timeoutMillis -= elapsed; 3107 start = System.currentTimeMillis(); 3108 CommandResult result = 3109 getRunUtil() 3110 .runTimedCmd( 3111 timeoutMillis, 3112 deviceManager.getAdbPath(), 3113 "connect", 3114 microdroidSerial); 3115 if (result.getStatus() != CommandStatus.SUCCESS) { 3116 throw new DeviceRuntimeException( 3117 deviceManager.getAdbPath() 3118 + " connect " 3119 + microdroidSerial 3120 + " has failed: " 3121 + result, 3122 DeviceErrorIdentifier.SHELL_COMMAND_ERROR); 3123 } 3124 disconnected = 3125 result.getStdout().trim().equals("failed to connect to " + microdroidSerial); 3126 if (disconnected) { 3127 // adb demands us to disconnect if the prior connection was a failure. 3128 // b/194375443: this somtimes fails, thus 'try*'. 3129 getRunUtil() 3130 .runTimedCmd( 3131 10000, deviceManager.getAdbPath(), "disconnect", microdroidSerial); 3132 } 3133 } 3134 3135 elapsed = System.currentTimeMillis() - start; 3136 timeoutMillis -= elapsed; 3137 if (timeoutMillis > 0) { 3138 getRunUtil() 3139 .runTimedCmd( 3140 timeoutMillis, 3141 deviceManager.getAdbPath(), 3142 "-s", 3143 microdroidSerial, 3144 "wait-for-device"); 3145 } 3146 boolean dataAvailable = false; 3147 while (!dataAvailable && timeoutMillis >= 0) { 3148 elapsed = System.currentTimeMillis() - start; 3149 timeoutMillis -= elapsed; 3150 start = System.currentTimeMillis(); 3151 final String checkCmd = "if [ -d /data/local/tmp ]; then echo 1; fi"; 3152 dataAvailable = 3153 microdroidHelper.runOnMicrodroid(microdroidSerial, checkCmd).equals("1"); 3154 } 3155 // Check if it actually booted by reading a sysprop. 3156 if (!microdroidHelper 3157 .runOnMicrodroid(microdroidSerial, "getprop", "ro.hardware") 3158 .equals("microdroid")) { 3159 throw new DeviceRuntimeException( 3160 String.format("Device '%s' was not booted.", microdroidSerial), 3161 DeviceErrorIdentifier.SHELL_COMMAND_ERROR); 3162 } 3163 } 3164 3165 /** 3166 * Shuts down the microdroid device, if one exist. 3167 * 3168 * @throws DeviceNotAvailableException 3169 */ shutdownMicrodroid(@onnull ITestDevice microdroidDevice)3170 public void shutdownMicrodroid(@Nonnull ITestDevice microdroidDevice) 3171 throws DeviceNotAvailableException { 3172 Process process = ((NativeDevice) microdroidDevice).getMicrodroidProcess(); 3173 if (process == null) { 3174 throw new IllegalArgumentException("Process is null. TestDevice is not a Microdroid. "); 3175 } 3176 if (!mStartedMicrodroids.containsKey(process)) { 3177 throw new IllegalArgumentException( 3178 "Microdroid device was not started in this TestDevice."); 3179 } 3180 3181 process.destroy(); 3182 try { 3183 process.waitFor(); 3184 } catch (InterruptedException ex) { 3185 } 3186 3187 // disconnect from microdroid 3188 getRunUtil() 3189 .runTimedCmd( 3190 10000, 3191 GlobalConfiguration.getDeviceManagerInstance().getAdbPath(), 3192 "disconnect", 3193 microdroidDevice.getSerialNumber()); 3194 3195 GlobalConfiguration.getDeviceManagerInstance() 3196 .freeDevice(microdroidDevice, FreeDeviceState.AVAILABLE); 3197 MicrodroidTracker tracker = mStartedMicrodroids.remove(process); 3198 getRunUtil().allowInterrupt(true); 3199 try { 3200 tracker.executor.shutdownNow(); 3201 tracker.executor.awaitTermination(1L, TimeUnit.MINUTES); 3202 } catch (InterruptedException e) { 3203 CLog.e(e); 3204 } 3205 } 3206 3207 // TODO (b/274941025): remove when shell commands using this method are merged in AOSP executeShellV2CommandThatReturnsBooleanSafe( String cmdFormat, Object... cmdArgs)3208 private boolean executeShellV2CommandThatReturnsBooleanSafe( 3209 String cmdFormat, Object... cmdArgs) { 3210 try { 3211 return executeShellV2CommandThatReturnsBoolean(cmdFormat, cmdArgs); 3212 } catch (Exception e) { 3213 CLog.e(e); 3214 return false; 3215 } 3216 } 3217 executeShellV2CommandThatReturnsBoolean(String cmdFormat, Object... cmdArgs)3218 private boolean executeShellV2CommandThatReturnsBoolean(String cmdFormat, Object... cmdArgs) 3219 throws DeviceNotAvailableException { 3220 String cmd = String.format(cmdFormat, cmdArgs); 3221 CommandResult res = executeShellV2Command(cmd); 3222 if (!CommandStatus.SUCCESS.equals(res.getStatus())) { 3223 throw new DeviceRuntimeException( 3224 "Command '" + cmd + "' failed: " + res, 3225 DeviceErrorIdentifier.SHELL_COMMAND_ERROR); 3226 } 3227 String output = res.getStdout(); 3228 switch (output.trim().toLowerCase()) { 3229 case "true": 3230 return true; 3231 case "false": 3232 return false; 3233 default: 3234 throw new DeviceRuntimeException( 3235 "Non-boolean result for '" + cmd + "': " + output, 3236 DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE); 3237 } 3238 } 3239 3240 /** A builder used to create a Microdroid TestDevice. */ 3241 public static class MicrodroidBuilder { 3242 private File mApkFile; 3243 private String mApkPath; 3244 private String mConfigPath; 3245 private String mDebugLevel; 3246 private int mMemoryMib; 3247 private Integer mNumCpus; 3248 private String mCpuAffinity; 3249 private String mCpuTopology; 3250 private List<String> mExtraIdsigPaths; 3251 private boolean mProtectedVm; 3252 private Map<String, String> mTestDeviceOptions; 3253 private Map<File, String> mBootFiles; 3254 private long mAdbConnectTimeoutMs; 3255 private List<String> mAssignedDevices; 3256 private String mGki; 3257 private String mInstanceIdFile; // Path to instance_id file 3258 private String mInstanceImg; // Path to instance_img file 3259 private boolean mHugePages; 3260 3261 /** Creates a builder for the given APK/apkPath and the payload config file in APK. */ MicrodroidBuilder(File apkFile, String apkPath, @Nonnull String configPath)3262 private MicrodroidBuilder(File apkFile, String apkPath, @Nonnull String configPath) { 3263 mApkFile = apkFile; 3264 mApkPath = apkPath; 3265 mConfigPath = configPath; 3266 mDebugLevel = null; 3267 mMemoryMib = 0; 3268 mNumCpus = null; 3269 mCpuAffinity = null; 3270 mExtraIdsigPaths = new ArrayList<>(); 3271 mProtectedVm = false; // Vm is unprotected by default. 3272 mTestDeviceOptions = new LinkedHashMap<>(); 3273 mBootFiles = new LinkedHashMap<>(); 3274 mAdbConnectTimeoutMs = MICRODROID_DEFAULT_ADB_CONNECT_TIMEOUT_MINUTES * 60 * 1000; 3275 mAssignedDevices = new ArrayList<>(); 3276 mInstanceIdFile = null; 3277 mInstanceImg = null; 3278 } 3279 3280 /** Creates a Microdroid builder for the given APK and the payload config file in APK. */ fromFile( @onnull File apkFile, @Nonnull String configPath)3281 public static MicrodroidBuilder fromFile( 3282 @Nonnull File apkFile, @Nonnull String configPath) { 3283 return new MicrodroidBuilder(apkFile, null, configPath); 3284 } 3285 3286 /** 3287 * Creates a Microdroid builder for the given apkPath and the payload config file in APK. 3288 */ fromDevicePath( @onnull String apkPath, @Nonnull String configPath)3289 public static MicrodroidBuilder fromDevicePath( 3290 @Nonnull String apkPath, @Nonnull String configPath) { 3291 return new MicrodroidBuilder(null, apkPath, configPath); 3292 } 3293 3294 /** 3295 * Sets the debug level. 3296 * 3297 * <p>Supported values: "none" and "full". Android T also supports "app_only". 3298 */ debugLevel(String debugLevel)3299 public MicrodroidBuilder debugLevel(String debugLevel) { 3300 mDebugLevel = debugLevel; 3301 return this; 3302 } 3303 3304 /** 3305 * Sets the amount of RAM to give the VM. If this is zero or negative then the default will 3306 * be used. 3307 */ memoryMib(int memoryMib)3308 public MicrodroidBuilder memoryMib(int memoryMib) { 3309 mMemoryMib = memoryMib; 3310 return this; 3311 } 3312 3313 /** 3314 * Sets the number of vCPUs in the VM. Defaults to 1. 3315 * 3316 * <p>Only supported in Android T. 3317 */ numCpus(int num)3318 public MicrodroidBuilder numCpus(int num) { 3319 mNumCpus = num; 3320 return this; 3321 } 3322 3323 /** 3324 * Sets on which host CPUs the vCPUs can run. The format is a comma-separated list of CPUs 3325 * or CPU ranges to run vCPUs on. e.g. "0,1-3,5" to choose host CPUs 0, 1, 2, 3, and 5. Or 3326 * this can be a colon-separated list of assignments of vCPU to host CPU assignments. e.g. 3327 * "0=0:1=1:2=2" to map vCPU 0 to host CPU 0, and so on. 3328 * 3329 * <p>Only supported in Android T. 3330 */ cpuAffinity(String affinity)3331 public MicrodroidBuilder cpuAffinity(String affinity) { 3332 mCpuAffinity = affinity; 3333 return this; 3334 } 3335 3336 /** Sets the CPU topology configuration. Supported values: "one_cpu" and "match_host". */ cpuTopology(String cpuTopology)3337 public MicrodroidBuilder cpuTopology(String cpuTopology) { 3338 mCpuTopology = cpuTopology; 3339 return this; 3340 } 3341 3342 /** Sets whether the VM will be protected or not. */ protectedVm(boolean isProtectedVm)3343 public MicrodroidBuilder protectedVm(boolean isProtectedVm) { 3344 mProtectedVm = isProtectedVm; 3345 return this; 3346 } 3347 3348 /** Adds extra idsig file to the list. */ addExtraIdsigPath(String extraIdsigPath)3349 public MicrodroidBuilder addExtraIdsigPath(String extraIdsigPath) { 3350 if (!Strings.isNullOrEmpty(extraIdsigPath)) { 3351 mExtraIdsigPaths.add(extraIdsigPath); 3352 } 3353 return this; 3354 } 3355 3356 /** 3357 * Sets a {@link TestDeviceOptions} for the microdroid TestDevice. 3358 * 3359 * @param optionName The name of the TestDeviceOption to set 3360 * @param valueText The value 3361 * @return the microdroid builder. 3362 */ addTestDeviceOption(String optionName, String valueText)3363 public MicrodroidBuilder addTestDeviceOption(String optionName, String valueText) { 3364 mTestDeviceOptions.put(optionName, valueText); 3365 return this; 3366 } 3367 3368 /** 3369 * Adds a file for booting to be pushed to {@link #TEST_ROOT}. 3370 * 3371 * <p>Use this method if an file is required for booting microdroid. Otherwise use {@link 3372 * TestDevice#pushFile}. 3373 * 3374 * @param localFile The local file on the host 3375 * @param remoteFileName The remote file name on the device 3376 * @return the microdroid builder. 3377 */ addBootFile(File localFile, String remoteFileName)3378 public MicrodroidBuilder addBootFile(File localFile, String remoteFileName) { 3379 mBootFiles.put(localFile, remoteFileName); 3380 return this; 3381 } 3382 3383 /** 3384 * Adds a device to assign to microdroid. 3385 * 3386 * @param sysfsNode The path to the sysfs node to assign 3387 * @return the microdroid builder. 3388 */ addAssignableDevice(String sysfsNode)3389 public MicrodroidBuilder addAssignableDevice(String sysfsNode) { 3390 mAssignedDevices.add(sysfsNode); 3391 return this; 3392 } 3393 3394 /** 3395 * Sets the timeout for adb connect to microdroid TestDevice in millis. 3396 * 3397 * @param timeoutMs The timeout in millis 3398 */ setAdbConnectTimeoutMs(long timeoutMs)3399 public MicrodroidBuilder setAdbConnectTimeoutMs(long timeoutMs) { 3400 mAdbConnectTimeoutMs = timeoutMs; 3401 return this; 3402 } 3403 3404 /** 3405 * Uses GKI kernel instead of microdroid kernel 3406 * 3407 * @param version The GKI version to use 3408 */ gki(String version)3409 public MicrodroidBuilder gki(String version) { 3410 mGki = version; 3411 return this; 3412 } 3413 3414 /** 3415 * Sets the instance_id path. 3416 * 3417 * @param instanceIdPath: Path to the instanceId 3418 */ instanceIdFile(String instanceIdPath)3419 public MicrodroidBuilder instanceIdFile(String instanceIdPath) { 3420 mInstanceIdFile = instanceIdPath; 3421 return this; 3422 } 3423 3424 /** 3425 * Sets instance.img file path. 3426 * 3427 * @param instanceIdPath: Path to the instanceId 3428 */ instanceImgFile(String instanceImgPath)3429 public MicrodroidBuilder instanceImgFile(String instanceImgPath) { 3430 mInstanceImg = instanceImgPath; 3431 return this; 3432 } 3433 3434 /** 3435 * Sets whether to hint the kernel for transparent hugepages. 3436 * 3437 * @return the microdroid builder. 3438 */ hugePages(boolean hintHugePages)3439 public MicrodroidBuilder hugePages(boolean hintHugePages) { 3440 mHugePages = hintHugePages; 3441 return this; 3442 } 3443 3444 /** Starts a Micrdroid TestDevice on the given TestDevice. */ build(@onnull TestDevice device)3445 public ITestDevice build(@Nonnull TestDevice device) throws DeviceNotAvailableException { 3446 if (mNumCpus != null) { 3447 if (device.getApiLevel() != 33) { 3448 throw new IllegalStateException( 3449 "Setting number of CPUs only supported with API level 33"); 3450 } 3451 if (mNumCpus < 1) { 3452 throw new IllegalArgumentException("Number of vCPUs can not be less than 1."); 3453 } 3454 } 3455 3456 if (!Strings.isNullOrEmpty(mCpuTopology)) { 3457 device.checkApiLevelAgainstNextRelease("vm-cpu-topology", 34); 3458 } 3459 3460 if (mCpuAffinity != null) { 3461 if (device.getApiLevel() != 33) { 3462 throw new IllegalStateException( 3463 "Setting CPU affinity only supported with API level 33"); 3464 } 3465 if (!Pattern.matches("[\\d]+(-[\\d]+)?(,[\\d]+(-[\\d]+)?)*", mCpuAffinity) 3466 && !Pattern.matches("[\\d]+=[\\d]+(:[\\d]+=[\\d]+)*", mCpuAffinity)) { 3467 throw new IllegalArgumentException( 3468 "CPU affinity [" + mCpuAffinity + "]" + " is invalid"); 3469 } 3470 } 3471 if (mInstanceIdFile == null) { 3472 mInstanceIdFile = TEST_ROOT + INSTANCE_ID_FILE; 3473 } 3474 if (mInstanceImg == null) { 3475 mInstanceImg = TEST_ROOT + INSTANCE_IMG; 3476 } 3477 3478 return device.startMicrodroid(this); 3479 } 3480 } 3481 handleInstallationError(InstallException e)3482 private String handleInstallationError(InstallException e) { 3483 String message = e.getMessage(); 3484 if (message == null) { 3485 message = 3486 String.format( 3487 "InstallException during package installation. " + "cause: %s", 3488 StreamUtil.getStackTrace(e)); 3489 } 3490 return message; 3491 } 3492 handleInstallReceiver(InstallReceiver receiver, File packageFile)3493 private String handleInstallReceiver(InstallReceiver receiver, File packageFile) { 3494 if (receiver.isSuccessfullyCompleted()) { 3495 return null; 3496 } 3497 if (receiver.getErrorMessage() == null) { 3498 return String.format("Installation of %s timed out", packageFile.getAbsolutePath()); 3499 } 3500 String error = receiver.getErrorMessage(); 3501 if (error.contains("cmd: Failure calling service package") 3502 || error.contains("Can't find service: package")) { 3503 String message = 3504 String.format( 3505 "Failed to install '%s'. Device might have" 3506 + " crashed, it returned: %s", 3507 packageFile.getName(), error); 3508 throw new DeviceRuntimeException(message, DeviceErrorIdentifier.DEVICE_CRASHED); 3509 } 3510 return error; 3511 } 3512 } 3513