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