1 /* 2 * Copyright (C) 2011 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.targetprep; 17 18 import static com.android.tradefed.targetprep.UserHelper.RUN_TESTS_AS_USER_KEY; 19 20 import com.android.annotations.VisibleForTesting; 21 import com.android.incfs.install.IncrementalInstallSession; 22 import com.android.incfs.install.IncrementalInstallSession.Builder; 23 import com.android.incfs.install.PendingBlock; 24 import com.android.incfs.install.adb.ddmlib.DeviceConnection; 25 import com.android.incfs.install.adb.ddmlib.DeviceLogger; 26 import com.android.tradefed.build.IBuildInfo; 27 import com.android.tradefed.build.IDeviceBuildInfo; 28 import com.android.tradefed.config.Option; 29 import com.android.tradefed.config.Option.Importance; 30 import com.android.tradefed.config.OptionClass; 31 import com.android.tradefed.device.DeviceNotAvailableException; 32 import com.android.tradefed.device.ITestDevice; 33 import com.android.tradefed.device.NativeDevice; 34 import com.android.tradefed.invoker.IInvocationContext; 35 import com.android.tradefed.invoker.InvocationContext; 36 import com.android.tradefed.invoker.TestInformation; 37 import com.android.tradefed.log.LogUtil.CLog; 38 import com.android.tradefed.observatory.IDiscoverDependencies; 39 import com.android.tradefed.result.error.DeviceErrorIdentifier; 40 import com.android.tradefed.result.error.InfraErrorIdentifier; 41 import com.android.tradefed.testtype.IAbi; 42 import com.android.tradefed.testtype.IAbiReceiver; 43 import com.android.tradefed.util.AaptParser; 44 import com.android.tradefed.util.AaptParser.AaptVersion; 45 import com.android.tradefed.util.AbiFormatter; 46 import com.android.tradefed.util.BuildTestsZipUtils; 47 import com.android.utils.StdLogger; 48 49 import com.google.common.collect.ImmutableList; 50 import com.google.common.collect.ImmutableListMultimap; 51 import com.google.common.collect.Multimaps; 52 53 import java.io.File; 54 import java.io.IOException; 55 import java.nio.file.Files; 56 import java.nio.file.Path; 57 import java.nio.file.Paths; 58 import java.security.SecureRandom; 59 import java.util.ArrayList; 60 import java.util.Arrays; 61 import java.util.Collection; 62 import java.util.HashMap; 63 import java.util.HashSet; 64 import java.util.LinkedHashMap; 65 import java.util.List; 66 import java.util.Map; 67 import java.util.Random; 68 import java.util.Set; 69 import java.util.concurrent.Executors; 70 import java.util.concurrent.TimeUnit; 71 import java.util.stream.Collectors; 72 import java.util.stream.Stream; 73 74 /** 75 * A {@link ITargetPreparer} that installs one or more apps from a {@link 76 * IDeviceBuildInfo#getTestsDir()} folder onto device. 77 * 78 * <p>This preparer will look in alternate directories if the tests zip does not exist or does not 79 * contain the required apk. The search will go in order from the last alternative dir specified to 80 * the first. 81 */ 82 @OptionClass(alias = "tests-zip-app") 83 public class TestAppInstallSetup extends BaseTargetPreparer 84 implements IAbiReceiver, IDiscoverDependencies { 85 86 /** The mode the apk should be install in. */ 87 private enum InstallMode { 88 FULL, 89 INSTANT, 90 } 91 92 // An error message that occurs when a test APK is already present on the DUT, 93 // but cannot be updated. When this occurs, the package is removed from the 94 // device so that installation can continue like normal. 95 private static final String INSTALL_FAILED_UPDATE_INCOMPATIBLE = 96 "INSTALL_FAILED_UPDATE_INCOMPATIBLE"; 97 98 @VisibleForTesting static final String TEST_FILE_NAME_OPTION = "test-file-name"; 99 100 @Option( 101 name = TEST_FILE_NAME_OPTION, 102 description = 103 "the name of an apk file to be installed on device. Can be repeated. Items " 104 + "that are directories will have any APKs contained therein, " 105 + "including subdirectories, grouped by package name and installed.", 106 importance = Importance.IF_UNSET) 107 private List<File> mTestFiles = new ArrayList<>(); 108 109 // A string made of split apk file names divided by ",". 110 // See "https://developer.android.com/studio/build/configure-apk-splits" on how to split 111 // apk to several files. 112 @Option( 113 name = "split-apk-file-names", 114 description = 115 "the split apk file names separted by comma that will be installed on device." 116 + " Can be repeated for multiple split apk sets. See" 117 + " https://developer.android.com/studio/build/configure-apk-splits on how" 118 + " to split apk to several files") 119 private List<String> mSplitApkFileNames = new ArrayList<>(); 120 121 @VisibleForTesting static final String THROW_IF_NOT_FOUND_OPTION = "throw-if-not-found"; 122 123 @Option( 124 name = THROW_IF_NOT_FOUND_OPTION, 125 description = "Throw exception if the specified file is not found.") 126 private boolean mThrowIfNoFile = true; 127 128 @Option(name = AbiFormatter.FORCE_ABI_STRING, 129 description = AbiFormatter.FORCE_ABI_DESCRIPTION, 130 importance = Importance.IF_UNSET) 131 private String mForceAbi = null; 132 133 @Option(name = "install-arg", 134 description = "Additional arguments to be passed to install command, " 135 + "including leading dash, e.g. \"-d\"") 136 private Collection<String> mInstallArgs = new ArrayList<>(); 137 138 @Option( 139 name = "force-queryable", 140 description = "Whether apks should be installed as force queryable.") 141 private Boolean mForceQueryable = null; 142 143 @Option( 144 name = "cleanup-apks", 145 description = 146 "Whether apks installed should be uninstalled after test. Note that the " 147 + "preparer does not verify if the apks are successfully removed.") 148 private boolean mCleanup = true; 149 150 @VisibleForTesting static final String CHECK_MIN_SDK_OPTION = "check-min-sdk"; 151 152 @Option( 153 name = CHECK_MIN_SDK_OPTION, 154 description = 155 "check app's min sdk prior to install and skip if device api level is too low.") 156 private boolean mCheckMinSdk = false; 157 158 /** @deprecated use test-file-name instead now that it is a File. */ 159 @Deprecated 160 @Option( 161 name = "alt-dir", 162 description = 163 "Alternate directory to look for the apk if the apk is not in the tests " 164 + "zip file. For each alternate dir, will look in //, //data/app, " 165 + "//DATA/app, //DATA/app/apk_name/ and //DATA/priv-app/apk_name/. " 166 + "Can be repeated. Look for apks in last alt-dir first.") 167 private List<File> mAltDirs = new ArrayList<>(); 168 169 /** @deprecated goes in pair with alt-dir which is deprecated */ 170 @Deprecated 171 @Option( 172 name = "alt-dir-behavior", 173 description = 174 "The order of alternate directory to be used when searching for apks to " 175 + "install") 176 private AltDirBehavior mAltDirBehavior = AltDirBehavior.FALLBACK; 177 178 @Option(name = "instant-mode", description = "Whether or not to install apk in instant mode.") 179 private boolean mInstantMode = false; 180 181 @Option(name = "aapt-version", description = "The version of AAPT for APK parsing.") 182 private AaptVersion mAaptVersion = AaptVersion.AAPT2; 183 184 @Option( 185 name = "force-install-mode", 186 description = 187 "Force the preparer to ignore instant-mode option, and install in the" 188 + " requested mode.") 189 private InstallMode mInstallationMode = null; 190 191 @Option( 192 name = "incremental", 193 description = 194 "Performs an installation using incremental streaming. Given the" 195 + " non-deterministic nature of an incremental installation, it is not" 196 + " guaranteed that a test run with this option will yield the same" 197 + " results of previous or future invocations.") 198 @VisibleForTesting 199 protected boolean mIncrementalInstallation = false; 200 201 @Option( 202 name = "incremental-block-filter", 203 description = 204 "Decimal representation of the percentage of data blocks" 205 + " to be filtered out during an incremental" 206 + " installation.") 207 protected double mBlockFilterPercentage = 0.0; 208 209 @Option( 210 name = "incremental-install-timeout-secs", 211 description = 212 "Specifies the maximum permitted duration of" + " an incremental installation.") 213 protected int mIncrementalInstallTimeout = 1800; 214 215 private IAbi mAbi = null; 216 private Integer mUserId = null; 217 private Boolean mGrantPermission = null; 218 219 private Set<String> mPackagesInstalled = new HashSet<>(); 220 private TestInformation mTestInfo; 221 @VisibleForTesting protected IncrementalInstallSession incrementalInstallSession; 222 setTestInformation(TestInformation testInfo)223 protected void setTestInformation(TestInformation testInfo) { 224 mTestInfo = testInfo; 225 } 226 227 /** Adds a file or directory to the list of apks to installed. */ addTestFile(File file)228 public void addTestFile(File file) { 229 mTestFiles.add(file); 230 } 231 232 /** Adds a file name to the list of apks to installed. */ addTestFileName(String fileName)233 public void addTestFileName(String fileName) { 234 addTestFile(new File(fileName)); 235 } 236 237 /** Helper to parse an apk file with aapt. */ 238 @VisibleForTesting doAaptParse(File apkFile)239 AaptParser doAaptParse(File apkFile) { 240 return AaptParser.parse(apkFile); 241 } 242 243 @VisibleForTesting clearTestFile()244 void clearTestFile() { 245 mTestFiles.clear(); 246 } 247 248 /** 249 * Adds a set of file names divided by ',' in a string to be installed as split apks 250 * 251 * @param fileNames a string of file names divided by ',' 252 */ addSplitApkFileNames(String fileNames)253 public void addSplitApkFileNames(String fileNames) { 254 mSplitApkFileNames.add(fileNames); 255 } 256 257 @VisibleForTesting clearSplitApkFileNames()258 void clearSplitApkFileNames() { 259 mSplitApkFileNames.clear(); 260 } 261 262 /** Returns a copy of the list of specified test apk names. */ getTestsFileName()263 public List<File> getTestsFileName() { 264 return mTestFiles; 265 } 266 267 /** Sets whether or not the installed apk should be cleaned on tearDown */ setCleanApk(boolean shouldClean)268 public void setCleanApk(boolean shouldClean) { 269 mCleanup = shouldClean; 270 } 271 272 /** 273 * If the apk should be installed for a particular user, sets the id of the user to install for. 274 */ setUserId(int userId)275 public void setUserId(int userId) { 276 mUserId = userId; 277 } 278 279 /** If a userId is provided, grantPermission can be set for the apk installation. */ setShouldGrantPermission(boolean shouldGrant)280 public void setShouldGrantPermission(boolean shouldGrant) { 281 mGrantPermission = shouldGrant; 282 } 283 284 /** Sets the version of AAPT for APK parsing. */ setAaptVersion(AaptVersion aaptVersion)285 public void setAaptVersion(AaptVersion aaptVersion) { 286 mAaptVersion = aaptVersion; 287 } 288 289 /** Adds one apk installation arg to be used. */ addInstallArg(String arg)290 public void addInstallArg(String arg) { 291 mInstallArgs.add(arg); 292 } 293 294 /** 295 * The default value of the force queryable is true. Update it to false if the apk to be 296 * installed should not be queryable. 297 */ setForceQueryable(boolean forceQueryable)298 public void setForceQueryable(boolean forceQueryable) { 299 mForceQueryable = forceQueryable; 300 } 301 302 /** 303 * Resolve the actual apk path based on testing artifact information inside build info. 304 * 305 * @param testInfo The {@link TestInformation} for the invocation. 306 * @param apkFileName filename of the apk to install 307 * @return a {@link File} representing the physical apk file on host or {@code null} if the file 308 * does not exist. 309 */ getLocalPathForFilename(TestInformation testInfo, String apkFileName)310 protected File getLocalPathForFilename(TestInformation testInfo, String apkFileName) 311 throws TargetSetupError { 312 try { 313 return BuildTestsZipUtils.getApkFile( 314 testInfo.getBuildInfo(), 315 apkFileName, 316 mAltDirs, 317 mAltDirBehavior, 318 false /* use resource as fallback */, 319 null /* device signing key */); 320 } catch (IOException ioe) { 321 throw new TargetSetupError( 322 String.format( 323 "failed to resolve apk path for apk %s in build %s", 324 apkFileName, testInfo.getBuildInfo().toString()), 325 ioe, 326 testInfo.getDevice().getDeviceDescriptor(), 327 InfraErrorIdentifier.CONFIGURED_ARTIFACT_NOT_FOUND); 328 } 329 } 330 331 /** @deprecated Temporary backward compatible callback. */ 332 @Deprecated 333 @Override setUp(ITestDevice device, IBuildInfo buildInfo)334 public void setUp(ITestDevice device, IBuildInfo buildInfo) 335 throws TargetSetupError, BuildError, DeviceNotAvailableException { 336 IInvocationContext context = new InvocationContext(); 337 context.addAllocatedDevice("device", device); 338 context.addDeviceBuildInfo("device", buildInfo); 339 TestInformation backwardCompatible = 340 TestInformation.newBuilder().setInvocationContext(context).build(); 341 setUp(backwardCompatible); 342 } 343 344 /** {@inheritDoc} */ 345 @Override setUp(TestInformation testInfo)346 public void setUp(TestInformation testInfo) 347 throws TargetSetupError, BuildError, DeviceNotAvailableException { 348 mTestInfo = testInfo; 349 if (mTestFiles.isEmpty() && mSplitApkFileNames.isEmpty()) { 350 CLog.i("No test apps to install, skipping"); 351 return; 352 } 353 // resolve abi flags 354 if (mAbi != null && mForceAbi != null) { 355 throw new IllegalStateException("cannot specify both abi flags: --abi and --force-abi"); 356 } 357 358 // We are going to need several "ro.build" props, save some time (0.4 sec) by prefetching 359 if (getDevice() instanceof NativeDevice) { 360 ((NativeDevice) getDevice()).batchPrefetchStartupBuildProps(); 361 } 362 String abiName = null; 363 if (mAbi != null) { 364 abiName = mAbi.getName(); 365 } else if (mForceAbi != null) { 366 abiName = AbiFormatter.getDefaultAbi(getDevice(), mForceAbi); 367 } 368 // Set all the extra install args outside the loop to avoid adding them several times. 369 if (abiName != null && testInfo.getDevice().getApiLevel() > 20) { 370 mInstallArgs.add(String.format("--abi %s", abiName)); 371 } 372 // Handle instant mode: if we are forced in one installation mode or not. 373 // Some preparer are locked in one installation mode or another, they ignore the 374 // 'instant-mode' option and stays in their mode. 375 if (mInstallationMode != null) { 376 if (InstallMode.INSTANT.equals(mInstallationMode)) { 377 mInstallArgs.add("--instant"); 378 } 379 } else { 380 if (mInstantMode) { 381 mInstallArgs.add("--instant"); 382 } 383 } 384 385 if (mUserId == null && testInfo.properties().get(RUN_TESTS_AS_USER_KEY) != null) { 386 mUserId = Integer.parseInt(testInfo.properties().get(RUN_TESTS_AS_USER_KEY)); 387 if (!testInfo.getDevice().getUserInfos().containsKey(mUserId)) { 388 CLog.w("User requested: %s doesn't exist on device. Ignoring it.", mUserId); 389 mUserId = null; 390 } else { 391 CLog.d("Using user %s from testInfo properties.", mUserId); 392 } 393 } 394 395 if (mForceQueryable == null) { 396 // Do not add --force-queryable if the device api level >= 34. Ideally, 397 // checkApiLevelAgainstNextRelease(34) should only return true for api 34 devices. But, 398 // it also returns true for branches like the tm-xx-plus-aosp. Adding another condition 399 // ro.build.id==TM to handle this special case. 400 mForceQueryable = 401 !getDevice().checkApiLevelAgainstNextRelease(34) 402 || "TM".equals(getDevice().getBuildAlias()); 403 } 404 if (mForceQueryable && getDevice().isAppEnumerationSupported()) { 405 mInstallArgs.add("--force-queryable"); 406 } 407 408 // Add bypass flag for low target sdk apps when installing on U+ devices 409 if (getDevice().isBypassLowTargetSdkBlockSupported()) { 410 mInstallArgs.add("--bypass-low-target-sdk-block"); 411 } 412 413 for (File testAppName : mTestFiles) { 414 Map<File, String> appFilesAndPackages = 415 resolveApkFiles(testInfo, findApkFiles(testAppName)); 416 installer(testInfo, appFilesAndPackages); 417 } 418 419 for (String testAppNames : mSplitApkFileNames) { 420 List<String> apkNames = Arrays.asList(testAppNames.split(",")); 421 List<File> apkFileNames = 422 apkNames.stream().map(a -> new File(a)).collect(Collectors.toList()); 423 Map<File, String> appFilesAndPackages = resolveApkFiles(testInfo, apkFileNames); 424 installer(testInfo, appFilesAndPackages); 425 } 426 } 427 428 /** 429 * Returns the device that the preparer should apply to. 430 * 431 * @throws TargetSetupError 432 */ getDevice()433 public ITestDevice getDevice() throws TargetSetupError { 434 return mTestInfo.getDevice(); 435 } 436 getTestInfo()437 public TestInformation getTestInfo() { 438 return mTestInfo; 439 } 440 441 @Override setAbi(IAbi abi)442 public void setAbi(IAbi abi) { 443 mAbi = abi; 444 } 445 446 @Override getAbi()447 public IAbi getAbi() { 448 return mAbi; 449 } 450 451 /** 452 * Sets whether or not --instant should be used when installing the apk. Will have no effect if 453 * force-install-mode is set. 454 */ setInstantMode(boolean mode)455 public final void setInstantMode(boolean mode) { 456 mInstantMode = mode; 457 } 458 459 /** Returns whether or not instant mode installation has been enabled. */ isInstantMode()460 public final boolean isInstantMode() { 461 return mInstantMode; 462 } 463 464 /** {@inheritDoc} */ 465 @Override tearDown(TestInformation testInfo, Throwable e)466 public void tearDown(TestInformation testInfo, Throwable e) throws DeviceNotAvailableException { 467 mTestInfo = testInfo; 468 if (mCleanup && !(e instanceof DeviceNotAvailableException)) { 469 for (String packageName : mPackagesInstalled) { 470 try { 471 uninstallPackage(getDevice(), packageName); 472 } catch (TargetSetupError tse) { 473 CLog.e(tse); 474 } 475 } 476 } 477 } 478 479 /** 480 * Set an alternate directory. 481 */ setAltDir(File altDir)482 public void setAltDir(File altDir) { 483 mAltDirs.add(altDir); 484 } 485 486 /** 487 * Set an alternate directory behaviors. 488 */ setAltDirBehavior(AltDirBehavior altDirBehavior)489 public void setAltDirBehavior(AltDirBehavior altDirBehavior) { 490 mAltDirBehavior = altDirBehavior; 491 } 492 493 /** Returns True if Apks will be cleaned up during tear down. */ isCleanUpEnabled()494 public boolean isCleanUpEnabled() { 495 return mCleanup; 496 } 497 498 /** 499 * Attempt to install an package or split package on the device. 500 * 501 * @param testInfo the {@link TestInformation} for the invocation 502 * @param appFilesAndPackages The apks and their package to be installed. 503 */ installer(TestInformation testInfo, Map<File, String> appFilesAndPackages)504 protected void installer(TestInformation testInfo, Map<File, String> appFilesAndPackages) 505 throws TargetSetupError, DeviceNotAvailableException { 506 ITestDevice device = testInfo.getDevice(); 507 508 // TODO(hzalek): Consider changing resolveApkFiles's return to a Multimap to avoid building 509 // it here. 510 ImmutableListMultimap<String, File> packageToFiles = 511 ImmutableListMultimap.copyOf(appFilesAndPackages.entrySet()).inverse(); 512 513 Builder builder = null; 514 if (mIncrementalInstallation) { 515 builder = getIncrementalInstallSessionBuilder(); 516 } 517 518 for (Map.Entry<String, List<File>> e : Multimaps.asMap(packageToFiles).entrySet()) { 519 if (mIncrementalInstallation) { 520 CLog.d( 521 "Performing incremental installation of apk %s with %s ...", 522 e.getKey(), e.getValue()); 523 addPackageToIncrementalInstallSession(builder, e.getKey(), e.getValue()); 524 if (mCleanup) { 525 mPackagesInstalled.add(e.getKey()); 526 } 527 } else { 528 installSinglePackage(device, e.getKey(), e.getValue()); 529 } 530 } 531 532 if (mIncrementalInstallation && builder != null) { 533 installPackageIncrementally(builder); 534 } 535 } 536 installSinglePackage( ITestDevice testDevice, String packageName, List<File> apkFiles)537 private void installSinglePackage( 538 ITestDevice testDevice, String packageName, List<File> apkFiles) 539 throws TargetSetupError, DeviceNotAvailableException { 540 541 if (apkFiles.isEmpty()) { 542 return; 543 } 544 545 CLog.d("Installing apk %s with %s ...", packageName, apkFiles); 546 String result = installPackage(testDevice, apkFiles); 547 548 if (result != null) { 549 if (result.startsWith(INSTALL_FAILED_UPDATE_INCOMPATIBLE)) { 550 // Try to uninstall package and reinstall. 551 uninstallPackage(testDevice, packageName); 552 result = installPackage(testDevice, apkFiles); 553 } 554 } 555 556 if (result != null) { 557 throw new TargetSetupError( 558 String.format( 559 "Failed to install %s with %s on %s. Reason: '%s'", 560 packageName, apkFiles, testDevice.getSerialNumber(), result), 561 testDevice.getDeviceDescriptor(), 562 DeviceErrorIdentifier.APK_INSTALLATION_FAILED); 563 } 564 565 if (mCleanup) { 566 mPackagesInstalled.add(packageName); 567 } 568 } 569 570 /** Helper to resolve some apk to their File and Package. */ 571 @VisibleForTesting resolveApkFiles(TestInformation testInfo, List<File> apkFiles)572 protected Map<File, String> resolveApkFiles(TestInformation testInfo, List<File> apkFiles) 573 throws TargetSetupError, DeviceNotAvailableException { 574 Map<File, String> appFiles = new LinkedHashMap<>(); 575 ITestDevice device = testInfo.getDevice(); 576 for (File apkFile : apkFiles) { 577 File testAppFile = null; 578 if (apkFile.isAbsolute()) { 579 testAppFile = apkFile; 580 } 581 if (testAppFile == null) { 582 testAppFile = getLocalPathForFilename(testInfo, apkFile.getName()); 583 } 584 if (testAppFile == null) { 585 if (mThrowIfNoFile) { 586 throw new TargetSetupError( 587 String.format("Test app %s was not found.", apkFile.getName()), 588 device.getDeviceDescriptor(), 589 InfraErrorIdentifier.CONFIGURED_ARTIFACT_NOT_FOUND); 590 } else { 591 CLog.d("Test app %s was not found.", apkFile.getName()); 592 continue; 593 } 594 } 595 if (!testAppFile.canRead()) { 596 if (mThrowIfNoFile) { 597 throw new TargetSetupError( 598 String.format("Could not read file %s.", testAppFile.toString()), 599 device.getDeviceDescriptor()); 600 } else { 601 CLog.d("Could not read file %s.", testAppFile.toString()); 602 continue; 603 } 604 } 605 606 if (mCheckMinSdk) { 607 AaptParser aaptParser = doAaptParse(testAppFile); 608 if (aaptParser == null) { 609 throw new TargetSetupError( 610 String.format( 611 "Failed to extract info from `%s` using aapt", 612 testAppFile.getAbsoluteFile().getName()), 613 device.getDeviceDescriptor()); 614 } 615 if (device.getApiLevel() < aaptParser.getSdkVersion()) { 616 CLog.w( 617 "Skipping installing apk %s on device %s because " 618 + "SDK level require is %d, but device SDK level is %d", 619 apkFile.toString(), 620 device.getSerialNumber(), 621 aaptParser.getSdkVersion(), 622 device.getApiLevel()); 623 } else { 624 appFiles.put(testAppFile, parsePackageName(testAppFile)); 625 } 626 } else { 627 appFiles.put(testAppFile, parsePackageName(testAppFile)); 628 } 629 } 630 return appFiles; 631 } 632 633 /** 634 * Returns the provided file if not a directory or all APK files contained in the directory tree 635 * rooted at the provided path otherwise. 636 */ findApkFiles(File fileOrDirectory)637 private List<File> findApkFiles(File fileOrDirectory) throws TargetSetupError { 638 639 if (!fileOrDirectory.isDirectory()) { 640 return ImmutableList.of(fileOrDirectory); 641 } 642 643 List<File> apkFiles; 644 645 try (Stream<Path> paths = Files.walk(fileOrDirectory.toPath())) { 646 apkFiles = 647 paths.filter(p -> p.toString().endsWith(".apk")) 648 .filter(Files::isRegularFile) 649 .map(Path::toFile) 650 .collect(Collectors.toList()); 651 } catch (IOException e) { 652 throw new TargetSetupError( 653 String.format( 654 "Could not list files of specified directory: %s", fileOrDirectory), 655 e, 656 null, 657 false, 658 InfraErrorIdentifier.CONFIGURED_ARTIFACT_NOT_FOUND); 659 } 660 661 if (mThrowIfNoFile && apkFiles.isEmpty()) { 662 throw new TargetSetupError( 663 String.format( 664 "Could not find any files in specified directory: %s", fileOrDirectory), 665 null, 666 null, 667 false, 668 InfraErrorIdentifier.CONFIGURED_ARTIFACT_NOT_FOUND); 669 } 670 671 return apkFiles; 672 } 673 674 /** 675 * Attempt to install a package or split package on the device. 676 * 677 * @param device the {@link ITestDevice} to install package 678 * @param appFiles List of Files. If apkFiles contains only one apk file, the app will be 679 * installed as a whole package with single file. If apkFiles contains more than one name, 680 * the app will be installed as split apk with multiple files. 681 */ installPackage(ITestDevice device, List<File> appFiles)682 private String installPackage(ITestDevice device, List<File> appFiles) 683 throws DeviceNotAvailableException { 684 // Handle the different install use cases (with or without a user) 685 if (mUserId == null) { 686 if (appFiles.size() == 1) { 687 return device.installPackage( 688 appFiles.get(0), true, mInstallArgs.toArray(new String[] {})); 689 } else { 690 return device.installPackages( 691 appFiles, true, mInstallArgs.toArray(new String[] {})); 692 } 693 } else if (mGrantPermission != null) { 694 if (appFiles.size() == 1) { 695 return device.installPackageForUser( 696 appFiles.get(0), 697 true, 698 mGrantPermission, 699 mUserId, 700 mInstallArgs.toArray(new String[] {})); 701 } else { 702 return device.installPackagesForUser( 703 appFiles, 704 true, 705 mGrantPermission, 706 mUserId, 707 mInstallArgs.toArray(new String[] {})); 708 } 709 } else { 710 if (appFiles.size() == 1) { 711 return device.installPackageForUser( 712 appFiles.get(0), true, mUserId, mInstallArgs.toArray(new String[] {})); 713 } else { 714 return device.installPackagesForUser( 715 appFiles, true, mUserId, mInstallArgs.toArray(new String[] {})); 716 } 717 } 718 } 719 720 /** Attempt to remove the package from the device. */ uninstallPackage(ITestDevice device, String packageName)721 protected void uninstallPackage(ITestDevice device, String packageName) 722 throws DeviceNotAvailableException { 723 String msg; 724 if (mUserId == null) { 725 msg = device.uninstallPackage(packageName); 726 } else { 727 msg = device.uninstallPackageForUser(packageName, mUserId); 728 } 729 if (msg != null) { 730 CLog.w(String.format("error uninstalling package '%s': %s", packageName, msg)); 731 } 732 if (mIncrementalInstallation) { 733 incrementalInstallSession.close(); 734 } 735 } 736 737 /** Get the package name from the test app. */ parsePackageName(File testAppFile)738 protected String parsePackageName(File testAppFile) throws TargetSetupError { 739 AaptParser parser = AaptParser.parse(testAppFile, mAaptVersion); 740 if (parser == null) { 741 throw new TargetSetupError( 742 String.format( 743 "AaptParser failed for file %s. The APK won't be installed", 744 testAppFile.getName()), 745 null, 746 null, 747 false, // Not device side error, doesn't need descriptor 748 DeviceErrorIdentifier.AAPT_PARSER_FAILED); 749 } 750 return parser.getPackageName(); 751 } 752 753 /** 754 * Add APKs from package to incremental installation session builder object. 755 * 756 * @param builder The Builder object for the incremental install session. 757 * @param packageName The name of the package to be added. 758 * @param packageFiles List of files to be added to builder object. 759 * @throws TargetSetupError 760 */ addPackageToIncrementalInstallSession( Builder builder, String packageName, List<File> packageFiles)761 private void addPackageToIncrementalInstallSession( 762 Builder builder, String packageName, List<File> packageFiles) throws TargetSetupError { 763 for (File apk : packageFiles) { 764 Path apkPath = apk.toPath(); 765 Path apkSignaturePath = Paths.get(String.format("%s.idsig", apkPath.toString())); 766 if (!apkSignaturePath.toFile().exists()) { 767 throw new TargetSetupError( 768 String.format( 769 "Unable to retrieve v4 signature for file: %s", 770 apkPath.getFileName()), 771 getDevice().getDeviceDescriptor(), 772 InfraErrorIdentifier.CONFIGURED_ARTIFACT_NOT_FOUND); 773 } 774 builder.addApk(apkPath, apkSignaturePath); 775 } 776 } 777 778 /** 779 * Start the incremental installation session for a test app. 780 * 781 * @param builder The Builder object for the incremental install session. 782 * @throws TargetSetupError 783 */ 784 @VisibleForTesting installPackageIncrementally(Builder builder)785 protected void installPackageIncrementally(Builder builder) throws TargetSetupError { 786 try { 787 incrementalInstallSession = builder.build(); 788 String deviceSerialNumber = getDevice().getSerialNumber(); 789 DeviceConnection.Factory deviceConnection = 790 DeviceConnection.getFactory(deviceSerialNumber); 791 incrementalInstallSession.start(Executors.newCachedThreadPool(), deviceConnection); 792 incrementalInstallSession.waitForInstallCompleted( 793 mIncrementalInstallTimeout, TimeUnit.SECONDS); 794 } catch (InterruptedException | IOException e) { 795 throw new TargetSetupError( 796 String.format("Failed to start incremental install session."), 797 e, 798 getDevice().getDeviceDescriptor(), 799 DeviceErrorIdentifier.APK_INSTALLATION_FAILED); 800 } 801 } 802 803 /** Initialize the session builder for installing a test app incrementally. */ 804 @VisibleForTesting getIncrementalInstallSessionBuilder()805 protected Builder getIncrementalInstallSessionBuilder() { 806 if (mGrantPermission != null && mGrantPermission) { 807 mInstallArgs.add("-g"); 808 } 809 810 if (mUserId != null) { 811 mInstallArgs.add("--user"); 812 mInstallArgs.add(Integer.toString(mUserId)); 813 } 814 815 Builder incrementalInstallSessionBuilder = 816 new Builder() 817 .setLogger(new DeviceLogger(new StdLogger(StdLogger.Level.ERROR))) 818 .addExtraArgs(mInstallArgs.toArray(new String[] {})); 819 820 // Add block filter to installation if a block filter percentage is specified. 821 if (mBlockFilterPercentage > 0) { 822 long randomSeed = new SecureRandom().nextLong(); 823 Random randomBlock = new Random(randomSeed); 824 Map<Path, Set<Integer>> apkBlockMappings = new HashMap<>(); 825 826 CLog.i("Block filter seed: %d.", randomSeed); 827 828 incrementalInstallSessionBuilder.setBlockFilter( 829 (PendingBlock b) -> { 830 Path apkPath = b.getPath(); 831 synchronized (apkBlockMappings) { 832 // Generate block indexs to filter for APK installation. 833 if (!apkBlockMappings.containsKey(apkPath)) { 834 int blockCount = b.getFileBlockCount(); 835 int numBlocks = (int) (blockCount * mBlockFilterPercentage); 836 Set<Integer> blocksToFilter = new HashSet<Integer>(numBlocks); 837 while (blocksToFilter.size() < numBlocks) { 838 int blockIndex = randomBlock.nextInt(blockCount); 839 blocksToFilter.add(blockIndex); 840 } 841 apkBlockMappings.put(apkPath, blocksToFilter); 842 } 843 844 return !apkBlockMappings.get(apkPath).contains(b.getBlockIndex()); 845 } 846 }); 847 } 848 849 return incrementalInstallSessionBuilder; 850 } 851 852 @Override reportDependencies()853 public Set<String> reportDependencies() { 854 Set<String> deps = new HashSet<String>(); 855 for (File f : getTestsFileName()) { 856 if (!f.exists()) deps.add(f.getName()); 857 } 858 for (String testAppNames : mSplitApkFileNames) { 859 List<String> apkNames = Arrays.asList(testAppNames.split(",")); 860 deps.addAll(apkNames); 861 } 862 return deps; 863 } 864 } 865