1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.tradefed.targetprep; 17 18 import com.android.tradefed.build.IAppBuildInfo; 19 import com.android.tradefed.build.IBuildInfo; 20 import com.android.tradefed.build.VersionedFile; 21 import com.android.tradefed.config.Option; 22 import com.android.tradefed.config.OptionClass; 23 import com.android.tradefed.device.CollectingOutputReceiver; 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.util.AaptParser; 28 29 import com.google.common.annotations.VisibleForTesting; 30 31 import java.io.File; 32 import java.util.ArrayList; 33 import java.util.HashSet; 34 import java.util.List; 35 import java.util.Set; 36 import java.util.concurrent.TimeUnit; 37 38 /** 39 * A {@link ITargetPreparer} that installs an apk and its tests. 40 * 41 * <p>Requires 'aapt' on PATH when --uninstall is set 42 */ 43 @OptionClass(alias = "app-setup") 44 public class AppSetup extends BaseTargetPreparer implements ITargetCleaner { 45 46 @Option(name="reboot", description="reboot device after running tests.") 47 private boolean mReboot = true; 48 49 @Option(name = "install", description = "install all apks in build.") 50 private boolean mInstall = true; 51 52 @Option(name = "uninstall", description = 53 "uninstall only apks in build after test completes.") 54 private boolean mUninstall = true; 55 56 @Option(name = "uninstall-all", description = 57 "uninstall all unnstallable apks found on device after test completes.") 58 private boolean mUninstallAll = false; 59 60 @Option(name = "skip-uninstall-pkg", description = 61 "force retention of this package when --uninstall-all is set.") 62 private Set<String> mSkipUninstallPkgs = new HashSet<String>(); 63 64 @Option(name = "install-flag", description = 65 "optional flag(s) to provide when installing apks.") 66 private ArrayList<String> mInstallFlags = new ArrayList<>(); 67 68 @Option(name = "post-install-cmd", description = 69 "optional post-install adb shell commands; can be repeated.") 70 private List<String> mPostInstallCmds = new ArrayList<>(); 71 72 @Option(name = "post-install-cmd-timeout", description = 73 "max time allowed in ms for a post-install adb shell command." + 74 "DeviceUnresponsiveException will be thrown if it is timed out.") 75 private long mPostInstallCmdTimeout = 2 * 60 * 1000; // default to 2 minutes 76 77 @Option(name = "check-min-sdk", description = 78 "check app's min sdk prior to install and skip if device api level is too low.") 79 private boolean mCheckMinSdk = false; 80 81 /** contains package names of installed apps. Used for uninstall */ 82 private Set<String> mInstalledPkgs = new HashSet<String>(); 83 84 /** 85 * {@inheritDoc} 86 */ 87 @Override setUp(ITestDevice device, IBuildInfo buildInfo)88 public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError, 89 DeviceNotAvailableException, BuildError { 90 if (!(buildInfo instanceof IAppBuildInfo)) { 91 throw new IllegalArgumentException("Provided buildInfo is not a AppBuildInfo"); 92 } 93 IAppBuildInfo appBuild = (IAppBuildInfo)buildInfo; 94 CLog.i("Performing setup on %s", device.getSerialNumber()); 95 96 // double check that device is clean, in case it has unexpected cruft on it 97 if (mUninstallAll && !uninstallAllApps(device)) { 98 // cannot cleanup device! Bad things may happen in future tests. Take device out 99 // of service 100 // TODO: in future, consider doing more sophisticated recovery operations 101 throw new DeviceNotAvailableException(String.format( 102 "Failed to uninstall apps on %s", device.getSerialNumber()), 103 device.getSerialNumber()); 104 } 105 106 if (mInstall) { 107 for (VersionedFile apkFile : appBuild.getAppPackageFiles()) { 108 if (mCheckMinSdk) { 109 AaptParser aaptParser = doAaptParse(apkFile.getFile()); 110 if (aaptParser == null) { 111 throw new TargetSetupError( 112 String.format("Failed to extract info from '%s' using aapt", 113 apkFile.getFile().getName()), device.getDeviceDescriptor()); 114 } 115 if (device.getApiLevel() < aaptParser.getSdkVersion()) { 116 CLog.w("Skipping installing apk %s on device %s because " + 117 "SDK level require is %d, but device SDK level is %d", 118 apkFile.toString(), device.getSerialNumber(), 119 aaptParser.getSdkVersion(), device.getApiLevel()); 120 continue; 121 } 122 } 123 String result = device.installPackage(apkFile.getFile(), true, 124 mInstallFlags.toArray(new String[mInstallFlags.size()])); 125 if (result != null) { 126 // typically install failures means something is wrong with apk. 127 // TODO: in future add more logic to throw targetsetup vs build vs 128 // devicenotavail depending on error code 129 throw new BuildError(String.format( 130 "Failed to install %s on %s. Reason: %s", 131 apkFile.getFile().getName(), device.getSerialNumber(), result), 132 device.getDeviceDescriptor()); 133 } 134 if (mUninstall && !mUninstallAll) { 135 addPackageNameToUninstall(apkFile.getFile(), device); 136 } 137 } 138 } 139 140 if (!mPostInstallCmds.isEmpty()){ 141 for (String cmd : mPostInstallCmds) { 142 // If the command had any output, the executeShellCommand method will log it at the 143 // VERBOSE level; so no need to do any logging from here. 144 CLog.d("About to run setup command on device %s: %s", device.getSerialNumber(), cmd); 145 device.executeShellCommand(cmd, new CollectingOutputReceiver(), 146 mPostInstallCmdTimeout, TimeUnit.MILLISECONDS, 1); 147 } 148 } 149 } 150 151 /** 152 * Helper to parse an apk file with aapt. 153 */ 154 @VisibleForTesting doAaptParse(File apkFile)155 AaptParser doAaptParse(File apkFile) { 156 return AaptParser.parse(apkFile); 157 } 158 addPackageNameToUninstall(File apkFile, ITestDevice device)159 private void addPackageNameToUninstall(File apkFile, ITestDevice device) 160 throws TargetSetupError { 161 AaptParser aaptParser = doAaptParse(apkFile); 162 if (aaptParser == null) { 163 throw new TargetSetupError(String.format("Failed to extract info from '%s' using aapt", 164 apkFile.getAbsolutePath()), device.getDeviceDescriptor()); 165 } 166 if (aaptParser.getPackageName() == null) { 167 throw new TargetSetupError(String.format( 168 "Failed to find package name for '%s' using aapt", apkFile.getAbsolutePath()), 169 device.getDeviceDescriptor()); 170 } 171 mInstalledPkgs.add(aaptParser.getPackageName()); 172 } 173 174 /** 175 * {@inheritDoc} 176 */ 177 @Override tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e)178 public void tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e) 179 throws DeviceNotAvailableException { 180 if (e instanceof DeviceNotAvailableException) { 181 return; 182 } 183 // reboot device before uninstalling apps, in case device is wedged 184 if (mReboot) { 185 device.reboot(); 186 } 187 if (mUninstall && !mUninstallAll) { 188 for (String pkgName : mInstalledPkgs) { 189 String result = device.uninstallPackage(pkgName); 190 if (result != null) { 191 CLog.e("Failed to uninstall %s: %s", pkgName, result); 192 // TODO: consider throwing here 193 } 194 } 195 } 196 if (mUninstallAll && !uninstallAllApps(device)) { 197 // cannot cleanup device! Bad things may happen in future tests. Take device out 198 // of service 199 // TODO: in future, consider doing more sophisticated recovery operations 200 throw new DeviceNotAvailableException(String.format( 201 "Failed to uninstall apps on %s", device.getSerialNumber()), 202 device.getSerialNumber()); 203 } 204 } 205 206 /** 207 * Make multiple attempts to uninstall apps, aborting if failed 208 * 209 * @return {@code true} if all apps were uninstalled, {@code false} otherwise. 210 */ uninstallAllApps(ITestDevice device)211 private boolean uninstallAllApps(ITestDevice device) throws DeviceNotAvailableException { 212 // TODO: consider moving this to ITestDevice, so more sophisticated recovery attempts 213 // can be performed 214 for (int i = 0; i < 3; i++) { 215 Set<String> pkgs = getAllAppsToUninstall(device); 216 if (pkgs.isEmpty()) { 217 return true; 218 } 219 for (String pkg : pkgs) { 220 String result = device.uninstallPackage(pkg); 221 if (result != null) { 222 CLog.w("Uninstall of %s on %s failed: %s", pkg, device.getSerialNumber(), 223 result); 224 } 225 } 226 } 227 // check getAppsToUninstall one more time, since last attempt through loop might have been 228 // successful 229 return getAllAppsToUninstall(device).isEmpty(); 230 } 231 getAllAppsToUninstall(ITestDevice device)232 private Set<String> getAllAppsToUninstall(ITestDevice device) throws DeviceNotAvailableException { 233 Set<String> pkgs = device.getUninstallablePackageNames(); 234 pkgs.removeAll(mSkipUninstallPkgs); 235 return pkgs; 236 } 237 } 238