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