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.ArrayList;
36 import java.util.Collection;
37 import java.util.List;
38 
39 /**
40  * A {@link ITargetPreparer} that installs one or more apps from a {@link
41  * IDeviceBuildInfo#getTestsDir()} folder onto device.
42  *
43  * <p>This preparer will look in alternate directories if the tests zip does not exist or does not
44  * contain the required apk. The search will go in order from the last alternative dir specified to
45  * the first.
46  */
47 @OptionClass(alias = "tests-zip-app")
48 public class TestAppInstallSetup extends BaseTargetPreparer
49         implements ITargetCleaner, IAbiReceiver {
50 
51     /** The mode the apk should be install in. */
52     private enum InstallMode {
53         FULL,
54         INSTANT,
55     }
56 
57     // An error message that occurs when a test APK is already present on the DUT,
58     // but cannot be updated. When this occurs, the package is removed from the
59     // device so that installation can continue like normal.
60     private static final String INSTALL_FAILED_UPDATE_INCOMPATIBLE =
61             "INSTALL_FAILED_UPDATE_INCOMPATIBLE";
62 
63     @Option(name = "test-file-name",
64             description = "the name of a test zip file to install on device. Can be repeated.",
65             importance = Importance.IF_UNSET)
66     private Collection<String> mTestFileNames = new ArrayList<String>();
67 
68     @Option(
69         name = "throw-if-not-found",
70         description = "Throw exception if the specified file is not found."
71     )
72     private boolean mThrowIfNoFile = true;
73 
74     @Option(name = AbiFormatter.FORCE_ABI_STRING,
75             description = AbiFormatter.FORCE_ABI_DESCRIPTION,
76             importance = Importance.IF_UNSET)
77     private String mForceAbi = null;
78 
79     @Option(name = "install-arg",
80             description = "Additional arguments to be passed to install command, "
81                     + "including leading dash, e.g. \"-d\"")
82     private Collection<String> mInstallArgs = new ArrayList<>();
83 
84     @Option(name = "cleanup-apks",
85             description = "Whether apks installed should be uninstalled after test. Note that the "
86                     + "preparer does not verify if the apks are successfully removed.")
87     private boolean mCleanup = false;
88 
89     @Option(name = "alt-dir",
90             description = "Alternate directory to look for the apk if the apk is not in the tests "
91                     + "zip file. For each alternate dir, will look in //, //data/app, //DATA/app, "
92                     + "//DATA/app/apk_name/ and //DATA/priv-app/apk_name/. Can be repeated. "
93                     + "Look for apks in last alt-dir first.")
94     private List<File> mAltDirs = new ArrayList<>();
95 
96     @Option(name = "alt-dir-behavior", description = "The order of alternate directory to be used "
97             + "when searching for apks to install")
98     private AltDirBehavior mAltDirBehavior = AltDirBehavior.FALLBACK;
99 
100     @Option(name = "instant-mode", description = "Whether or not to install apk in instant mode.")
101     private boolean mInstantMode = false;
102 
103     @Option(
104         name = "force-install-mode",
105         description =
106                 "Force the preparer to ignore instant-mode option, and install in the requested mode."
107     )
108     private InstallMode mInstallMode = null;
109 
110     private IAbi mAbi = null;
111     private Integer mUserId = null;
112     private Boolean mGrantPermission = null;
113 
114     private List<String> mPackagesInstalled = null;
115 
116     /**
117      * Adds a file to the list of apks to install
118      *
119      * @param fileName
120      */
addTestFileName(String fileName)121     public void addTestFileName(String fileName) {
122         mTestFileNames.add(fileName);
123     }
124 
125     /** Returns a copy of the list of specified test apk names. */
getTestsFileName()126     public List<String> getTestsFileName() {
127         return new ArrayList<String>(mTestFileNames);
128     }
129 
130     /** Sets whether or not the installed apk should be cleaned on tearDown */
setCleanApk(boolean shouldClean)131     public void setCleanApk(boolean shouldClean) {
132         mCleanup = shouldClean;
133     }
134 
135     /**
136      * If the apk should be installed for a particular user, sets the id of the user to install for.
137      */
setUserId(int userId)138     public void setUserId(int userId) {
139         mUserId = userId;
140     }
141 
142     /** If a userId is provided, grantPermission can be set for the apk installation. */
setShouldGrantPermission(boolean shouldGrant)143     public void setShouldGrantPermission(boolean shouldGrant) {
144         mGrantPermission = shouldGrant;
145     }
146 
147     /** Adds one apk installation arg to be used. */
addInstallArg(String arg)148     public void addInstallArg(String arg) {
149         mInstallArgs.add(arg);
150     }
151 
152     /**
153      * Resolve the actual apk path based on testing artifact information inside build info.
154      *
155      * @param buildInfo build artifact information
156      * @param apkFileName filename of the apk to install
157      * @param device the {@link ITestDevice} being prepared
158      * @return a {@link File} representing the physical apk file on host or {@code null} if the file
159      *     does not exist.
160      */
getLocalPathForFilename( IBuildInfo buildInfo, String apkFileName, ITestDevice device)161     protected File getLocalPathForFilename(
162             IBuildInfo buildInfo, String apkFileName, ITestDevice device) throws TargetSetupError {
163         try {
164             return BuildTestsZipUtils.getApkFile(buildInfo, apkFileName, mAltDirs, mAltDirBehavior,
165                     false /* use resource as fallback */,
166                     null /* device signing key */);
167         } catch (IOException ioe) {
168             throw new TargetSetupError("failed to resolve apk path", ioe,
169                     device.getDeviceDescriptor());
170         }
171     }
172 
173     /** {@inheritDoc} */
174     @Override
setUp(ITestDevice device, IBuildInfo buildInfo)175     public void setUp(ITestDevice device, IBuildInfo buildInfo)
176             throws TargetSetupError, DeviceNotAvailableException {
177         if (mTestFileNames == null || mTestFileNames.size() == 0) {
178             CLog.i("No test apps to install, skipping");
179             return;
180         }
181         if (mCleanup) {
182             mPackagesInstalled = new ArrayList<>();
183         }
184 
185         for (String testAppName : mTestFileNames) {
186             if (testAppName == null || testAppName.trim().isEmpty()) {
187                 continue;
188             }
189             File testAppFile = getLocalPathForFilename(buildInfo, testAppName, device);
190             if (testAppFile == null) {
191                 if (mThrowIfNoFile) {
192                     throw new TargetSetupError(
193                             String.format("Test app %s was not found.", testAppName),
194                             device.getDeviceDescriptor());
195                 } else {
196                     CLog.d("Test app %s was not found.", testAppName);
197                     continue;
198                 }
199             }
200             if (!testAppFile.canRead()) {
201                 if (mThrowIfNoFile) {
202                     throw new TargetSetupError(
203                             String.format("Could not read file %s.", testAppName),
204                             device.getDeviceDescriptor());
205                 } else {
206                     CLog.d("Could not read file %s.", testAppName);
207                     continue;
208                 }
209             }
210             // resolve abi flags
211             if (mAbi != null && mForceAbi != null) {
212                 throw new IllegalStateException("cannot specify both abi flags");
213             }
214             String abiName = null;
215             if (mAbi != null) {
216                 abiName = mAbi.getName();
217             } else if (mForceAbi != null) {
218                 abiName = AbiFormatter.getDefaultAbi(device, mForceAbi);
219             }
220             if (abiName != null) {
221                 mInstallArgs.add(String.format("--abi %s", abiName));
222             }
223             // Handle instant mode: if we are forced in one installation mode or not.
224             if (mInstallMode != null) {
225                 if (InstallMode.INSTANT.equals(mInstallMode)) {
226                     mInstallArgs.add("--instant");
227                 }
228             } else {
229                 if (mInstantMode) {
230                     mInstallArgs.add("--instant");
231                 }
232             }
233 
234             String packageName = parsePackageName(testAppFile, device.getDeviceDescriptor());
235             CLog.d("Installing apk from %s ...", testAppFile.getAbsolutePath());
236             String result = installPackage(device, testAppFile);
237             if (result != null) {
238                 if (result.startsWith(INSTALL_FAILED_UPDATE_INCOMPATIBLE)) {
239                     // Try to uninstall package and reinstall.
240                     uninstallPackage(device, packageName);
241                     result = installPackage(device, testAppFile);
242                 }
243             }
244             if (result != null) {
245                 throw new TargetSetupError(
246                         String.format("Failed to install %s on %s. Reason: '%s'", testAppName,
247                                 device.getSerialNumber(), result), device.getDeviceDescriptor());
248             }
249             if (mCleanup) {
250                 mPackagesInstalled.add(packageName);
251             }
252         }
253     }
254 
255     @Override
setAbi(IAbi abi)256     public void setAbi(IAbi abi) {
257         mAbi = abi;
258     }
259 
260     @Override
getAbi()261     public IAbi getAbi() {
262         return mAbi;
263     }
264 
265     /**
266      * {@inheritDoc}
267      */
268     @Override
tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e)269     public void tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e)
270             throws DeviceNotAvailableException {
271         if (mCleanup && mPackagesInstalled != null && !(e instanceof DeviceNotAvailableException)) {
272             for (String packageName : mPackagesInstalled) {
273                 uninstallPackage(device, packageName);
274             }
275         }
276     }
277 
278     /**
279      * Set an alternate directory.
280      */
setAltDir(File altDir)281     public void setAltDir(File altDir) {
282         mAltDirs.add(altDir);
283     }
284 
285     /**
286      * Set an alternate directory behaviors.
287      */
setAltDirBehavior(AltDirBehavior altDirBehavior)288     public void setAltDirBehavior(AltDirBehavior altDirBehavior) {
289         mAltDirBehavior = altDirBehavior;
290     }
291 
292     /** Attempt to install a package on the device. */
installPackage(ITestDevice device, File testAppFile)293     private String installPackage(ITestDevice device, File testAppFile)
294             throws DeviceNotAvailableException {
295         // Handle the different install use cases (with or without a user)
296         if (mUserId == null) {
297             return device.installPackage(testAppFile, true, mInstallArgs.toArray(new String[] {}));
298         } else if (mGrantPermission != null) {
299             return device.installPackageForUser(
300                     testAppFile,
301                     true,
302                     mGrantPermission,
303                     mUserId,
304                     mInstallArgs.toArray(new String[] {}));
305         } else {
306             return device.installPackageForUser(
307                     testAppFile, true, mUserId, mInstallArgs.toArray(new String[] {}));
308         }
309     }
310 
311     /** Attempt to remove the package from the device. */
uninstallPackage(ITestDevice device, String packageName)312     private void uninstallPackage(ITestDevice device, String packageName)
313             throws DeviceNotAvailableException {
314         String msg = device.uninstallPackage(packageName);
315         if (msg != null) {
316             CLog.w(String.format("error uninstalling package '%s': %s", packageName, msg));
317         }
318     }
319 
320     /** Get the package name from the test app. */
parsePackageName(File testAppFile, DeviceDescriptor deviceDescriptor)321     protected String parsePackageName(File testAppFile, DeviceDescriptor deviceDescriptor)
322             throws TargetSetupError {
323         AaptParser parser = AaptParser.parse(testAppFile);
324         if (parser == null) {
325             throw new TargetSetupError("apk installed but AaptParser failed", deviceDescriptor);
326         }
327         return parser.getPackageName();
328     }
329 }
330