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.tradefed.build.IBuildInfo; 19 import com.android.tradefed.build.IDeviceBuildInfo; 20 import com.android.tradefed.command.remote.DeviceDescriptor; 21 import com.android.tradefed.config.Option; 22 import com.android.tradefed.config.Option.Importance; 23 import com.android.tradefed.config.OptionClass; 24 import com.android.tradefed.device.DeviceNotAvailableException; 25 import com.android.tradefed.device.ITestDevice; 26 import com.android.tradefed.log.LogUtil.CLog; 27 import com.android.tradefed.testtype.IAbi; 28 import com.android.tradefed.testtype.IAbiReceiver; 29 import com.android.tradefed.util.AaptParser; 30 import com.android.tradefed.util.AbiFormatter; 31 import com.android.tradefed.util.BuildTestsZipUtils; 32 33 import java.io.File; 34 import java.io.IOException; 35 import java.util.Arrays; 36 import java.util.ArrayList; 37 import java.util.Collection; 38 import java.util.List; 39 40 /** 41 * A {@link ITargetPreparer} that installs one or more apps from a {@link 42 * IDeviceBuildInfo#getTestsDir()} folder onto device. 43 * 44 * <p>This preparer will look in alternate directories if the tests zip does not exist or does not 45 * contain the required apk. The search will go in order from the last alternative dir specified to 46 * the first. 47 */ 48 @OptionClass(alias = "tests-zip-app") 49 public class TestAppInstallSetup extends BaseTargetPreparer 50 implements ITargetCleaner, IAbiReceiver { 51 52 /** The mode the apk should be install in. */ 53 private enum InstallMode { 54 FULL, 55 INSTANT, 56 } 57 58 // An error message that occurs when a test APK is already present on the DUT, 59 // but cannot be updated. When this occurs, the package is removed from the 60 // device so that installation can continue like normal. 61 private static final String INSTALL_FAILED_UPDATE_INCOMPATIBLE = 62 "INSTALL_FAILED_UPDATE_INCOMPATIBLE"; 63 64 @Option( 65 name = "test-file-name", 66 description = "the name of an apk file to be installed on device. Can be repeated.", 67 importance = Importance.IF_UNSET 68 ) 69 private Collection<String> mTestFileNames = new ArrayList<String>(); 70 71 // A string made of split apk file names divided by ",". 72 // See "https://developer.android.com/studio/build/configure-apk-splits" on how to split 73 // apk to several files. 74 @Option( 75 name = "split-apk-file-names", 76 description = 77 "the split apk file names separted by comma that will be installed on device. " 78 + "Can be repeated for multiple split apk sets." 79 + "See https://developer.android.com/studio/build/configure-apk-splits on " 80 + "how to split apk to several files" 81 ) 82 private Collection<String> mSplitApkFileNames = new ArrayList<String>(); 83 84 @Option( 85 name = "throw-if-not-found", 86 description = "Throw exception if the specified file is not found." 87 ) 88 private boolean mThrowIfNoFile = true; 89 90 @Option(name = AbiFormatter.FORCE_ABI_STRING, 91 description = AbiFormatter.FORCE_ABI_DESCRIPTION, 92 importance = Importance.IF_UNSET) 93 private String mForceAbi = null; 94 95 @Option(name = "install-arg", 96 description = "Additional arguments to be passed to install command, " 97 + "including leading dash, e.g. \"-d\"") 98 private Collection<String> mInstallArgs = new ArrayList<>(); 99 100 @Option(name = "cleanup-apks", 101 description = "Whether apks installed should be uninstalled after test. Note that the " 102 + "preparer does not verify if the apks are successfully removed.") 103 private boolean mCleanup = false; 104 105 @Option(name = "alt-dir", 106 description = "Alternate directory to look for the apk if the apk is not in the tests " 107 + "zip file. For each alternate dir, will look in //, //data/app, //DATA/app, " 108 + "//DATA/app/apk_name/ and //DATA/priv-app/apk_name/. Can be repeated. " 109 + "Look for apks in last alt-dir first.") 110 private List<File> mAltDirs = new ArrayList<>(); 111 112 @Option(name = "alt-dir-behavior", description = "The order of alternate directory to be used " 113 + "when searching for apks to install") 114 private AltDirBehavior mAltDirBehavior = AltDirBehavior.FALLBACK; 115 116 @Option(name = "instant-mode", description = "Whether or not to install apk in instant mode.") 117 private boolean mInstantMode = false; 118 119 @Option( 120 name = "force-install-mode", 121 description = 122 "Force the preparer to ignore instant-mode option, and install in the requested mode." 123 ) 124 private InstallMode mInstallationMode = null; 125 126 private IAbi mAbi = null; 127 private Integer mUserId = null; 128 private Boolean mGrantPermission = null; 129 130 private List<String> mPackagesInstalled = null; 131 132 /** 133 * Adds a file name to the list of apks to installed 134 * 135 * @param fileName 136 */ addTestFileName(String fileName)137 public void addTestFileName(String fileName) { 138 mTestFileNames.add(fileName); 139 } 140 141 /** 142 * Adds a set of file names divided by ',' in a string to be installed as split apks 143 * 144 * @param fileNames a string of file names divided by ',' 145 */ addSplitApkFileNames(String fileNames)146 public void addSplitApkFileNames(String fileNames) { 147 mSplitApkFileNames.add(fileNames); 148 } 149 150 /** Returns a copy of the list of specified test apk names. */ getTestsFileName()151 public List<String> getTestsFileName() { 152 return new ArrayList<String>(mTestFileNames); 153 } 154 155 /** Sets whether or not the installed apk should be cleaned on tearDown */ setCleanApk(boolean shouldClean)156 public void setCleanApk(boolean shouldClean) { 157 mCleanup = shouldClean; 158 } 159 160 /** 161 * If the apk should be installed for a particular user, sets the id of the user to install for. 162 */ setUserId(int userId)163 public void setUserId(int userId) { 164 mUserId = userId; 165 } 166 167 /** If a userId is provided, grantPermission can be set for the apk installation. */ setShouldGrantPermission(boolean shouldGrant)168 public void setShouldGrantPermission(boolean shouldGrant) { 169 mGrantPermission = shouldGrant; 170 } 171 172 /** Adds one apk installation arg to be used. */ addInstallArg(String arg)173 public void addInstallArg(String arg) { 174 mInstallArgs.add(arg); 175 } 176 177 /** 178 * Resolve the actual apk path based on testing artifact information inside build info. 179 * 180 * @param buildInfo build artifact information 181 * @param apkFileName filename of the apk to install 182 * @param device the {@link ITestDevice} being prepared 183 * @return a {@link File} representing the physical apk file on host or {@code null} if the file 184 * does not exist. 185 */ getLocalPathForFilename( IBuildInfo buildInfo, String apkFileName, ITestDevice device)186 protected File getLocalPathForFilename( 187 IBuildInfo buildInfo, String apkFileName, ITestDevice device) throws TargetSetupError { 188 try { 189 return BuildTestsZipUtils.getApkFile(buildInfo, apkFileName, mAltDirs, mAltDirBehavior, 190 false /* use resource as fallback */, 191 null /* device signing key */); 192 } catch (IOException ioe) { 193 throw new TargetSetupError( 194 String.format( 195 "failed to resolve apk path for apk %s in build %s", 196 apkFileName, buildInfo.toString()), 197 ioe, 198 device.getDeviceDescriptor()); 199 } 200 } 201 202 /** {@inheritDoc} */ 203 @Override setUp(ITestDevice device, IBuildInfo buildInfo)204 public void setUp(ITestDevice device, IBuildInfo buildInfo) 205 throws TargetSetupError, DeviceNotAvailableException { 206 if (mTestFileNames.isEmpty() && mSplitApkFileNames.isEmpty()) { 207 CLog.i("No test apps to install, skipping"); 208 return; 209 } 210 if (mCleanup) { 211 mPackagesInstalled = new ArrayList<>(); 212 } 213 214 // resolve abi flags 215 if (mAbi != null && mForceAbi != null) { 216 throw new IllegalStateException("cannot specify both abi flags: --abi and --force-abi"); 217 } 218 String abiName = null; 219 if (mAbi != null) { 220 abiName = mAbi.getName(); 221 } else if (mForceAbi != null) { 222 abiName = AbiFormatter.getDefaultAbi(device, mForceAbi); 223 } 224 225 // Set all the extra install args outside the loop to avoid adding them several times. 226 if (abiName != null) { 227 mInstallArgs.add(String.format("--abi %s", abiName)); 228 } 229 // Handle instant mode: if we are forced in one installation mode or not. 230 // Some preparer are locked in one installation mode or another, they ignore the 231 // 'instant-mode' option and stays in their mode. 232 if (mInstallationMode != null) { 233 if (InstallMode.INSTANT.equals(mInstallationMode)) { 234 mInstallArgs.add("--instant"); 235 } 236 } else { 237 if (mInstantMode) { 238 mInstallArgs.add("--instant"); 239 } 240 } 241 242 for (String testAppName : mTestFileNames) { 243 installer(device, buildInfo, Arrays.asList(new String[] {testAppName})); 244 } 245 246 for (String testAppNames : mSplitApkFileNames) { 247 List<String> apkNames = Arrays.asList(testAppNames.split(",")); 248 installer(device, buildInfo, apkNames); 249 } 250 } 251 252 @Override setAbi(IAbi abi)253 public void setAbi(IAbi abi) { 254 mAbi = abi; 255 } 256 257 @Override getAbi()258 public IAbi getAbi() { 259 return mAbi; 260 } 261 262 /** 263 * Sets whether or not --instant should be used when installing the apk. Will have no effect if 264 * force-install-mode is set. 265 */ setInstantMode(boolean mode)266 public final void setInstantMode(boolean mode) { 267 mInstantMode = mode; 268 } 269 270 /** Returns whether or not instant mode installation has been enabled. */ isInstantMode()271 public final boolean isInstantMode() { 272 return mInstantMode; 273 } 274 275 /** 276 * {@inheritDoc} 277 */ 278 @Override tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e)279 public void tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e) 280 throws DeviceNotAvailableException { 281 if (mCleanup && mPackagesInstalled != null && !(e instanceof DeviceNotAvailableException)) { 282 for (String packageName : mPackagesInstalled) { 283 uninstallPackage(device, packageName); 284 } 285 } 286 } 287 288 /** 289 * Set an alternate directory. 290 */ setAltDir(File altDir)291 public void setAltDir(File altDir) { 292 mAltDirs.add(altDir); 293 } 294 295 /** 296 * Set an alternate directory behaviors. 297 */ setAltDirBehavior(AltDirBehavior altDirBehavior)298 public void setAltDirBehavior(AltDirBehavior altDirBehavior) { 299 mAltDirBehavior = altDirBehavior; 300 } 301 302 /** 303 * Attempt to install an package or split package on the device. 304 * 305 * @param device the {@link ITestDevice} to install package 306 * @param buildInfo build artifact information 307 * @param apkNames List of String. The application file base names to be installed. If apkNames 308 * contains only one apk name, the apk will be installed as single package. If apkNames 309 * contains more than one name, the apks will be installed as split apks. 310 */ installer(ITestDevice device, IBuildInfo buildInfo, List<String> apkNames)311 protected void installer(ITestDevice device, IBuildInfo buildInfo, List<String> apkNames) 312 throws TargetSetupError, DeviceNotAvailableException { 313 List<File> appFiles = new ArrayList<File>(); 314 List<String> packageNames = new ArrayList<String>(); 315 for (String name : apkNames) { 316 if (name == null || name.trim().isEmpty()) { 317 continue; 318 } 319 File testAppFile = getLocalPathForFilename(buildInfo, name, device); 320 if (testAppFile == null) { 321 if (mThrowIfNoFile) { 322 throw new TargetSetupError( 323 String.format("Test app %s was not found.", name), 324 device.getDeviceDescriptor()); 325 } else { 326 CLog.d("Test app %s was not found.", name); 327 continue; 328 } 329 } 330 if (!testAppFile.canRead()) { 331 if (mThrowIfNoFile) { 332 throw new TargetSetupError( 333 String.format("Could not read file %s.", testAppFile.toString()), 334 device.getDeviceDescriptor()); 335 } else { 336 CLog.d("Could not read file %s.", testAppFile.toString()); 337 continue; 338 } 339 } 340 appFiles.add(testAppFile); 341 String packageName = parsePackageName(testAppFile, device.getDeviceDescriptor()); 342 if (!packageNames.contains(packageName)) { 343 packageNames.add(packageName); 344 } 345 } 346 347 if (appFiles.isEmpty()) { 348 return; 349 } 350 351 CLog.d("Installing apk %s with %s ...", packageNames.toString(), appFiles.toString()); 352 String result = installPackage(device, appFiles); 353 if (result != null) { 354 if (result.startsWith(INSTALL_FAILED_UPDATE_INCOMPATIBLE)) { 355 // Try to uninstall package and reinstall. 356 for (String packageName : packageNames) { 357 uninstallPackage(device, packageName); 358 } 359 result = installPackage(device, appFiles); 360 } 361 } 362 if (result != null) { 363 throw new TargetSetupError( 364 String.format( 365 "Failed to install %s with %s on %s. Reason: '%s'", 366 packageNames.toString(), 367 appFiles.toString(), 368 device.getSerialNumber(), 369 result), 370 device.getDeviceDescriptor()); 371 } 372 if (mCleanup) { 373 if (mPackagesInstalled == null) { 374 mPackagesInstalled = new ArrayList<>(); 375 } 376 mPackagesInstalled.addAll(packageNames); 377 } 378 } 379 380 /** 381 * Attempt to install a package or split package on the device. 382 * 383 * @param device the {@link ITestDevice} to install package 384 * @param apkFiles List of Files. If apkFiles contains only one apk file, the app will be 385 * installed as a whole package with single file. If apkFiles contains more than one name, 386 * the app will be installed as split apk with multiple files. 387 */ installPackage(ITestDevice device, List<File> appFiles)388 private String installPackage(ITestDevice device, List<File> appFiles) 389 throws DeviceNotAvailableException { 390 // Handle the different install use cases (with or without a user) 391 if (mUserId == null) { 392 if (appFiles.size() == 1) { 393 return device.installPackage( 394 appFiles.get(0), true, mInstallArgs.toArray(new String[] {})); 395 } else { 396 return device.installPackages( 397 appFiles, true, mInstallArgs.toArray(new String[] {})); 398 } 399 } else if (mGrantPermission != null) { 400 if (appFiles.size() == 1) { 401 return device.installPackageForUser( 402 appFiles.get(0), 403 true, 404 mGrantPermission, 405 mUserId, 406 mInstallArgs.toArray(new String[] {})); 407 } else { 408 return device.installPackagesForUser( 409 appFiles, 410 true, 411 mGrantPermission, 412 mUserId, 413 mInstallArgs.toArray(new String[] {})); 414 } 415 } else { 416 if (appFiles.size() == 1) { 417 return device.installPackageForUser( 418 appFiles.get(0), true, mUserId, mInstallArgs.toArray(new String[] {})); 419 } else { 420 return device.installPackagesForUser( 421 appFiles, true, mUserId, mInstallArgs.toArray(new String[] {})); 422 } 423 } 424 } 425 426 /** Attempt to remove the package from the device. */ uninstallPackage(ITestDevice device, String packageName)427 protected void uninstallPackage(ITestDevice device, String packageName) 428 throws DeviceNotAvailableException { 429 String msg = device.uninstallPackage(packageName); 430 if (msg != null) { 431 CLog.w(String.format("error uninstalling package '%s': %s", packageName, msg)); 432 } 433 } 434 435 /** Get the package name from the test app. */ parsePackageName(File testAppFile, DeviceDescriptor deviceDescriptor)436 protected String parsePackageName(File testAppFile, DeviceDescriptor deviceDescriptor) 437 throws TargetSetupError { 438 AaptParser parser = AaptParser.parse(testAppFile); 439 if (parser == null) { 440 throw new TargetSetupError("apk installed but AaptParser failed", deviceDescriptor); 441 } 442 return parser.getPackageName(); 443 } 444 } 445 446