• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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