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.log.LogUtil.CLog; 27 import com.android.tradefed.result.ByteArrayInputStreamSource; 28 import com.android.tradefed.result.FileInputStreamSource; 29 import com.android.tradefed.result.InputStreamSource; 30 import com.android.tradefed.util.CommandResult; 31 import com.android.tradefed.util.CommandStatus; 32 import com.android.tradefed.util.KeyguardControllerState; 33 import com.android.tradefed.util.RunUtil; 34 import com.android.tradefed.util.StreamUtil; 35 import com.android.tradefed.util.UserUtil; 36 37 import com.google.common.annotations.VisibleForTesting; 38 import com.google.common.base.Strings; 39 40 import java.awt.Image; 41 import java.awt.image.BufferedImage; 42 import java.io.ByteArrayOutputStream; 43 import java.io.File; 44 import java.io.IOException; 45 import java.util.ArrayList; 46 import java.util.Arrays; 47 import java.util.HashMap; 48 import java.util.HashSet; 49 import java.util.List; 50 import java.util.Map; 51 import java.util.Set; 52 import java.util.concurrent.TimeUnit; 53 import java.util.regex.Matcher; 54 import java.util.regex.Pattern; 55 56 import javax.imageio.ImageIO; 57 58 /** 59 * Implementation of a {@link ITestDevice} for a full stack android device 60 */ 61 public class TestDevice extends NativeDevice { 62 63 /** number of attempts made to clear dialogs */ 64 private static final int NUM_CLEAR_ATTEMPTS = 5; 65 /** the command used to dismiss a error dialog. Currently sends a DPAD_CENTER key event */ 66 static final String DISMISS_DIALOG_CMD = "input keyevent 23"; 67 /** Commands that can be used to dismiss the keyguard. */ 68 public static final String DISMISS_KEYGUARD_CMD = "input keyevent 82"; 69 70 /** 71 * Alternative command to dismiss the keyguard by requesting the Window Manager service to do 72 * it. Api 23 and after. 73 */ 74 static final String DISMISS_KEYGUARD_WM_CMD = "wm dismiss-keyguard"; 75 76 /** Timeout to wait for input dispatch to become ready **/ 77 private static final long INPUT_DISPATCH_READY_TIMEOUT = 5 * 1000; 78 /** command to test input dispatch readiness **/ 79 private static final String TEST_INPUT_CMD = "dumpsys input"; 80 81 private static final long AM_COMMAND_TIMEOUT = 10 * 1000; 82 private static final long CHECK_NEW_USER = 1000; 83 84 static final String LIST_PACKAGES_CMD = "pm list packages -f"; 85 private static final Pattern PACKAGE_REGEX = Pattern.compile("package:(.*)=(.*)"); 86 87 static final String LIST_APEXES_CMD = "pm list packages --apex-only --show-versioncode"; 88 private static final Pattern APEXES_REGEX = Pattern.compile("package:(.*) versionCode:(.*)"); 89 90 private static final int FLAG_PRIMARY = 1; // From the UserInfo class 91 92 private static final String[] SETTINGS_NAMESPACE = {"system", "secure", "global"}; 93 94 /** user pattern in the output of "pm list users" = TEXT{<id>:<name>:<flags>} TEXT * */ 95 private static final String USER_PATTERN = "(.*?\\{)(\\d+)(:)(.*)(:)(\\d+)(\\}.*)"; 96 /** Pattern to find the display ids of "dumpsys SurfaceFlinger" */ 97 private static final String DISPLAY_ID_PATTERN = "(Display )(?<id>\\d+)( color modes:)"; 98 99 private static final int API_LEVEL_GET_CURRENT_USER = 24; 100 /** Timeout to wait for a screenshot before giving up to avoid hanging forever */ 101 private static final long MAX_SCREENSHOT_TIMEOUT = 5 * 60 * 1000; // 5 min 102 103 /** adb shell am dumpheap <service pid> <dump file path> */ 104 private static final String DUMPHEAP_CMD = "am dumpheap %s %s"; 105 /** Time given to a file to be dumped on device side */ 106 private static final long DUMPHEAP_TIME = 5000l; 107 108 /** Timeout in minutes for the package installation */ 109 static final long INSTALL_TIMEOUT_MINUTES = 4; 110 /** Max timeout to output for package installation */ 111 static final long INSTALL_TIMEOUT_TO_OUTPUT_MINUTES = 3; 112 113 private boolean mWasWifiHelperInstalled = false; 114 115 private static final String APEX_SUFFIX = ".apex"; 116 private static final String APEX_ARG = "--apex"; 117 118 /** 119 * @param device 120 * @param stateMonitor 121 * @param allocationMonitor 122 */ TestDevice(IDevice device, IDeviceStateMonitor stateMonitor, IDeviceMonitor allocationMonitor)123 public TestDevice(IDevice device, IDeviceStateMonitor stateMonitor, 124 IDeviceMonitor allocationMonitor) { 125 super(device, stateMonitor, allocationMonitor); 126 } 127 128 /** 129 * Core implementation of package installation, with retries around 130 * {@link IDevice#installPackage(String, boolean, String...)} 131 * @param packageFile 132 * @param reinstall 133 * @param extraArgs 134 * @return the response from the installation 135 * @throws DeviceNotAvailableException 136 */ internalInstallPackage( final File packageFile, final boolean reinstall, final List<String> extraArgs)137 private String internalInstallPackage( 138 final File packageFile, final boolean reinstall, final List<String> extraArgs) 139 throws DeviceNotAvailableException { 140 List<String> args = new ArrayList<>(extraArgs); 141 if (packageFile.getName().endsWith(APEX_SUFFIX)) { 142 args.add(APEX_ARG); 143 } 144 // use array to store response, so it can be returned to caller 145 final String[] response = new String[1]; 146 DeviceAction installAction = 147 new DeviceAction() { 148 @Override 149 public boolean run() throws InstallException { 150 try { 151 InstallReceiver receiver = createInstallReceiver(); 152 getIDevice() 153 .installPackage( 154 packageFile.getAbsolutePath(), 155 reinstall, 156 receiver, 157 INSTALL_TIMEOUT_MINUTES, 158 INSTALL_TIMEOUT_TO_OUTPUT_MINUTES, 159 TimeUnit.MINUTES, 160 args.toArray(new String[] {})); 161 if (receiver.isSuccessfullyCompleted()) { 162 response[0] = null; 163 } else if (receiver.getErrorMessage() == null) { 164 response[0] = 165 String.format( 166 "Installation of %s timed out", 167 packageFile.getAbsolutePath()); 168 } else { 169 response[0] = receiver.getErrorMessage(); 170 } 171 } catch (InstallException e) { 172 String message = e.getMessage(); 173 if (message == null) { 174 message = 175 String.format( 176 "InstallException during package installation. " 177 + "cause: %s", 178 StreamUtil.getStackTrace(e)); 179 } 180 response[0] = message; 181 } 182 return response[0] == null; 183 } 184 }; 185 performDeviceAction(String.format("install %s", packageFile.getAbsolutePath()), 186 installAction, MAX_RETRY_ATTEMPTS); 187 return response[0]; 188 } 189 190 /** 191 * Creates and return an {@link InstallReceiver} for {@link #internalInstallPackage(File, 192 * boolean, List)} and {@link #installPackage(File, File, boolean, String...)} testing. 193 */ 194 @VisibleForTesting createInstallReceiver()195 InstallReceiver createInstallReceiver() { 196 return new InstallReceiver(); 197 } 198 199 /** 200 * {@inheritDoc} 201 */ 202 @Override installPackage(final File packageFile, final boolean reinstall, final String... extraArgs)203 public String installPackage(final File packageFile, final boolean reinstall, 204 final String... extraArgs) throws DeviceNotAvailableException { 205 boolean runtimePermissionSupported = isRuntimePermissionSupported(); 206 List<String> args = new ArrayList<>(Arrays.asList(extraArgs)); 207 // grant all permissions by default if feature is supported 208 if (runtimePermissionSupported) { 209 args.add("-g"); 210 } 211 return internalInstallPackage(packageFile, reinstall, args); 212 } 213 214 /** 215 * {@inheritDoc} 216 */ 217 @Override installPackage(File packageFile, boolean reinstall, boolean grantPermissions, String... extraArgs)218 public String installPackage(File packageFile, boolean reinstall, boolean grantPermissions, 219 String... extraArgs) throws DeviceNotAvailableException { 220 ensureRuntimePermissionSupported(); 221 List<String> args = new ArrayList<>(Arrays.asList(extraArgs)); 222 if (grantPermissions) { 223 args.add("-g"); 224 } 225 return internalInstallPackage(packageFile, reinstall, args); 226 } 227 228 /** 229 * {@inheritDoc} 230 */ 231 @Override installPackageForUser(File packageFile, boolean reinstall, int userId, String... extraArgs)232 public String installPackageForUser(File packageFile, boolean reinstall, int userId, 233 String... extraArgs) throws DeviceNotAvailableException { 234 boolean runtimePermissionSupported = isRuntimePermissionSupported(); 235 List<String> args = new ArrayList<>(Arrays.asList(extraArgs)); 236 // grant all permissions by default if feature is supported 237 if (runtimePermissionSupported) { 238 args.add("-g"); 239 } 240 args.add("--user"); 241 args.add(Integer.toString(userId)); 242 return internalInstallPackage(packageFile, reinstall, args); 243 } 244 245 /** 246 * {@inheritDoc} 247 */ 248 @Override installPackageForUser(File packageFile, boolean reinstall, boolean grantPermissions, int userId, String... extraArgs)249 public String installPackageForUser(File packageFile, boolean reinstall, 250 boolean grantPermissions, int userId, String... extraArgs) 251 throws DeviceNotAvailableException { 252 ensureRuntimePermissionSupported(); 253 List<String> args = new ArrayList<>(Arrays.asList(extraArgs)); 254 if (grantPermissions) { 255 args.add("-g"); 256 } 257 args.add("--user"); 258 args.add(Integer.toString(userId)); 259 return internalInstallPackage(packageFile, reinstall, args); 260 } 261 installPackage(final File packageFile, final File certFile, final boolean reinstall, final String... extraArgs)262 public String installPackage(final File packageFile, final File certFile, 263 final boolean reinstall, final String... extraArgs) throws DeviceNotAvailableException { 264 // use array to store response, so it can be returned to caller 265 final String[] response = new String[1]; 266 DeviceAction installAction = 267 new DeviceAction() { 268 @Override 269 public boolean run() 270 throws InstallException, SyncException, IOException, TimeoutException, 271 AdbCommandRejectedException { 272 // TODO: create a getIDevice().installPackage(File, File...) method when the 273 // dist cert functionality is ready to be open sourced 274 String remotePackagePath = 275 getIDevice().syncPackageToDevice(packageFile.getAbsolutePath()); 276 String remoteCertPath = 277 getIDevice().syncPackageToDevice(certFile.getAbsolutePath()); 278 // trick installRemotePackage into issuing a 'pm install <apk> <cert>' 279 // command, by adding apk path to extraArgs, and using cert as the 280 // 'apk file'. 281 String[] newExtraArgs = new String[extraArgs.length + 1]; 282 System.arraycopy(extraArgs, 0, newExtraArgs, 0, extraArgs.length); 283 newExtraArgs[newExtraArgs.length - 1] = 284 String.format("\"%s\"", remotePackagePath); 285 try { 286 InstallReceiver receiver = createInstallReceiver(); 287 getIDevice() 288 .installRemotePackage( 289 remoteCertPath, 290 reinstall, 291 receiver, 292 INSTALL_TIMEOUT_MINUTES, 293 INSTALL_TIMEOUT_TO_OUTPUT_MINUTES, 294 TimeUnit.MINUTES, 295 newExtraArgs); 296 if (receiver.isSuccessfullyCompleted()) { 297 response[0] = null; 298 } else if (receiver.getErrorMessage() == null) { 299 response[0] = 300 String.format( 301 "Installation of %s timed out.", 302 packageFile.getAbsolutePath()); 303 } else { 304 response[0] = receiver.getErrorMessage(); 305 } 306 } catch (InstallException e) { 307 String message = e.getMessage(); 308 if (message == null) { 309 message = 310 String.format( 311 "InstallException during package installation. " 312 + "cause: %s", 313 StreamUtil.getStackTrace(e)); 314 } 315 response[0] = message; 316 } finally { 317 getIDevice().removeRemotePackage(remotePackagePath); 318 getIDevice().removeRemotePackage(remoteCertPath); 319 } 320 return true; 321 } 322 }; 323 performDeviceAction(String.format("install %s", packageFile.getAbsolutePath()), 324 installAction, MAX_RETRY_ATTEMPTS); 325 return response[0]; 326 } 327 328 /** 329 * {@inheritDoc} 330 */ 331 @Override uninstallPackage(final String packageName)332 public String uninstallPackage(final String packageName) throws DeviceNotAvailableException { 333 // use array to store response, so it can be returned to caller 334 final String[] response = new String[1]; 335 DeviceAction uninstallAction = new DeviceAction() { 336 @Override 337 public boolean run() throws InstallException { 338 CLog.d("Uninstalling %s", packageName); 339 String result = getIDevice().uninstallPackage(packageName); 340 response[0] = result; 341 return result == null; 342 } 343 }; 344 performDeviceAction(String.format("uninstall %s", packageName), uninstallAction, 345 MAX_RETRY_ATTEMPTS); 346 return response[0]; 347 } 348 349 /** 350 * Core implementation for installing application with split apk files {@link 351 * IDevice#installPackages(List, boolean, List)} See 352 * "https://developer.android.com/studio/build/configure-apk-splits" on how to split apk to 353 * several files. 354 * 355 * @param packageFiles the local apk files 356 * @param reinstall <code>true</code> if a reinstall should be performed 357 * @param extraArgs optional extra arguments to pass. See 'adb shell pm install --help' for 358 * available options. 359 * @return the response from the installation <code>null</code> if installation succeeds. 360 * @throws DeviceNotAvailableException 361 */ internalInstallPackages( final List<File> packageFiles, final boolean reinstall, final List<String> extraArgs)362 private String internalInstallPackages( 363 final List<File> packageFiles, final boolean reinstall, final List<String> extraArgs) 364 throws DeviceNotAvailableException { 365 // use array to store response, so it can be returned to caller 366 final String[] response = new String[1]; 367 DeviceAction installAction = 368 new DeviceAction() { 369 @Override 370 public boolean run() throws InstallException { 371 try { 372 getIDevice() 373 .installPackages( 374 packageFiles, 375 reinstall, 376 extraArgs, 377 INSTALL_TIMEOUT_MINUTES, 378 TimeUnit.MINUTES); 379 response[0] = null; 380 return true; 381 } catch (InstallException e) { 382 response[0] = e.getMessage(); 383 if (response[0] == null) { 384 response[0] = 385 String.format( 386 "InstallException: %s", 387 StreamUtil.getStackTrace(e)); 388 } 389 return false; 390 } 391 } 392 }; 393 performDeviceAction( 394 String.format("install %s", packageFiles.toString()), 395 installAction, 396 MAX_RETRY_ATTEMPTS); 397 return response[0]; 398 } 399 400 /** {@inheritDoc} */ 401 @Override installPackages( final List<File> packageFiles, final boolean reinstall, final String... extraArgs)402 public String installPackages( 403 final List<File> packageFiles, final boolean reinstall, final String... extraArgs) 404 throws DeviceNotAvailableException { 405 // Grant all permissions by default if feature is supported 406 return installPackages(packageFiles, reinstall, isRuntimePermissionSupported(), extraArgs); 407 } 408 409 /** {@inheritDoc} */ 410 @Override installPackages( List<File> packageFiles, boolean reinstall, boolean grantPermissions, String... extraArgs)411 public String installPackages( 412 List<File> packageFiles, 413 boolean reinstall, 414 boolean grantPermissions, 415 String... extraArgs) 416 throws DeviceNotAvailableException { 417 List<String> args = new ArrayList<>(Arrays.asList(extraArgs)); 418 if (grantPermissions) { 419 ensureRuntimePermissionSupported(); 420 args.add("-g"); 421 } 422 return internalInstallPackages(packageFiles, reinstall, args); 423 } 424 425 /** {@inheritDoc} */ 426 @Override installPackagesForUser( List<File> packageFiles, boolean reinstall, int userId, String... extraArgs)427 public String installPackagesForUser( 428 List<File> packageFiles, boolean reinstall, int userId, String... extraArgs) 429 throws DeviceNotAvailableException { 430 // Grant all permissions by default if feature is supported 431 return installPackagesForUser( 432 packageFiles, reinstall, isRuntimePermissionSupported(), userId, extraArgs); 433 } 434 435 /** {@inheritDoc} */ 436 @Override installPackagesForUser( List<File> packageFiles, boolean reinstall, boolean grantPermissions, int userId, String... extraArgs)437 public String installPackagesForUser( 438 List<File> packageFiles, 439 boolean reinstall, 440 boolean grantPermissions, 441 int userId, 442 String... extraArgs) 443 throws DeviceNotAvailableException { 444 List<String> args = new ArrayList<>(Arrays.asList(extraArgs)); 445 if (grantPermissions) { 446 ensureRuntimePermissionSupported(); 447 args.add("-g"); 448 } 449 args.add("--user"); 450 args.add(Integer.toString(userId)); 451 return internalInstallPackages(packageFiles, reinstall, args); 452 } 453 454 /** 455 * Core implementation for split apk remote installation {@link IDevice#installPackage(String, 456 * boolean, String...)} See "https://developer.android.com/studio/build/configure-apk-splits" on 457 * how to split apk to several files. 458 * 459 * @param remoteApkPaths the remote apk file paths 460 * @param reinstall <code>true</code> if a reinstall should be performed 461 * @param extraArgs optional extra arguments to pass. See 'adb shell pm install --help' for 462 * available options. 463 * @return the response from the installation <code>null</code> if installation succeeds. 464 * @throws DeviceNotAvailableException 465 */ internalInstallRemotePackages( final List<String> remoteApkPaths, final boolean reinstall, final List<String> extraArgs)466 private String internalInstallRemotePackages( 467 final List<String> remoteApkPaths, 468 final boolean reinstall, 469 final List<String> extraArgs) 470 throws DeviceNotAvailableException { 471 // use array to store response, so it can be returned to caller 472 final String[] response = new String[1]; 473 DeviceAction installAction = 474 new DeviceAction() { 475 @Override 476 public boolean run() throws InstallException { 477 try { 478 getIDevice() 479 .installRemotePackages( 480 remoteApkPaths, 481 reinstall, 482 extraArgs, 483 INSTALL_TIMEOUT_MINUTES, 484 TimeUnit.MINUTES); 485 response[0] = null; 486 return true; 487 } catch (InstallException e) { 488 response[0] = e.getMessage(); 489 if (response[0] == null) { 490 response[0] = String.format( 491 "InstallException during package installation. cause: %s", 492 StreamUtil.getStackTrace(e)); 493 } 494 return false; 495 } 496 } 497 }; 498 performDeviceAction( 499 String.format("install %s", remoteApkPaths.toString()), 500 installAction, 501 MAX_RETRY_ATTEMPTS); 502 return response[0]; 503 } 504 505 /** {@inheritDoc} */ 506 @Override installRemotePackages( final List<String> remoteApkPaths, final boolean reinstall, final String... extraArgs)507 public String installRemotePackages( 508 final List<String> remoteApkPaths, final boolean reinstall, final String... extraArgs) 509 throws DeviceNotAvailableException { 510 // Grant all permissions by default if feature is supported 511 return installRemotePackages( 512 remoteApkPaths, reinstall, isRuntimePermissionSupported(), extraArgs); 513 } 514 515 /** {@inheritDoc} */ 516 @Override installRemotePackages( List<String> remoteApkPaths, boolean reinstall, boolean grantPermissions, String... extraArgs)517 public String installRemotePackages( 518 List<String> remoteApkPaths, 519 boolean reinstall, 520 boolean grantPermissions, 521 String... extraArgs) 522 throws DeviceNotAvailableException { 523 List<String> args = new ArrayList<>(Arrays.asList(extraArgs)); 524 if (grantPermissions) { 525 ensureRuntimePermissionSupported(); 526 args.add("-g"); 527 } 528 return internalInstallRemotePackages(remoteApkPaths, reinstall, args); 529 } 530 531 /** {@inheritDoc} */ 532 @Override getScreenshot()533 public InputStreamSource getScreenshot() throws DeviceNotAvailableException { 534 return getScreenshot("PNG"); 535 } 536 537 /** 538 * {@inheritDoc} 539 */ 540 @Override getScreenshot(String format)541 public InputStreamSource getScreenshot(String format) throws DeviceNotAvailableException { 542 return getScreenshot(format, true); 543 } 544 545 /** {@inheritDoc} */ 546 @Override getScreenshot(String format, boolean rescale)547 public InputStreamSource getScreenshot(String format, boolean rescale) 548 throws DeviceNotAvailableException { 549 if (!format.equalsIgnoreCase("PNG") && !format.equalsIgnoreCase("JPEG")){ 550 CLog.e("Screenshot: Format %s is not supported, defaulting to PNG.", format); 551 format = "PNG"; 552 } 553 ScreenshotAction action = new ScreenshotAction(); 554 if (performDeviceAction("screenshot", action, MAX_RETRY_ATTEMPTS)) { 555 byte[] imageData = 556 compressRawImage(action.mRawScreenshot, format.toUpperCase(), rescale); 557 if (imageData != null) { 558 return new ByteArrayInputStreamSource(imageData); 559 } 560 } 561 // Return an error in the buffer 562 return new ByteArrayInputStreamSource( 563 "Error: device reported null for screenshot.".getBytes()); 564 } 565 566 /** {@inheritDoc} */ 567 @Override getScreenshot(int displayId)568 public InputStreamSource getScreenshot(int displayId) throws DeviceNotAvailableException { 569 final String tmpDevicePath = String.format("/data/local/tmp/display_%s.png", displayId); 570 CommandResult result = 571 executeShellV2Command( 572 String.format("screencap -p -d %s %s", displayId, tmpDevicePath)); 573 if (!CommandStatus.SUCCESS.equals(result.getStatus())) { 574 // Return an error in the buffer 575 CLog.e("Error: device reported error for screenshot: %s", result.getStderr()); 576 return null; 577 } 578 try { 579 File tmpScreenshot = pullFile(tmpDevicePath); 580 if (tmpScreenshot == null) { 581 return null; 582 } 583 return new FileInputStreamSource(tmpScreenshot, true); 584 } finally { 585 deleteFile(tmpDevicePath); 586 } 587 } 588 589 private class ScreenshotAction implements DeviceAction { 590 591 RawImage mRawScreenshot; 592 593 /** 594 * {@inheritDoc} 595 */ 596 @Override run()597 public boolean run() throws IOException, TimeoutException, AdbCommandRejectedException, 598 ShellCommandUnresponsiveException, InstallException, SyncException { 599 mRawScreenshot = 600 getIDevice().getScreenshot(MAX_SCREENSHOT_TIMEOUT, TimeUnit.MILLISECONDS); 601 return mRawScreenshot != null; 602 } 603 } 604 605 /** 606 * Helper to compress a rawImage obtained from the screen. 607 * 608 * @param rawImage {@link RawImage} to compress. 609 * @param format resulting format of compressed image. PNG and JPEG are supported. 610 * @param rescale if rescaling should be done to further reduce size of compressed image. 611 * @return compressed image. 612 */ 613 @VisibleForTesting compressRawImage(RawImage rawImage, String format, boolean rescale)614 byte[] compressRawImage(RawImage rawImage, String format, boolean rescale) { 615 BufferedImage image = rawImageToBufferedImage(rawImage, format); 616 617 // Rescale to reduce size if needed 618 // Screenshot default format is 1080 x 1920, 8-bit/color RGBA 619 // By cutting in half we can easily keep good quality and smaller size 620 if (rescale) { 621 image = rescaleImage(image); 622 } 623 624 return getImageData(image, format); 625 } 626 627 /** 628 * Converts {@link RawImage} to {@link BufferedImage} in specified format. 629 * 630 * @param rawImage {@link RawImage} to convert. 631 * @param format resulting format of image. PNG and JPEG are supported. 632 * @return converted image. 633 */ 634 @VisibleForTesting rawImageToBufferedImage(RawImage rawImage, String format)635 BufferedImage rawImageToBufferedImage(RawImage rawImage, String format) { 636 BufferedImage image = null; 637 638 if ("JPEG".equalsIgnoreCase(format)) { 639 //JPEG does not support ARGB without a special encoder 640 image = 641 new BufferedImage( 642 rawImage.width, rawImage.height, BufferedImage.TYPE_3BYTE_BGR); 643 } 644 else { 645 image = new BufferedImage(rawImage.width, rawImage.height, BufferedImage.TYPE_INT_ARGB); 646 } 647 648 // borrowed conversion logic from platform/sdk/screenshot/.../Screenshot.java 649 int index = 0; 650 int IndexInc = rawImage.bpp >> 3; 651 for (int y = 0 ; y < rawImage.height ; y++) { 652 for (int x = 0 ; x < rawImage.width ; x++) { 653 int value = rawImage.getARGB(index); 654 index += IndexInc; 655 image.setRGB(x, y, value); 656 } 657 } 658 659 return image; 660 } 661 662 /** 663 * Rescales image cutting it in half. 664 * 665 * @param image source {@link BufferedImage}. 666 * @return resulting scaled image. 667 */ 668 @VisibleForTesting rescaleImage(BufferedImage image)669 BufferedImage rescaleImage(BufferedImage image) { 670 int shortEdge = Math.min(image.getHeight(), image.getWidth()); 671 if (shortEdge > 720) { 672 Image resized = 673 image.getScaledInstance( 674 image.getWidth() / 2, image.getHeight() / 2, Image.SCALE_SMOOTH); 675 image = 676 new BufferedImage( 677 image.getWidth() / 2, image.getHeight() / 2, Image.SCALE_REPLICATE); 678 image.getGraphics().drawImage(resized, 0, 0, null); 679 } 680 return image; 681 } 682 683 /** 684 * Gets byte array representation of {@link BufferedImage}. 685 * 686 * @param image source {@link BufferedImage}. 687 * @param format resulting format of image. PNG and JPEG are supported. 688 * @return byte array representation of the image. 689 */ 690 @VisibleForTesting getImageData(BufferedImage image, String format)691 byte[] getImageData(BufferedImage image, String format) { 692 // store compressed image in memory, and let callers write to persistent storage 693 // use initial buffer size of 128K 694 byte[] imageData = null; 695 ByteArrayOutputStream imageOut = new ByteArrayOutputStream(128*1024); 696 try { 697 if (ImageIO.write(image, format, imageOut)) { 698 imageData = imageOut.toByteArray(); 699 } else { 700 CLog.e("Failed to compress screenshot to png"); 701 } 702 } catch (IOException e) { 703 CLog.e("Failed to compress screenshot to png"); 704 CLog.e(e); 705 } 706 StreamUtil.close(imageOut); 707 return imageData; 708 } 709 710 /** 711 * {@inheritDoc} 712 */ 713 @Override clearErrorDialogs()714 public boolean clearErrorDialogs() throws DeviceNotAvailableException { 715 // attempt to clear error dialogs multiple times 716 for (int i = 0; i < NUM_CLEAR_ATTEMPTS; i++) { 717 int numErrorDialogs = getErrorDialogCount(); 718 if (numErrorDialogs == 0) { 719 return true; 720 } 721 doClearDialogs(numErrorDialogs); 722 } 723 if (getErrorDialogCount() > 0) { 724 // at this point, all attempts to clear error dialogs completely have failed 725 // it might be the case that the process keeps showing new dialogs immediately after 726 // clearing. There's really no workaround, but to dump an error 727 CLog.e("error dialogs still exist on %s.", getSerialNumber()); 728 return false; 729 } 730 return true; 731 } 732 733 /** 734 * Detects the number of crash or ANR dialogs currently displayed. 735 * <p/> 736 * Parses output of 'dump activity processes' 737 * 738 * @return count of dialogs displayed 739 * @throws DeviceNotAvailableException 740 */ getErrorDialogCount()741 private int getErrorDialogCount() throws DeviceNotAvailableException { 742 int errorDialogCount = 0; 743 Pattern crashPattern = Pattern.compile(".*crashing=true.*AppErrorDialog.*"); 744 Pattern anrPattern = Pattern.compile(".*notResponding=true.*AppNotRespondingDialog.*"); 745 String systemStatusOutput = 746 executeShellCommand( 747 "dumpsys activity processes | grep -e .*crashing=true.*AppErrorDialog.* -e .*notResponding=true.*AppNotRespondingDialog.*"); 748 Matcher crashMatcher = crashPattern.matcher(systemStatusOutput); 749 while (crashMatcher.find()) { 750 errorDialogCount++; 751 } 752 Matcher anrMatcher = anrPattern.matcher(systemStatusOutput); 753 while (anrMatcher.find()) { 754 errorDialogCount++; 755 } 756 757 return errorDialogCount; 758 } 759 doClearDialogs(int numDialogs)760 private void doClearDialogs(int numDialogs) throws DeviceNotAvailableException { 761 CLog.i("Attempted to clear %d dialogs on %s", numDialogs, getSerialNumber()); 762 for (int i=0; i < numDialogs; i++) { 763 // send DPAD_CENTER 764 executeShellCommand(DISMISS_DIALOG_CMD); 765 } 766 } 767 768 /** 769 * {@inheritDoc} 770 */ 771 @Override disableKeyguard()772 public void disableKeyguard() throws DeviceNotAvailableException { 773 long start = System.currentTimeMillis(); 774 while (true) { 775 Boolean ready = isDeviceInputReady(); 776 if (ready == null) { 777 // unsupported API level, bail 778 break; 779 } 780 if (ready) { 781 // input dispatch is ready, bail 782 break; 783 } 784 long timeSpent = System.currentTimeMillis() - start; 785 if (timeSpent > INPUT_DISPATCH_READY_TIMEOUT) { 786 CLog.w("Timeout after waiting %dms on enabling of input dispatch", timeSpent); 787 // break & proceed anyway 788 break; 789 } else { 790 getRunUtil().sleep(1000); 791 } 792 } 793 if (getApiLevel() >= 23) { 794 CLog.i( 795 "Attempting to disable keyguard on %s using %s", 796 getSerialNumber(), DISMISS_KEYGUARD_WM_CMD); 797 String output = executeShellCommand(DISMISS_KEYGUARD_WM_CMD); 798 CLog.i("output of %s: %s", DISMISS_KEYGUARD_WM_CMD, output); 799 } else { 800 CLog.i("Command: %s, is not supported, falling back to %s", DISMISS_KEYGUARD_WM_CMD, 801 DISMISS_KEYGUARD_CMD); 802 executeShellCommand(DISMISS_KEYGUARD_CMD); 803 } 804 // TODO: check that keyguard was actually dismissed. 805 } 806 807 /** {@inheritDoc} */ 808 @Override getKeyguardState()809 public KeyguardControllerState getKeyguardState() throws DeviceNotAvailableException { 810 String output = 811 executeShellCommand("dumpsys activity activities | grep -A3 KeyguardController:"); 812 CLog.d("Output from KeyguardController: %s", output); 813 KeyguardControllerState state = 814 KeyguardControllerState.create(Arrays.asList(output.trim().split("\n"))); 815 return state; 816 } 817 818 /** 819 * Tests the device to see if input dispatcher is ready 820 * 821 * @return <code>null</code> if not supported by platform, or the actual readiness state 822 * @throws DeviceNotAvailableException 823 */ isDeviceInputReady()824 Boolean isDeviceInputReady() throws DeviceNotAvailableException { 825 CollectingOutputReceiver receiver = new CollectingOutputReceiver(); 826 executeShellCommand(TEST_INPUT_CMD, receiver); 827 String output = receiver.getOutput(); 828 Matcher m = INPUT_DISPATCH_STATE_REGEX.matcher(output); 829 if (!m.find()) { 830 // output does not contain the line at all, implying unsupported API level, bail 831 return null; 832 } 833 return "1".equals(m.group(1)); 834 } 835 836 /** 837 * {@inheritDoc} 838 */ 839 @Override prePostBootSetup()840 protected void prePostBootSetup() throws DeviceNotAvailableException { 841 if (mOptions.isDisableKeyguard()) { 842 disableKeyguard(); 843 } 844 } 845 846 /** 847 * Performs an reboot via framework power manager 848 * 849 * Must have root access, device must be API Level 18 or above 850 * 851 * @param into the mode to reboot into, currently supported: bootloader, recovery, leave it 852 * null for a plain reboot 853 * @return <code>true</code> if the device rebooted, <code>false</code> if not successful or 854 * unsupported 855 * @throws DeviceNotAvailableException 856 */ doAdbFrameworkReboot(final String into)857 private boolean doAdbFrameworkReboot(final String into) throws DeviceNotAvailableException { 858 // use framework reboot when: 859 // 1. device API level >= 18 860 // 2. has adb root 861 // 3. framework is running 862 if (!isEnableAdbRoot()) { 863 CLog.i("framework reboot is not supported; when enable root is disabled"); 864 return false; 865 } 866 enableAdbRoot(); 867 if (getApiLevel() >= 18 && isAdbRoot()) { 868 try { 869 // check framework running 870 String output = executeShellCommand("pm path android"); 871 if (output == null || !output.contains("package:")) { 872 CLog.v("framework reboot: can't detect framework running"); 873 return false; 874 } 875 String command = "svc power reboot"; 876 if (into != null && !into.isEmpty()) { 877 command = String.format("%s %s", command, into); 878 } 879 executeShellCommand(command); 880 } catch (DeviceUnresponsiveException due) { 881 CLog.v("framework reboot: device unresponsive to shell command, using fallback"); 882 return false; 883 } 884 boolean notAvailable = waitForDeviceNotAvailable(30 * 1000); 885 if (notAvailable) { 886 postAdbReboot(); 887 } 888 return notAvailable; 889 } else { 890 CLog.v("framework reboot: not supported"); 891 return false; 892 } 893 } 894 895 /** 896 * Perform a adb reboot. 897 * 898 * @param into the bootloader name to reboot into, or <code>null</code> to just reboot the 899 * device. 900 * @throws DeviceNotAvailableException 901 */ 902 @Override doAdbReboot(final String into)903 protected void doAdbReboot(final String into) throws DeviceNotAvailableException { 904 if (!doAdbFrameworkReboot(into)) { 905 super.doAdbReboot(into); 906 } 907 } 908 909 /** 910 * {@inheritDoc} 911 */ 912 @Override getInstalledPackageNames()913 public Set<String> getInstalledPackageNames() throws DeviceNotAvailableException { 914 return getInstalledPackageNames(null, null); 915 } 916 917 /** {@inheritDoc} */ 918 @Override isPackageInstalled(String packageName)919 public boolean isPackageInstalled(String packageName) throws DeviceNotAvailableException { 920 return getInstalledPackageNames(packageName, null).contains(packageName); 921 } 922 923 /** {@inheritDoc} */ 924 @Override isPackageInstalled(String packageName, String userId)925 public boolean isPackageInstalled(String packageName, String userId) 926 throws DeviceNotAvailableException { 927 return getInstalledPackageNames(packageName, userId).contains(packageName); 928 } 929 930 /** {@inheritDoc} */ 931 @Override getActiveApexes()932 public Set<ApexInfo> getActiveApexes() throws DeviceNotAvailableException { 933 Set<ApexInfo> ret = new HashSet<>(); 934 String output = executeShellCommand(LIST_APEXES_CMD); 935 if (output != null) { 936 Matcher m = APEXES_REGEX.matcher(output); 937 while (m.find()) { 938 String name = m.group(1); 939 long version = Long.valueOf(m.group(2)); 940 ret.add(new ApexInfo(name, version)); 941 } 942 } 943 return ret; 944 } 945 946 /** 947 * A {@link com.android.tradefed.device.NativeDevice.DeviceAction} 948 * for retrieving package system service info, and do retries on 949 * failures. 950 */ 951 private class DumpPkgAction implements DeviceAction { 952 953 Map<String, PackageInfo> mPkgInfoMap; 954 DumpPkgAction()955 DumpPkgAction() { 956 } 957 958 @Override run()959 public boolean run() throws IOException, TimeoutException, AdbCommandRejectedException, 960 ShellCommandUnresponsiveException, InstallException, SyncException { 961 DumpsysPackageReceiver receiver = new DumpsysPackageReceiver(); 962 getIDevice().executeShellCommand("dumpsys package p", receiver); 963 mPkgInfoMap = receiver.getPackages(); 964 if (mPkgInfoMap.size() == 0) { 965 // Package parsing can fail if package manager is currently down. throw exception 966 // to retry 967 CLog.w("no packages found from dumpsys package p."); 968 throw new IOException(); 969 } 970 return true; 971 } 972 } 973 974 /** 975 * {@inheritDoc} 976 */ 977 @Override getUninstallablePackageNames()978 public Set<String> getUninstallablePackageNames() throws DeviceNotAvailableException { 979 DumpPkgAction action = new DumpPkgAction(); 980 performDeviceAction("dumpsys package p", action, MAX_RETRY_ATTEMPTS); 981 982 Set<String> pkgs = new HashSet<String>(); 983 for (PackageInfo pkgInfo : action.mPkgInfoMap.values()) { 984 if (!pkgInfo.isSystemApp() || pkgInfo.isUpdatedSystemApp()) { 985 CLog.d("Found uninstallable package %s", pkgInfo.getPackageName()); 986 pkgs.add(pkgInfo.getPackageName()); 987 } 988 } 989 return pkgs; 990 } 991 992 /** 993 * {@inheritDoc} 994 */ 995 @Override getAppPackageInfo(String packageName)996 public PackageInfo getAppPackageInfo(String packageName) throws DeviceNotAvailableException { 997 DumpPkgAction action = new DumpPkgAction(); 998 performDeviceAction("dumpsys package", action, MAX_RETRY_ATTEMPTS); 999 return action.mPkgInfoMap.get(packageName); 1000 } 1001 1002 // TODO: convert this to use DumpPkgAction getInstalledPackageNames(String packageNameSearched, String userId)1003 private Set<String> getInstalledPackageNames(String packageNameSearched, String userId) 1004 throws DeviceNotAvailableException { 1005 Set<String> packages= new HashSet<String>(); 1006 String command = LIST_PACKAGES_CMD; 1007 if (userId != null) { 1008 command += String.format(" --user %s", userId); 1009 } 1010 if (packageNameSearched != null) { 1011 command += (" | grep " + packageNameSearched); 1012 } 1013 String output = executeShellCommand(command); 1014 if (output != null) { 1015 Matcher m = PACKAGE_REGEX.matcher(output); 1016 while (m.find()) { 1017 String packageName = m.group(2); 1018 if (packageNameSearched != null && packageName.equals(packageNameSearched)) { 1019 packages.add(packageName); 1020 } else if (packageNameSearched == null) { 1021 packages.add(packageName); 1022 } 1023 } 1024 } 1025 return packages; 1026 } 1027 1028 /** 1029 * {@inheritDoc} 1030 */ 1031 @Override listUsers()1032 public ArrayList<Integer> listUsers() throws DeviceNotAvailableException { 1033 ArrayList<String[]> users = tokenizeListUsers(); 1034 ArrayList<Integer> userIds = new ArrayList<Integer>(users.size()); 1035 for (String[] user : users) { 1036 userIds.add(Integer.parseInt(user[1])); 1037 } 1038 return userIds; 1039 } 1040 1041 /** 1042 * Tokenizes the output of 'pm list users'. 1043 * The returned tokens for each user have the form: {"\tUserInfo", Integer.toString(id), name, 1044 * Integer.toHexString(flag), "[running]"}; (the last one being optional) 1045 * @return a list of arrays of strings, each element of the list representing the tokens 1046 * for a user, or {@code null} if there was an error while tokenizing the adb command output. 1047 */ tokenizeListUsers()1048 private ArrayList<String[]> tokenizeListUsers() throws DeviceNotAvailableException { 1049 String command = "pm list users"; 1050 String commandOutput = executeShellCommand(command); 1051 // Extract the id of all existing users. 1052 String[] lines = commandOutput.split("\\r?\\n"); 1053 if (!lines[0].equals("Users:")) { 1054 throw new DeviceRuntimeException( 1055 String.format("'%s' in not a valid output for 'pm list users'", commandOutput)); 1056 } 1057 ArrayList<String[]> users = new ArrayList<String[]>(lines.length - 1); 1058 for (int i = 1; i < lines.length; i++) { 1059 // Individual user is printed out like this: 1060 // \tUserInfo{$id$:$name$:$Integer.toHexString(flags)$} [running] 1061 String[] tokens = lines[i].split("\\{|\\}|:"); 1062 if (tokens.length != 4 && tokens.length != 5) { 1063 throw new DeviceRuntimeException( 1064 String.format( 1065 "device output: '%s' \nline: '%s' was not in the expected " 1066 + "format for user info.", 1067 commandOutput, lines[i])); 1068 } 1069 users.add(tokens); 1070 } 1071 return users; 1072 } 1073 1074 /** 1075 * {@inheritDoc} 1076 */ 1077 @Override getMaxNumberOfUsersSupported()1078 public int getMaxNumberOfUsersSupported() throws DeviceNotAvailableException { 1079 String command = "pm get-max-users"; 1080 String commandOutput = executeShellCommand(command); 1081 try { 1082 return Integer.parseInt(commandOutput.substring(commandOutput.lastIndexOf(" ")).trim()); 1083 } catch (NumberFormatException e) { 1084 CLog.e("Failed to parse result: %s", commandOutput); 1085 } 1086 return 0; 1087 } 1088 1089 /** {@inheritDoc} */ 1090 @Override getMaxNumberOfRunningUsersSupported()1091 public int getMaxNumberOfRunningUsersSupported() throws DeviceNotAvailableException { 1092 checkApiLevelAgainstNextRelease("get-max-running-users", 28); 1093 String command = "pm get-max-running-users"; 1094 String commandOutput = executeShellCommand(command); 1095 try { 1096 return Integer.parseInt(commandOutput.substring(commandOutput.lastIndexOf(" ")).trim()); 1097 } catch (NumberFormatException e) { 1098 CLog.e("Failed to parse result: %s", commandOutput); 1099 } 1100 return 0; 1101 } 1102 1103 /** 1104 * {@inheritDoc} 1105 */ 1106 @Override isMultiUserSupported()1107 public boolean isMultiUserSupported() throws DeviceNotAvailableException { 1108 return getMaxNumberOfUsersSupported() > 1; 1109 } 1110 1111 /** 1112 * {@inheritDoc} 1113 */ 1114 @Override createUser(String name)1115 public int createUser(String name) throws DeviceNotAvailableException, IllegalStateException { 1116 return createUser(name, false, false); 1117 } 1118 1119 /** {@inheritDoc} */ 1120 @Override createUserNoThrow(String name)1121 public int createUserNoThrow(String name) throws DeviceNotAvailableException { 1122 try { 1123 return createUser(name); 1124 } catch (IllegalStateException e) { 1125 CLog.e("Error creating user: " + e.toString()); 1126 return -1; 1127 } 1128 } 1129 1130 /** 1131 * {@inheritDoc} 1132 */ 1133 @Override createUser(String name, boolean guest, boolean ephemeral)1134 public int createUser(String name, boolean guest, boolean ephemeral) 1135 throws DeviceNotAvailableException, IllegalStateException { 1136 String command ="pm create-user " + (guest ? "--guest " : "") 1137 + (ephemeral ? "--ephemeral " : "") + name; 1138 final String output = executeShellCommand(command); 1139 if (output.startsWith("Success")) { 1140 try { 1141 resetContentProviderSetup(); 1142 return Integer.parseInt(output.substring(output.lastIndexOf(" ")).trim()); 1143 } catch (NumberFormatException e) { 1144 CLog.e("Failed to parse result: %s", output); 1145 } 1146 } 1147 throw new IllegalStateException(String.format("Failed to create user: %s", output)); 1148 } 1149 1150 /** 1151 * {@inheritDoc} 1152 */ 1153 @Override removeUser(int userId)1154 public boolean removeUser(int userId) throws DeviceNotAvailableException { 1155 final String output = executeShellCommand(String.format("pm remove-user %s", userId)); 1156 if (output.startsWith("Error")) { 1157 CLog.e("Failed to remove user: %s", output); 1158 return false; 1159 } 1160 return true; 1161 } 1162 1163 /** 1164 * {@inheritDoc} 1165 */ 1166 @Override startUser(int userId)1167 public boolean startUser(int userId) throws DeviceNotAvailableException { 1168 return startUser(userId, false); 1169 } 1170 1171 /** {@inheritDoc} */ 1172 @Override startUser(int userId, boolean waitFlag)1173 public boolean startUser(int userId, boolean waitFlag) throws DeviceNotAvailableException { 1174 if (waitFlag) { 1175 checkApiLevelAgainstNextRelease("start-user -w", 29); 1176 } 1177 String cmd = "am start-user " + (waitFlag ? "-w " : "") + userId; 1178 1179 CLog.d("Starting user with command: %s", cmd); 1180 final String output = executeShellCommand(cmd); 1181 if (output.startsWith("Error")) { 1182 CLog.e("Failed to start user: %s", output); 1183 return false; 1184 } 1185 if (waitFlag) { 1186 String state = executeShellCommand("am get-started-user-state " + userId); 1187 if (!state.contains("RUNNING_UNLOCKED")) { 1188 CLog.w("User %s is not RUNNING_UNLOCKED after start-user -w. (%s).", userId, state); 1189 return false; 1190 } 1191 } 1192 return true; 1193 } 1194 1195 /** 1196 * {@inheritDoc} 1197 */ 1198 @Override stopUser(int userId)1199 public boolean stopUser(int userId) throws DeviceNotAvailableException { 1200 // No error or status code is returned. 1201 return stopUser(userId, false, false); 1202 } 1203 1204 /** 1205 * {@inheritDoc} 1206 */ 1207 @Override stopUser(int userId, boolean waitFlag, boolean forceFlag)1208 public boolean stopUser(int userId, boolean waitFlag, boolean forceFlag) 1209 throws DeviceNotAvailableException { 1210 final int apiLevel = getApiLevel(); 1211 if (waitFlag && apiLevel < 23) { 1212 throw new IllegalArgumentException("stop-user -w requires API level >= 23"); 1213 } 1214 if (forceFlag && apiLevel < 24) { 1215 throw new IllegalArgumentException("stop-user -f requires API level >= 24"); 1216 } 1217 StringBuilder cmd = new StringBuilder("am stop-user "); 1218 if (waitFlag) { 1219 cmd.append("-w "); 1220 } 1221 if (forceFlag) { 1222 cmd.append("-f "); 1223 } 1224 cmd.append(userId); 1225 1226 CLog.d("stopping user with command: %s", cmd.toString()); 1227 final String output = executeShellCommand(cmd.toString()); 1228 if (output.contains("Error: Can't stop system user")) { 1229 CLog.e("Cannot stop System user."); 1230 return false; 1231 } 1232 if (output.contains("Can't stop current user")) { 1233 CLog.e("Cannot stop current user."); 1234 return false; 1235 } 1236 if (isUserRunning(userId)) { 1237 CLog.w("User Id: %s is still running after the stop-user command.", userId); 1238 return false; 1239 } 1240 return true; 1241 } 1242 1243 /** 1244 * {@inheritDoc} 1245 */ 1246 @Override getPrimaryUserId()1247 public Integer getPrimaryUserId() throws DeviceNotAvailableException { 1248 ArrayList<String[]> users = tokenizeListUsers(); 1249 for (String[] user : users) { 1250 int flag = Integer.parseInt(user[3], 16); 1251 if ((flag & FLAG_PRIMARY) != 0) { 1252 return Integer.parseInt(user[1]); 1253 } 1254 } 1255 return null; 1256 } 1257 1258 /** {@inheritDoc} */ 1259 @Override getCurrentUser()1260 public int getCurrentUser() throws DeviceNotAvailableException, DeviceRuntimeException { 1261 checkApiLevelAgainstNextRelease("get-current-user", API_LEVEL_GET_CURRENT_USER); 1262 final String output = executeShellCommand("am get-current-user"); 1263 try { 1264 int userId = Integer.parseInt(output.trim()); 1265 if (userId < 0) { 1266 throw new DeviceRuntimeException( 1267 String.format( 1268 "Invalid user id '%s' was returned for get-current-user", userId)); 1269 } 1270 return userId; 1271 } catch (NumberFormatException e) { 1272 throw new DeviceRuntimeException(e); 1273 } 1274 } 1275 findUserInfo(String pmListUsersOutput)1276 private Matcher findUserInfo(String pmListUsersOutput) { 1277 Pattern pattern = Pattern.compile(USER_PATTERN); 1278 Matcher matcher = pattern.matcher(pmListUsersOutput); 1279 return matcher; 1280 } 1281 1282 /** 1283 * {@inheritDoc} 1284 */ 1285 @Override getUserFlags(int userId)1286 public int getUserFlags(int userId) throws DeviceNotAvailableException { 1287 checkApiLevelAgainst("getUserFlags", 22); 1288 final String commandOutput = executeShellCommand("pm list users"); 1289 Matcher matcher = findUserInfo(commandOutput); 1290 while(matcher.find()) { 1291 if (Integer.parseInt(matcher.group(2)) == userId) { 1292 return Integer.parseInt(matcher.group(6), 16); 1293 } 1294 } 1295 CLog.w("Could not find any flags for userId: %d in output: %s", userId, commandOutput); 1296 return INVALID_USER_ID; 1297 } 1298 1299 /** {@inheritDoc} */ 1300 @Override isUserSecondary(int userId)1301 public boolean isUserSecondary(int userId) throws DeviceNotAvailableException { 1302 if (userId == UserUtil.USER_SYSTEM) { 1303 return false; 1304 } 1305 int flags = getUserFlags(userId); 1306 if (flags == INVALID_USER_ID) { 1307 return false; 1308 } 1309 return (flags & UserUtil.FLAGS_NOT_SECONDARY) == 0; 1310 } 1311 1312 /** 1313 * {@inheritDoc} 1314 */ 1315 @Override isUserRunning(int userId)1316 public boolean isUserRunning(int userId) throws DeviceNotAvailableException { 1317 checkApiLevelAgainst("isUserIdRunning", 22); 1318 final String commandOutput = executeShellCommand("pm list users"); 1319 Matcher matcher = findUserInfo(commandOutput); 1320 while(matcher.find()) { 1321 if (Integer.parseInt(matcher.group(2)) == userId) { 1322 if (matcher.group(7).contains("running")) { 1323 return true; 1324 } 1325 } 1326 } 1327 return false; 1328 } 1329 1330 /** 1331 * {@inheritDoc} 1332 */ 1333 @Override getUserSerialNumber(int userId)1334 public int getUserSerialNumber(int userId) throws DeviceNotAvailableException { 1335 checkApiLevelAgainst("getUserSerialNumber", 22); 1336 final String commandOutput = executeShellCommand("dumpsys user"); 1337 // example: UserInfo{0:Test:13} serialNo=0 1338 String userSerialPatter = "(.*\\{)(\\d+)(.*\\})(.*=)(\\d+)"; 1339 Pattern pattern = Pattern.compile(userSerialPatter); 1340 Matcher matcher = pattern.matcher(commandOutput); 1341 while(matcher.find()) { 1342 if (Integer.parseInt(matcher.group(2)) == userId) { 1343 return Integer.parseInt(matcher.group(5)); 1344 } 1345 } 1346 CLog.w("Could not find user serial number for userId: %d, in output: %s", 1347 userId, commandOutput); 1348 return INVALID_USER_ID; 1349 } 1350 1351 /** 1352 * {@inheritDoc} 1353 */ 1354 @Override switchUser(int userId)1355 public boolean switchUser(int userId) throws DeviceNotAvailableException { 1356 return switchUser(userId, AM_COMMAND_TIMEOUT); 1357 } 1358 1359 /** 1360 * {@inheritDoc} 1361 */ 1362 @Override switchUser(int userId, long timeout)1363 public boolean switchUser(int userId, long timeout) throws DeviceNotAvailableException { 1364 checkApiLevelAgainstNextRelease("switchUser", API_LEVEL_GET_CURRENT_USER); 1365 if (userId == getCurrentUser()) { 1366 CLog.w("Already running as user id: %s. Nothing to be done.", userId); 1367 return true; 1368 } 1369 resetContentProviderSetup(); 1370 executeShellCommand(String.format("am switch-user %d", userId)); 1371 long initialTime = getHostCurrentTime(); 1372 while (getHostCurrentTime() - initialTime <= timeout) { 1373 if (userId == getCurrentUser()) { 1374 // disable keyguard if option is true 1375 prePostBootSetup(); 1376 return true; 1377 } else { 1378 RunUtil.getDefault().sleep(getCheckNewUserSleep()); 1379 } 1380 } 1381 CLog.e("User did not switch in the given %d timeout", timeout); 1382 return false; 1383 } 1384 1385 /** 1386 * Exposed for testing. 1387 */ getCheckNewUserSleep()1388 protected long getCheckNewUserSleep() { 1389 return CHECK_NEW_USER; 1390 } 1391 1392 /** 1393 * Exposed for testing 1394 */ getHostCurrentTime()1395 protected long getHostCurrentTime() { 1396 return System.currentTimeMillis(); 1397 } 1398 1399 /** 1400 * {@inheritDoc} 1401 */ 1402 @Override hasFeature(String feature)1403 public boolean hasFeature(String feature) throws DeviceNotAvailableException { 1404 final String output = executeShellCommand("pm list features"); 1405 if (output.contains(feature)) { 1406 return true; 1407 } 1408 CLog.w("Feature: %s is not available on %s", feature, getSerialNumber()); 1409 return false; 1410 } 1411 1412 /** 1413 * {@inheritDoc} 1414 */ 1415 @Override getSetting(String namespace, String key)1416 public String getSetting(String namespace, String key) throws DeviceNotAvailableException { 1417 return getSettingInternal("", namespace.trim(), key.trim()); 1418 } 1419 1420 /** 1421 * {@inheritDoc} 1422 */ 1423 @Override getSetting(int userId, String namespace, String key)1424 public String getSetting(int userId, String namespace, String key) 1425 throws DeviceNotAvailableException { 1426 return getSettingInternal(String.format("--user %d", userId), namespace.trim(), key.trim()); 1427 } 1428 1429 /** 1430 * Internal Helper to get setting with or without a userId provided. 1431 */ getSettingInternal(String userFlag, String namespace, String key)1432 private String getSettingInternal(String userFlag, String namespace, String key) 1433 throws DeviceNotAvailableException { 1434 namespace = namespace.toLowerCase(); 1435 if (Arrays.asList(SETTINGS_NAMESPACE).contains(namespace)) { 1436 String cmd = String.format("settings %s get %s %s", userFlag, namespace, key); 1437 String output = executeShellCommand(cmd); 1438 if ("null".equals(output)) { 1439 CLog.w("settings returned null for command: %s. " 1440 + "please check if the namespace:key exists", cmd); 1441 return null; 1442 } 1443 return output.trim(); 1444 } 1445 CLog.e("Namespace requested: '%s' is not part of {system, secure, global}", namespace); 1446 return null; 1447 } 1448 1449 /** {@inheritDoc} */ 1450 @Override getAllSettings(String namespace)1451 public Map<String, String> getAllSettings(String namespace) throws DeviceNotAvailableException { 1452 return getAllSettingsInternal(namespace.trim()); 1453 } 1454 1455 /** Internal helper to get all settings */ getAllSettingsInternal(String namespace)1456 private Map<String, String> getAllSettingsInternal(String namespace) 1457 throws DeviceNotAvailableException { 1458 namespace = namespace.toLowerCase(); 1459 if (Arrays.asList(SETTINGS_NAMESPACE).contains(namespace)) { 1460 Map<String, String> map = new HashMap<>(); 1461 String cmd = String.format("settings list %s", namespace); 1462 String output = executeShellCommand(cmd); 1463 for (String line : output.split("\\n")) { 1464 // Setting's value could be empty 1465 String[] pair = line.trim().split("=", -1); 1466 if (pair.length > 1) { 1467 map.putIfAbsent(pair[0], pair[1]); 1468 } else { 1469 CLog.e("Unable to get setting from string: %s", line); 1470 } 1471 } 1472 return map; 1473 } 1474 CLog.e("Namespace requested: '%s' is not part of {system, secure, global}", namespace); 1475 return null; 1476 } 1477 1478 /** 1479 * {@inheritDoc} 1480 */ 1481 @Override setSetting(String namespace, String key, String value)1482 public void setSetting(String namespace, String key, String value) 1483 throws DeviceNotAvailableException { 1484 setSettingInternal("", namespace.trim(), key.trim(), value.trim()); 1485 } 1486 1487 /** 1488 * {@inheritDoc} 1489 */ 1490 @Override setSetting(int userId, String namespace, String key, String value)1491 public void setSetting(int userId, String namespace, String key, String value) 1492 throws DeviceNotAvailableException { 1493 setSettingInternal(String.format("--user %d", userId), namespace.trim(), key.trim(), 1494 value.trim()); 1495 } 1496 1497 /** 1498 * Internal helper to set a setting with or without a userId provided. 1499 */ setSettingInternal(String userFlag, String namespace, String key, String value)1500 private void setSettingInternal(String userFlag, String namespace, String key, String value) 1501 throws DeviceNotAvailableException { 1502 checkApiLevelAgainst("Changing settings", 22); 1503 if (Arrays.asList(SETTINGS_NAMESPACE).contains(namespace.toLowerCase())) { 1504 executeShellCommand(String.format("settings %s put %s %s %s", 1505 userFlag, namespace, key, value)); 1506 } else { 1507 throw new IllegalArgumentException("Namespace must be one of system, secure, global." 1508 + " You provided: " + namespace); 1509 } 1510 } 1511 1512 /** 1513 * {@inheritDoc} 1514 */ 1515 @Override getAndroidId(int userId)1516 public String getAndroidId(int userId) throws DeviceNotAvailableException { 1517 if (isAdbRoot()) { 1518 String cmd = String.format( 1519 "sqlite3 /data/user/%d/com.google.android.gsf/databases/gservices.db " 1520 + "'select value from main where name = \"android_id\"'", userId); 1521 String output = executeShellCommand(cmd).trim(); 1522 if (!output.contains("unable to open database")) { 1523 return output; 1524 } 1525 CLog.w("Couldn't find android-id, output: %s", output); 1526 } else { 1527 CLog.w("adb root is required."); 1528 } 1529 return null; 1530 } 1531 1532 /** 1533 * {@inheritDoc} 1534 */ 1535 @Override getAndroidIds()1536 public Map<Integer, String> getAndroidIds() throws DeviceNotAvailableException { 1537 ArrayList<Integer> userIds = listUsers(); 1538 if (userIds == null) { 1539 return null; 1540 } 1541 Map<Integer, String> androidIds = new HashMap<Integer, String>(); 1542 for (Integer id : userIds) { 1543 String androidId = getAndroidId(id); 1544 androidIds.put(id, androidId); 1545 } 1546 return androidIds; 1547 } 1548 1549 /** 1550 * {@inheritDoc} 1551 */ 1552 @Override createWifiHelper()1553 IWifiHelper createWifiHelper() throws DeviceNotAvailableException { 1554 mWasWifiHelperInstalled = true; 1555 return new WifiHelper(this, mOptions.getWifiUtilAPKPath()); 1556 } 1557 1558 /** 1559 * Alternative to {@link #createWifiHelper()} where we can choose whether to do the wifi helper 1560 * setup or not. 1561 */ 1562 @VisibleForTesting createWifiHelper(boolean doSetup)1563 IWifiHelper createWifiHelper(boolean doSetup) throws DeviceNotAvailableException { 1564 if (doSetup) { 1565 mWasWifiHelperInstalled = true; 1566 } 1567 return new WifiHelper(this, mOptions.getWifiUtilAPKPath(), doSetup); 1568 } 1569 1570 /** {@inheritDoc} */ 1571 @Override postInvocationTearDown()1572 public void postInvocationTearDown() { 1573 super.postInvocationTearDown(); 1574 // If wifi was installed and it's a real device, attempt to clean it. 1575 if (mWasWifiHelperInstalled) { 1576 mWasWifiHelperInstalled = false; 1577 if (getIDevice() instanceof StubDevice) { 1578 return; 1579 } 1580 if (!TestDeviceState.ONLINE.equals(getDeviceState())) { 1581 return; 1582 } 1583 try { 1584 // Uninstall the wifi utility if it was installed. 1585 IWifiHelper wifi = createWifiHelper(false); 1586 wifi.cleanUp(); 1587 } catch (DeviceNotAvailableException e) { 1588 CLog.e("Device became unavailable while uninstalling wifi util."); 1589 CLog.e(e); 1590 } 1591 } 1592 } 1593 1594 /** {@inheritDoc} */ 1595 @Override setDeviceOwner(String componentName, int userId)1596 public boolean setDeviceOwner(String componentName, int userId) 1597 throws DeviceNotAvailableException { 1598 final String command = "dpm set-device-owner --user " + userId + " '" + componentName + "'"; 1599 final String commandOutput = executeShellCommand(command); 1600 return commandOutput.startsWith("Success:"); 1601 } 1602 1603 /** {@inheritDoc} */ 1604 @Override removeAdmin(String componentName, int userId)1605 public boolean removeAdmin(String componentName, int userId) 1606 throws DeviceNotAvailableException { 1607 final String command = 1608 "dpm remove-active-admin --user " + userId + " '" + componentName + "'"; 1609 final String commandOutput = executeShellCommand(command); 1610 return commandOutput.startsWith("Success:"); 1611 } 1612 1613 /** {@inheritDoc} */ 1614 @Override removeOwners()1615 public void removeOwners() throws DeviceNotAvailableException { 1616 String command = "dumpsys device_policy"; 1617 String commandOutput = executeShellCommand(command); 1618 String[] lines = commandOutput.split("\\r?\\n"); 1619 for (int i = 0; i < lines.length; ++i) { 1620 String line = lines[i].trim(); 1621 if (line.contains("Profile Owner")) { 1622 // Line is "Profile owner (User <id>): 1623 String[] tokens = line.split("\\(|\\)| "); 1624 int userId = Integer.parseInt(tokens[4]); 1625 1626 i = moveToNextIndexMatchingRegex(".*admin=.*", lines, i); 1627 line = lines[i].trim(); 1628 // Line is admin=ComponentInfo{<component>} 1629 tokens = line.split("\\{|\\}"); 1630 String componentName = tokens[1]; 1631 CLog.d("Cleaning up profile owner " + userId + " " + componentName); 1632 removeAdmin(componentName, userId); 1633 } else if (line.contains("Device Owner:")) { 1634 i = moveToNextIndexMatchingRegex(".*admin=.*", lines, i); 1635 line = lines[i].trim(); 1636 // Line is admin=ComponentInfo{<component>} 1637 String[] tokens = line.split("\\{|\\}"); 1638 String componentName = tokens[1]; 1639 1640 // Skip to user id line. 1641 i = moveToNextIndexMatchingRegex(".*User ID:.*", lines, i); 1642 line = lines[i].trim(); 1643 // Line is User ID: <N> 1644 tokens = line.split(":"); 1645 int userId = Integer.parseInt(tokens[1].trim()); 1646 CLog.d("Cleaning up device owner " + userId + " " + componentName); 1647 removeAdmin(componentName, userId); 1648 } 1649 } 1650 } 1651 1652 /** 1653 * Search forward from the current index to find a string matching the given regex. 1654 * 1655 * @param regex The regex to match each line against. 1656 * @param lines An array of strings to be searched. 1657 * @param currentIndex the index to start searching from. 1658 * @return The index of a string beginning with the regex. 1659 * @throws IllegalStateException if the line cannot be found. 1660 */ moveToNextIndexMatchingRegex(String regex, String[] lines, int currentIndex)1661 private int moveToNextIndexMatchingRegex(String regex, String[] lines, int currentIndex) { 1662 while (currentIndex < lines.length && !lines[currentIndex].matches(regex)) { 1663 currentIndex++; 1664 } 1665 1666 if (currentIndex >= lines.length) { 1667 throw new IllegalStateException( 1668 "The output of 'dumpsys device_policy' was not as expected. Owners have not " 1669 + "been removed. This will leave the device in an unstable state and " 1670 + "will lead to further test failures."); 1671 } 1672 1673 return currentIndex; 1674 } 1675 1676 /** 1677 * Helper for Api level checking of features in the new release before we incremented the api 1678 * number. 1679 */ checkApiLevelAgainstNextRelease(String feature, int strictMinLevel)1680 private void checkApiLevelAgainstNextRelease(String feature, int strictMinLevel) 1681 throws DeviceNotAvailableException { 1682 if (checkApiLevelAgainstNextRelease(strictMinLevel)) { 1683 return; 1684 } 1685 throw new IllegalArgumentException( 1686 String.format( 1687 "%s not supported on %s. Must be API %d.", 1688 feature, getSerialNumber(), strictMinLevel)); 1689 } 1690 1691 @Override dumpHeap(String process, String devicePath)1692 public File dumpHeap(String process, String devicePath) throws DeviceNotAvailableException { 1693 if (Strings.isNullOrEmpty(devicePath) || Strings.isNullOrEmpty(process)) { 1694 throw new IllegalArgumentException("devicePath or process cannot be null or empty."); 1695 } 1696 String pid = getProcessPid(process); 1697 if (pid == null) { 1698 return null; 1699 } 1700 File dump = dumpAndPullHeap(pid, devicePath); 1701 // Clean the device. 1702 deleteFile(devicePath); 1703 return dump; 1704 } 1705 1706 /** Dump the heap file and pull it from the device. */ dumpAndPullHeap(String pid, String devicePath)1707 private File dumpAndPullHeap(String pid, String devicePath) throws DeviceNotAvailableException { 1708 executeShellCommand(String.format(DUMPHEAP_CMD, pid, devicePath)); 1709 // Allow a little bit of time for the file to populate on device side. 1710 int attempt = 0; 1711 // TODO: add an API to check device file size 1712 while (!doesFileExist(devicePath) && attempt < 3) { 1713 getRunUtil().sleep(DUMPHEAP_TIME); 1714 attempt++; 1715 } 1716 File dumpFile = pullFile(devicePath); 1717 return dumpFile; 1718 } 1719 1720 /** {@inheritDoc} */ 1721 @Override listDisplayIds()1722 public Set<Integer> listDisplayIds() throws DeviceNotAvailableException { 1723 Set<Integer> displays = new HashSet<>(); 1724 // Zero is the default display 1725 displays.add(0); 1726 CommandResult res = executeShellV2Command("dumpsys SurfaceFlinger | grep 'color modes:'"); 1727 if (!CommandStatus.SUCCESS.equals(res.getStatus())) { 1728 CLog.e("Something went wrong while listing displays: %s", res.getStderr()); 1729 return displays; 1730 } 1731 String output = res.getStdout(); 1732 Pattern p = Pattern.compile(DISPLAY_ID_PATTERN); 1733 for (String line : output.split("\n")) { 1734 Matcher m = p.matcher(line); 1735 if (m.matches()) { 1736 displays.add(Integer.parseInt(m.group("id"))); 1737 } 1738 } 1739 return displays; 1740 } 1741 } 1742