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 com.android.annotations.VisibleForTesting; 19 import com.android.tradefed.build.IBuildInfo; 20 import com.android.tradefed.build.IDeviceBuildInfo; 21 import com.android.tradefed.command.remote.DeviceDescriptor; 22 import com.android.tradefed.config.Option; 23 import com.android.tradefed.config.Option.Importance; 24 import com.android.tradefed.config.OptionClass; 25 import com.android.tradefed.device.DeviceNotAvailableException; 26 import com.android.tradefed.device.ITestDevice; 27 import com.android.tradefed.invoker.IInvocationContext; 28 import com.android.tradefed.invoker.InvocationContext; 29 import com.android.tradefed.invoker.TestInformation; 30 import com.android.tradefed.log.LogUtil.CLog; 31 import com.android.tradefed.result.error.DeviceErrorIdentifier; 32 import com.android.tradefed.result.error.InfraErrorIdentifier; 33 import com.android.tradefed.testtype.IAbi; 34 import com.android.tradefed.testtype.IAbiReceiver; 35 import com.android.tradefed.util.AaptParser; 36 import com.android.tradefed.util.AbiFormatter; 37 import com.android.tradefed.util.BuildTestsZipUtils; 38 39 import com.google.common.collect.ImmutableList; 40 import com.google.common.collect.ImmutableListMultimap; 41 import com.google.common.collect.Multimaps; 42 43 import java.io.File; 44 import java.io.IOException; 45 import java.nio.file.Files; 46 import java.nio.file.Path; 47 import java.util.ArrayList; 48 import java.util.Arrays; 49 import java.util.Collection; 50 import java.util.HashSet; 51 import java.util.LinkedHashMap; 52 import java.util.List; 53 import java.util.Map; 54 import java.util.Set; 55 import java.util.stream.Collectors; 56 import java.util.stream.Stream; 57 58 /** 59 * A {@link ITargetPreparer} that installs one or more apps from a {@link 60 * IDeviceBuildInfo#getTestsDir()} folder onto device. 61 * 62 * <p>This preparer will look in alternate directories if the tests zip does not exist or does not 63 * contain the required apk. The search will go in order from the last alternative dir specified to 64 * the first. 65 */ 66 @OptionClass(alias = "tests-zip-app") 67 public class TestAppInstallSetup extends BaseTargetPreparer implements IAbiReceiver { 68 69 /** The mode the apk should be install in. */ 70 private enum InstallMode { 71 FULL, 72 INSTANT, 73 } 74 75 // An error message that occurs when a test APK is already present on the DUT, 76 // but cannot be updated. When this occurs, the package is removed from the 77 // device so that installation can continue like normal. 78 private static final String INSTALL_FAILED_UPDATE_INCOMPATIBLE = 79 "INSTALL_FAILED_UPDATE_INCOMPATIBLE"; 80 81 @VisibleForTesting static final String TEST_FILE_NAME_OPTION = "test-file-name"; 82 83 @Option( 84 name = TEST_FILE_NAME_OPTION, 85 description = 86 "the name of an apk file to be installed on device. Can be repeated. Items " 87 + "that are directories will have any APKs contained therein, " 88 + "including subdirectories, grouped by package name and installed.", 89 importance = Importance.IF_UNSET) 90 private List<File> mTestFiles = new ArrayList<>(); 91 92 // A string made of split apk file names divided by ",". 93 // See "https://developer.android.com/studio/build/configure-apk-splits" on how to split 94 // apk to several files. 95 @Option( 96 name = "split-apk-file-names", 97 description = 98 "the split apk file names separted by comma that will be installed on device. " 99 + "Can be repeated for multiple split apk sets." 100 + "See https://developer.android.com/studio/build/configure-apk-splits on " 101 + "how to split apk to several files") 102 private List<String> mSplitApkFileNames = new ArrayList<>(); 103 104 @VisibleForTesting static final String THROW_IF_NOT_FOUND_OPTION = "throw-if-not-found"; 105 106 @Option( 107 name = THROW_IF_NOT_FOUND_OPTION, 108 description = "Throw exception if the specified file is not found.") 109 private boolean mThrowIfNoFile = true; 110 111 @Option(name = AbiFormatter.FORCE_ABI_STRING, 112 description = AbiFormatter.FORCE_ABI_DESCRIPTION, 113 importance = Importance.IF_UNSET) 114 private String mForceAbi = null; 115 116 @Option(name = "install-arg", 117 description = "Additional arguments to be passed to install command, " 118 + "including leading dash, e.g. \"-d\"") 119 private Collection<String> mInstallArgs = new ArrayList<>(); 120 121 @Option(name = "force-queryable", 122 description = "Whether apks should be installed as force queryable.") 123 private boolean mForceQueryable = true; 124 125 @Option( 126 name = "cleanup-apks", 127 description = 128 "Whether apks installed should be uninstalled after test. Note that the " 129 + "preparer does not verify if the apks are successfully removed.") 130 private boolean mCleanup = true; 131 132 /** @deprecated use test-file-name instead now that it is a File. */ 133 @Deprecated 134 @Option( 135 name = "alt-dir", 136 description = 137 "Alternate directory to look for the apk if the apk is not in the tests " 138 + "zip file. For each alternate dir, will look in //, //data/app, " 139 + "//DATA/app, //DATA/app/apk_name/ and //DATA/priv-app/apk_name/. " 140 + "Can be repeated. Look for apks in last alt-dir first.") 141 private List<File> mAltDirs = new ArrayList<>(); 142 143 /** @deprecated goes in pair with alt-dir which is deprecated */ 144 @Deprecated 145 @Option( 146 name = "alt-dir-behavior", 147 description = 148 "The order of alternate directory to be used when searching for apks to " 149 + "install") 150 private AltDirBehavior mAltDirBehavior = AltDirBehavior.FALLBACK; 151 152 @Option(name = "instant-mode", description = "Whether or not to install apk in instant mode.") 153 private boolean mInstantMode = false; 154 155 @Option( 156 name = "force-install-mode", 157 description = 158 "Force the preparer to ignore instant-mode option, and install in the requested mode." 159 ) 160 private InstallMode mInstallationMode = null; 161 162 private IAbi mAbi = null; 163 private Integer mUserId = null; 164 private Boolean mGrantPermission = null; 165 166 private Set<String> mPackagesInstalled = null; 167 private TestInformation mTestInfo; 168 setTestInformation(TestInformation testInfo)169 protected void setTestInformation(TestInformation testInfo) { 170 mTestInfo = testInfo; 171 } 172 173 /** Adds a file or directory to the list of apks to installed. */ addTestFile(File file)174 public void addTestFile(File file) { 175 mTestFiles.add(file); 176 } 177 178 /** Adds a file name to the list of apks to installed. */ addTestFileName(String fileName)179 public void addTestFileName(String fileName) { 180 addTestFile(new File(fileName)); 181 } 182 183 @VisibleForTesting clearTestFile()184 void clearTestFile() { 185 mTestFiles.clear(); 186 } 187 188 /** 189 * Adds a set of file names divided by ',' in a string to be installed as split apks 190 * 191 * @param fileNames a string of file names divided by ',' 192 */ addSplitApkFileNames(String fileNames)193 public void addSplitApkFileNames(String fileNames) { 194 mSplitApkFileNames.add(fileNames); 195 } 196 197 @VisibleForTesting clearSplitApkFileNames()198 void clearSplitApkFileNames() { 199 mSplitApkFileNames.clear(); 200 } 201 202 /** Returns a copy of the list of specified test apk names. */ getTestsFileName()203 public List<File> getTestsFileName() { 204 return mTestFiles; 205 } 206 207 /** Sets whether or not the installed apk should be cleaned on tearDown */ setCleanApk(boolean shouldClean)208 public void setCleanApk(boolean shouldClean) { 209 mCleanup = shouldClean; 210 } 211 212 /** 213 * If the apk should be installed for a particular user, sets the id of the user to install for. 214 */ setUserId(int userId)215 public void setUserId(int userId) { 216 mUserId = userId; 217 } 218 219 /** If a userId is provided, grantPermission can be set for the apk installation. */ setShouldGrantPermission(boolean shouldGrant)220 public void setShouldGrantPermission(boolean shouldGrant) { 221 mGrantPermission = shouldGrant; 222 } 223 224 /** Adds one apk installation arg to be used. */ addInstallArg(String arg)225 public void addInstallArg(String arg) { 226 mInstallArgs.add(arg); 227 } 228 229 /** 230 * Resolve the actual apk path based on testing artifact information inside build info. 231 * 232 * @param testInfo The {@link TestInformation} for the invocation. 233 * @param apkFileName filename of the apk to install 234 * @return a {@link File} representing the physical apk file on host or {@code null} if the file 235 * does not exist. 236 */ getLocalPathForFilename(TestInformation testInfo, String apkFileName)237 protected File getLocalPathForFilename(TestInformation testInfo, String apkFileName) 238 throws TargetSetupError { 239 try { 240 return BuildTestsZipUtils.getApkFile( 241 testInfo.getBuildInfo(), 242 apkFileName, 243 mAltDirs, 244 mAltDirBehavior, 245 false /* use resource as fallback */, 246 null /* device signing key */); 247 } catch (IOException ioe) { 248 throw new TargetSetupError( 249 String.format( 250 "failed to resolve apk path for apk %s in build %s", 251 apkFileName, testInfo.getBuildInfo().toString()), 252 ioe, 253 testInfo.getDevice().getDeviceDescriptor(), 254 InfraErrorIdentifier.ARTIFACT_NOT_FOUND); 255 } 256 } 257 258 /** @deprecated Temporary backward compatible callback. */ 259 @Deprecated 260 @Override setUp(ITestDevice device, IBuildInfo buildInfo)261 public void setUp(ITestDevice device, IBuildInfo buildInfo) 262 throws TargetSetupError, BuildError, DeviceNotAvailableException { 263 IInvocationContext context = new InvocationContext(); 264 context.addAllocatedDevice("device", device); 265 context.addDeviceBuildInfo("device", buildInfo); 266 TestInformation backwardCompatible = 267 TestInformation.newBuilder().setInvocationContext(context).build(); 268 setUp(backwardCompatible); 269 } 270 271 /** {@inheritDoc} */ 272 @Override setUp(TestInformation testInfo)273 public void setUp(TestInformation testInfo) 274 throws TargetSetupError, BuildError, DeviceNotAvailableException { 275 mTestInfo = testInfo; 276 if (mTestFiles.isEmpty() && mSplitApkFileNames.isEmpty()) { 277 CLog.i("No test apps to install, skipping"); 278 return; 279 } 280 if (mCleanup) { 281 mPackagesInstalled = new HashSet<>(); 282 } 283 284 // resolve abi flags 285 if (mAbi != null && mForceAbi != null) { 286 throw new IllegalStateException("cannot specify both abi flags: --abi and --force-abi"); 287 } 288 String abiName = null; 289 if (mAbi != null) { 290 abiName = mAbi.getName(); 291 } else if (mForceAbi != null) { 292 abiName = AbiFormatter.getDefaultAbi(getDevice(), mForceAbi); 293 } 294 295 // Set all the extra install args outside the loop to avoid adding them several times. 296 if (abiName != null && testInfo.getDevice().getApiLevel() > 20) { 297 mInstallArgs.add(String.format("--abi %s", abiName)); 298 } 299 // Handle instant mode: if we are forced in one installation mode or not. 300 // Some preparer are locked in one installation mode or another, they ignore the 301 // 'instant-mode' option and stays in their mode. 302 if (mInstallationMode != null) { 303 if (InstallMode.INSTANT.equals(mInstallationMode)) { 304 mInstallArgs.add("--instant"); 305 } 306 } else { 307 if (mInstantMode) { 308 mInstallArgs.add("--instant"); 309 } 310 } 311 312 if (mForceQueryable && getDevice().isAppEnumerationSupported()) { 313 mInstallArgs.add("--force-queryable"); 314 } 315 316 for (File testAppName : mTestFiles) { 317 Map<File, String> appFilesAndPackages = 318 resolveApkFiles( 319 testInfo, 320 findApkFiles(testAppName, testInfo.getDevice().getDeviceDescriptor())); 321 installer(testInfo, appFilesAndPackages); 322 } 323 324 for (String testAppNames : mSplitApkFileNames) { 325 List<String> apkNames = Arrays.asList(testAppNames.split(",")); 326 List<File> apkFileNames = 327 apkNames.stream().map(a -> new File(a)).collect(Collectors.toList()); 328 Map<File, String> appFilesAndPackages = resolveApkFiles(testInfo, apkFileNames); 329 installer(testInfo, appFilesAndPackages); 330 } 331 } 332 333 /** 334 * Returns the device that the preparer should apply to. 335 * 336 * @throws TargetSetupError 337 */ getDevice()338 public ITestDevice getDevice() throws TargetSetupError { 339 return mTestInfo.getDevice(); 340 } 341 getTestInfo()342 public TestInformation getTestInfo() { 343 return mTestInfo; 344 } 345 346 @Override setAbi(IAbi abi)347 public void setAbi(IAbi abi) { 348 mAbi = abi; 349 } 350 351 @Override getAbi()352 public IAbi getAbi() { 353 return mAbi; 354 } 355 356 /** 357 * Sets whether or not --instant should be used when installing the apk. Will have no effect if 358 * force-install-mode is set. 359 */ setInstantMode(boolean mode)360 public final void setInstantMode(boolean mode) { 361 mInstantMode = mode; 362 } 363 364 /** Returns whether or not instant mode installation has been enabled. */ isInstantMode()365 public final boolean isInstantMode() { 366 return mInstantMode; 367 } 368 369 /** {@inheritDoc} */ 370 @Override tearDown(TestInformation testInfo, Throwable e)371 public void tearDown(TestInformation testInfo, Throwable e) throws DeviceNotAvailableException { 372 mTestInfo = testInfo; 373 if (mCleanup && mPackagesInstalled != null && !(e instanceof DeviceNotAvailableException)) { 374 for (String packageName : mPackagesInstalled) { 375 try { 376 uninstallPackage(getDevice(), packageName); 377 } catch (TargetSetupError tse) { 378 CLog.e(tse); 379 } 380 } 381 } 382 } 383 384 /** 385 * Set an alternate directory. 386 */ setAltDir(File altDir)387 public void setAltDir(File altDir) { 388 mAltDirs.add(altDir); 389 } 390 391 /** 392 * Set an alternate directory behaviors. 393 */ setAltDirBehavior(AltDirBehavior altDirBehavior)394 public void setAltDirBehavior(AltDirBehavior altDirBehavior) { 395 mAltDirBehavior = altDirBehavior; 396 } 397 398 /** Returns True if Apks will be cleaned up during tear down. */ isCleanUpEnabled()399 public boolean isCleanUpEnabled() { 400 return mCleanup; 401 } 402 403 /** 404 * Attempt to install an package or split package on the device. 405 * 406 * @param testInfo the {@link TestInformation} for the invocation 407 * @param appFilesAndPackages The apks and their package to be installed. 408 */ installer(TestInformation testInfo, Map<File, String> appFilesAndPackages)409 protected void installer(TestInformation testInfo, Map<File, String> appFilesAndPackages) 410 throws TargetSetupError, DeviceNotAvailableException { 411 ITestDevice device = testInfo.getDevice(); 412 413 // TODO(hzalek): Consider changing resolveApkFiles's return to a Multimap to avoid building 414 // it here. 415 ImmutableListMultimap<String, File> packageToFiles = 416 ImmutableListMultimap.copyOf(appFilesAndPackages.entrySet()).inverse(); 417 418 for (Map.Entry<String, List<File>> e : Multimaps.asMap(packageToFiles).entrySet()) { 419 installSinglePackage(device, e.getKey(), e.getValue()); 420 } 421 } 422 installSinglePackage( ITestDevice testDevice, String packageName, List<File> apkFiles)423 private void installSinglePackage( 424 ITestDevice testDevice, String packageName, List<File> apkFiles) 425 throws TargetSetupError, DeviceNotAvailableException { 426 427 if (apkFiles.isEmpty()) { 428 return; 429 } 430 431 CLog.d("Installing apk %s with %s ...", packageName, apkFiles); 432 String result = installPackage(testDevice, apkFiles); 433 434 if (result != null) { 435 if (result.startsWith(INSTALL_FAILED_UPDATE_INCOMPATIBLE)) { 436 // Try to uninstall package and reinstall. 437 uninstallPackage(testDevice, packageName); 438 result = installPackage(testDevice, apkFiles); 439 } 440 } 441 442 if (result != null) { 443 throw new TargetSetupError( 444 String.format( 445 "Failed to install %s with %s on %s. Reason: '%s'", 446 packageName, apkFiles, testDevice.getSerialNumber(), result), 447 testDevice.getDeviceDescriptor(), 448 DeviceErrorIdentifier.APK_INSTALLATION_FAILED); 449 } 450 451 if (mCleanup) { 452 mPackagesInstalled.add(packageName); 453 } 454 } 455 456 /** Helper to resolve some apk to their File and Package. */ resolveApkFiles(TestInformation testInfo, List<File> apkFiles)457 protected Map<File, String> resolveApkFiles(TestInformation testInfo, List<File> apkFiles) 458 throws TargetSetupError { 459 Map<File, String> appFiles = new LinkedHashMap<>(); 460 ITestDevice device = testInfo.getDevice(); 461 for (File apkFile : apkFiles) { 462 File testAppFile = null; 463 if (apkFile.isAbsolute()) { 464 testAppFile = apkFile; 465 } 466 if (testAppFile == null) { 467 testAppFile = getLocalPathForFilename(testInfo, apkFile.getName()); 468 } 469 if (testAppFile == null) { 470 if (mThrowIfNoFile) { 471 throw new TargetSetupError( 472 String.format("Test app %s was not found.", apkFile.getName()), 473 device.getDeviceDescriptor(), 474 InfraErrorIdentifier.ARTIFACT_NOT_FOUND); 475 } else { 476 CLog.d("Test app %s was not found.", apkFile.getName()); 477 continue; 478 } 479 } 480 if (!testAppFile.canRead()) { 481 if (mThrowIfNoFile) { 482 throw new TargetSetupError( 483 String.format("Could not read file %s.", testAppFile.toString()), 484 device.getDeviceDescriptor()); 485 } else { 486 CLog.d("Could not read file %s.", testAppFile.toString()); 487 continue; 488 } 489 } 490 491 appFiles.put(testAppFile, parsePackageName(testAppFile, device.getDeviceDescriptor())); 492 } 493 return appFiles; 494 } 495 496 /** 497 * Returns the provided file if not a directory or all APK files contained in the directory tree 498 * rooted at the provided path otherwise. 499 */ findApkFiles(File fileOrDirectory, DeviceDescriptor deviceDescriptor)500 private List<File> findApkFiles(File fileOrDirectory, DeviceDescriptor deviceDescriptor) 501 throws TargetSetupError { 502 503 if (!fileOrDirectory.isDirectory()) { 504 return ImmutableList.of(fileOrDirectory); 505 } 506 507 List<File> apkFiles; 508 509 try (Stream<Path> paths = Files.walk(fileOrDirectory.toPath())) { 510 apkFiles = 511 paths.filter(p -> p.toString().endsWith(".apk")) 512 .filter(Files::isRegularFile) 513 .map(Path::toFile) 514 .collect(Collectors.toList()); 515 } catch (IOException e) { 516 throw new TargetSetupError( 517 String.format( 518 "Could not list files of specified directory: %s", fileOrDirectory), 519 e, 520 deviceDescriptor); 521 } 522 523 if (mThrowIfNoFile && apkFiles.isEmpty()) { 524 throw new TargetSetupError( 525 String.format( 526 "Could not find any files in specified directory: %s", fileOrDirectory), 527 deviceDescriptor); 528 } 529 530 return apkFiles; 531 } 532 533 /** 534 * Attempt to install a package or split package on the device. 535 * 536 * @param device the {@link ITestDevice} to install package 537 * @param appFiles List of Files. If apkFiles contains only one apk file, the app will be 538 * installed as a whole package with single file. If apkFiles contains more than one name, 539 * the app will be installed as split apk with multiple files. 540 */ installPackage(ITestDevice device, List<File> appFiles)541 private String installPackage(ITestDevice device, List<File> appFiles) 542 throws DeviceNotAvailableException { 543 // Handle the different install use cases (with or without a user) 544 if (mUserId == null) { 545 if (appFiles.size() == 1) { 546 return device.installPackage( 547 appFiles.get(0), true, mInstallArgs.toArray(new String[] {})); 548 } else { 549 return device.installPackages( 550 appFiles, true, mInstallArgs.toArray(new String[] {})); 551 } 552 } else if (mGrantPermission != null) { 553 if (appFiles.size() == 1) { 554 return device.installPackageForUser( 555 appFiles.get(0), 556 true, 557 mGrantPermission, 558 mUserId, 559 mInstallArgs.toArray(new String[] {})); 560 } else { 561 return device.installPackagesForUser( 562 appFiles, 563 true, 564 mGrantPermission, 565 mUserId, 566 mInstallArgs.toArray(new String[] {})); 567 } 568 } else { 569 if (appFiles.size() == 1) { 570 return device.installPackageForUser( 571 appFiles.get(0), true, mUserId, mInstallArgs.toArray(new String[] {})); 572 } else { 573 return device.installPackagesForUser( 574 appFiles, true, mUserId, mInstallArgs.toArray(new String[] {})); 575 } 576 } 577 } 578 579 /** Attempt to remove the package from the device. */ uninstallPackage(ITestDevice device, String packageName)580 protected void uninstallPackage(ITestDevice device, String packageName) 581 throws DeviceNotAvailableException { 582 String msg = device.uninstallPackage(packageName); 583 if (msg != null) { 584 CLog.w(String.format("error uninstalling package '%s': %s", packageName, msg)); 585 } 586 } 587 588 /** Get the package name from the test app. */ parsePackageName(File testAppFile, DeviceDescriptor deviceDescriptor)589 protected String parsePackageName(File testAppFile, DeviceDescriptor deviceDescriptor) 590 throws TargetSetupError { 591 AaptParser parser = AaptParser.parse(testAppFile); 592 if (parser == null) { 593 throw new TargetSetupError( 594 "apk installed but AaptParser failed", 595 deviceDescriptor, 596 DeviceErrorIdentifier.AAPT_PARSER_FAILED); 597 } 598 return parser.getPackageName(); 599 } 600 } 601 602