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.annotations.VisibleForTesting;
19 import com.android.tradefed.build.IBuildInfo;
20 import com.android.tradefed.build.IDeviceBuildInfo;
21 import com.android.tradefed.command.remote.DeviceDescriptor;
22 import com.android.tradefed.config.Option;
23 import com.android.tradefed.config.Option.Importance;
24 import com.android.tradefed.config.OptionClass;
25 import com.android.tradefed.device.DeviceNotAvailableException;
26 import com.android.tradefed.device.ITestDevice;
27 import com.android.tradefed.invoker.IInvocationContext;
28 import com.android.tradefed.invoker.InvocationContext;
29 import com.android.tradefed.invoker.TestInformation;
30 import com.android.tradefed.log.LogUtil.CLog;
31 import com.android.tradefed.result.error.DeviceErrorIdentifier;
32 import com.android.tradefed.result.error.InfraErrorIdentifier;
33 import com.android.tradefed.testtype.IAbi;
34 import com.android.tradefed.testtype.IAbiReceiver;
35 import com.android.tradefed.util.AaptParser;
36 import com.android.tradefed.util.AbiFormatter;
37 import com.android.tradefed.util.BuildTestsZipUtils;
38 
39 import com.google.common.collect.ImmutableList;
40 import com.google.common.collect.ImmutableListMultimap;
41 import com.google.common.collect.Multimaps;
42 
43 import java.io.File;
44 import java.io.IOException;
45 import java.nio.file.Files;
46 import java.nio.file.Path;
47 import java.util.ArrayList;
48 import java.util.Arrays;
49 import java.util.Collection;
50 import java.util.HashSet;
51 import java.util.LinkedHashMap;
52 import java.util.List;
53 import java.util.Map;
54 import java.util.Set;
55 import java.util.stream.Collectors;
56 import java.util.stream.Stream;
57 
58 /**
59  * A {@link ITargetPreparer} that installs one or more apps from a {@link
60  * IDeviceBuildInfo#getTestsDir()} folder onto device.
61  *
62  * <p>This preparer will look in alternate directories if the tests zip does not exist or does not
63  * contain the required apk. The search will go in order from the last alternative dir specified to
64  * the first.
65  */
66 @OptionClass(alias = "tests-zip-app")
67 public class TestAppInstallSetup extends BaseTargetPreparer implements IAbiReceiver {
68 
69     /** The mode the apk should be install in. */
70     private enum InstallMode {
71         FULL,
72         INSTANT,
73     }
74 
75     // An error message that occurs when a test APK is already present on the DUT,
76     // but cannot be updated. When this occurs, the package is removed from the
77     // device so that installation can continue like normal.
78     private static final String INSTALL_FAILED_UPDATE_INCOMPATIBLE =
79             "INSTALL_FAILED_UPDATE_INCOMPATIBLE";
80 
81     @VisibleForTesting static final String TEST_FILE_NAME_OPTION = "test-file-name";
82 
83     @Option(
84             name = TEST_FILE_NAME_OPTION,
85             description =
86                     "the name of an apk file to be installed on device. Can be repeated. Items "
87                             + "that are directories will have any APKs contained therein, "
88                             + "including subdirectories, grouped by package name and installed.",
89             importance = Importance.IF_UNSET)
90     private List<File> mTestFiles = new ArrayList<>();
91 
92     // A string made of split apk file names divided by ",".
93     // See "https://developer.android.com/studio/build/configure-apk-splits" on how to split
94     // apk to several files.
95     @Option(
96             name = "split-apk-file-names",
97             description =
98                     "the split apk file names separted by comma that will be installed on device. "
99                             + "Can be repeated for multiple split apk sets."
100                             + "See https://developer.android.com/studio/build/configure-apk-splits on "
101                             + "how to split apk to several files")
102     private List<String> mSplitApkFileNames = new ArrayList<>();
103 
104     @VisibleForTesting static final String THROW_IF_NOT_FOUND_OPTION = "throw-if-not-found";
105 
106     @Option(
107             name = THROW_IF_NOT_FOUND_OPTION,
108             description = "Throw exception if the specified file is not found.")
109     private boolean mThrowIfNoFile = true;
110 
111     @Option(name = AbiFormatter.FORCE_ABI_STRING,
112             description = AbiFormatter.FORCE_ABI_DESCRIPTION,
113             importance = Importance.IF_UNSET)
114     private String mForceAbi = null;
115 
116     @Option(name = "install-arg",
117             description = "Additional arguments to be passed to install command, "
118                     + "including leading dash, e.g. \"-d\"")
119     private Collection<String> mInstallArgs = new ArrayList<>();
120 
121     @Option(name = "force-queryable",
122             description = "Whether apks should be installed as force queryable.")
123     private boolean mForceQueryable = true;
124 
125     @Option(
126             name = "cleanup-apks",
127             description =
128                     "Whether apks installed should be uninstalled after test. Note that the "
129                             + "preparer does not verify if the apks are successfully removed.")
130     private boolean mCleanup = true;
131 
132     /** @deprecated use test-file-name instead now that it is a File. */
133     @Deprecated
134     @Option(
135             name = "alt-dir",
136             description =
137                     "Alternate directory to look for the apk if the apk is not in the tests "
138                             + "zip file. For each alternate dir, will look in //, //data/app, "
139                             + "//DATA/app, //DATA/app/apk_name/ and //DATA/priv-app/apk_name/. "
140                             + "Can be repeated. Look for apks in last alt-dir first.")
141     private List<File> mAltDirs = new ArrayList<>();
142 
143     /** @deprecated goes in pair with alt-dir which is deprecated */
144     @Deprecated
145     @Option(
146             name = "alt-dir-behavior",
147             description =
148                     "The order of alternate directory to be used when searching for apks to "
149                             + "install")
150     private AltDirBehavior mAltDirBehavior = AltDirBehavior.FALLBACK;
151 
152     @Option(name = "instant-mode", description = "Whether or not to install apk in instant mode.")
153     private boolean mInstantMode = false;
154 
155     @Option(
156         name = "force-install-mode",
157         description =
158                 "Force the preparer to ignore instant-mode option, and install in the requested mode."
159     )
160     private InstallMode mInstallationMode = null;
161 
162     private IAbi mAbi = null;
163     private Integer mUserId = null;
164     private Boolean mGrantPermission = null;
165 
166     private Set<String> mPackagesInstalled = null;
167     private TestInformation mTestInfo;
168 
setTestInformation(TestInformation testInfo)169     protected void setTestInformation(TestInformation testInfo) {
170         mTestInfo = testInfo;
171     }
172 
173     /** Adds a file or directory to the list of apks to installed. */
addTestFile(File file)174     public void addTestFile(File file) {
175         mTestFiles.add(file);
176     }
177 
178     /** Adds a file name to the list of apks to installed. */
addTestFileName(String fileName)179     public void addTestFileName(String fileName) {
180         addTestFile(new File(fileName));
181     }
182 
183     @VisibleForTesting
clearTestFile()184     void clearTestFile() {
185         mTestFiles.clear();
186     }
187 
188     /**
189      * Adds a set of file names divided by ',' in a string to be installed as split apks
190      *
191      * @param fileNames a string of file names divided by ','
192      */
addSplitApkFileNames(String fileNames)193     public void addSplitApkFileNames(String fileNames) {
194         mSplitApkFileNames.add(fileNames);
195     }
196 
197     @VisibleForTesting
clearSplitApkFileNames()198     void clearSplitApkFileNames() {
199         mSplitApkFileNames.clear();
200     }
201 
202     /** Returns a copy of the list of specified test apk names. */
getTestsFileName()203     public List<File> getTestsFileName() {
204         return mTestFiles;
205     }
206 
207     /** Sets whether or not the installed apk should be cleaned on tearDown */
setCleanApk(boolean shouldClean)208     public void setCleanApk(boolean shouldClean) {
209         mCleanup = shouldClean;
210     }
211 
212     /**
213      * If the apk should be installed for a particular user, sets the id of the user to install for.
214      */
setUserId(int userId)215     public void setUserId(int userId) {
216         mUserId = userId;
217     }
218 
219     /** If a userId is provided, grantPermission can be set for the apk installation. */
setShouldGrantPermission(boolean shouldGrant)220     public void setShouldGrantPermission(boolean shouldGrant) {
221         mGrantPermission = shouldGrant;
222     }
223 
224     /** Adds one apk installation arg to be used. */
addInstallArg(String arg)225     public void addInstallArg(String arg) {
226         mInstallArgs.add(arg);
227     }
228 
229     /**
230      * Resolve the actual apk path based on testing artifact information inside build info.
231      *
232      * @param testInfo The {@link TestInformation} for the invocation.
233      * @param apkFileName filename of the apk to install
234      * @return a {@link File} representing the physical apk file on host or {@code null} if the file
235      *     does not exist.
236      */
getLocalPathForFilename(TestInformation testInfo, String apkFileName)237     protected File getLocalPathForFilename(TestInformation testInfo, String apkFileName)
238             throws TargetSetupError {
239         try {
240             return BuildTestsZipUtils.getApkFile(
241                     testInfo.getBuildInfo(),
242                     apkFileName,
243                     mAltDirs,
244                     mAltDirBehavior,
245                     false /* use resource as fallback */,
246                     null /* device signing key */);
247         } catch (IOException ioe) {
248             throw new TargetSetupError(
249                     String.format(
250                             "failed to resolve apk path for apk %s in build %s",
251                             apkFileName, testInfo.getBuildInfo().toString()),
252                     ioe,
253                     testInfo.getDevice().getDeviceDescriptor(),
254                     InfraErrorIdentifier.ARTIFACT_NOT_FOUND);
255         }
256     }
257 
258     /** @deprecated Temporary backward compatible callback. */
259     @Deprecated
260     @Override
setUp(ITestDevice device, IBuildInfo buildInfo)261     public void setUp(ITestDevice device, IBuildInfo buildInfo)
262             throws TargetSetupError, BuildError, DeviceNotAvailableException {
263         IInvocationContext context = new InvocationContext();
264         context.addAllocatedDevice("device", device);
265         context.addDeviceBuildInfo("device", buildInfo);
266         TestInformation backwardCompatible =
267                 TestInformation.newBuilder().setInvocationContext(context).build();
268         setUp(backwardCompatible);
269     }
270 
271     /** {@inheritDoc} */
272     @Override
setUp(TestInformation testInfo)273     public void setUp(TestInformation testInfo)
274             throws TargetSetupError, BuildError, DeviceNotAvailableException {
275         mTestInfo = testInfo;
276         if (mTestFiles.isEmpty() && mSplitApkFileNames.isEmpty()) {
277             CLog.i("No test apps to install, skipping");
278             return;
279         }
280         if (mCleanup) {
281             mPackagesInstalled = new HashSet<>();
282         }
283 
284         // resolve abi flags
285         if (mAbi != null && mForceAbi != null) {
286             throw new IllegalStateException("cannot specify both abi flags: --abi and --force-abi");
287         }
288         String abiName = null;
289         if (mAbi != null) {
290             abiName = mAbi.getName();
291         } else if (mForceAbi != null) {
292             abiName = AbiFormatter.getDefaultAbi(getDevice(), mForceAbi);
293         }
294 
295         // Set all the extra install args outside the loop to avoid adding them several times.
296         if (abiName != null && testInfo.getDevice().getApiLevel() > 20) {
297             mInstallArgs.add(String.format("--abi %s", abiName));
298         }
299         // Handle instant mode: if we are forced in one installation mode or not.
300         // Some preparer are locked in one installation mode or another, they ignore the
301         // 'instant-mode' option and stays in their mode.
302         if (mInstallationMode != null) {
303             if (InstallMode.INSTANT.equals(mInstallationMode)) {
304                 mInstallArgs.add("--instant");
305             }
306         } else {
307             if (mInstantMode) {
308                 mInstallArgs.add("--instant");
309             }
310         }
311 
312         if (mForceQueryable && getDevice().isAppEnumerationSupported()) {
313             mInstallArgs.add("--force-queryable");
314         }
315 
316         for (File testAppName : mTestFiles) {
317             Map<File, String> appFilesAndPackages =
318                     resolveApkFiles(
319                             testInfo,
320                             findApkFiles(testAppName, testInfo.getDevice().getDeviceDescriptor()));
321             installer(testInfo, appFilesAndPackages);
322         }
323 
324         for (String testAppNames : mSplitApkFileNames) {
325             List<String> apkNames = Arrays.asList(testAppNames.split(","));
326             List<File> apkFileNames =
327                     apkNames.stream().map(a -> new File(a)).collect(Collectors.toList());
328             Map<File, String> appFilesAndPackages = resolveApkFiles(testInfo, apkFileNames);
329             installer(testInfo, appFilesAndPackages);
330         }
331     }
332 
333     /**
334      * Returns the device that the preparer should apply to.
335      *
336      * @throws TargetSetupError
337      */
getDevice()338     public ITestDevice getDevice() throws TargetSetupError {
339         return mTestInfo.getDevice();
340     }
341 
getTestInfo()342     public TestInformation getTestInfo() {
343         return mTestInfo;
344     }
345 
346     @Override
setAbi(IAbi abi)347     public void setAbi(IAbi abi) {
348         mAbi = abi;
349     }
350 
351     @Override
getAbi()352     public IAbi getAbi() {
353         return mAbi;
354     }
355 
356     /**
357      * Sets whether or not --instant should be used when installing the apk. Will have no effect if
358      * force-install-mode is set.
359      */
setInstantMode(boolean mode)360     public final void setInstantMode(boolean mode) {
361         mInstantMode = mode;
362     }
363 
364     /** Returns whether or not instant mode installation has been enabled. */
isInstantMode()365     public final boolean isInstantMode() {
366         return mInstantMode;
367     }
368 
369     /** {@inheritDoc} */
370     @Override
tearDown(TestInformation testInfo, Throwable e)371     public void tearDown(TestInformation testInfo, Throwable e) throws DeviceNotAvailableException {
372         mTestInfo = testInfo;
373         if (mCleanup && mPackagesInstalled != null && !(e instanceof DeviceNotAvailableException)) {
374             for (String packageName : mPackagesInstalled) {
375                 try {
376                     uninstallPackage(getDevice(), packageName);
377                 } catch (TargetSetupError tse) {
378                     CLog.e(tse);
379                 }
380             }
381         }
382     }
383 
384     /**
385      * Set an alternate directory.
386      */
setAltDir(File altDir)387     public void setAltDir(File altDir) {
388         mAltDirs.add(altDir);
389     }
390 
391     /**
392      * Set an alternate directory behaviors.
393      */
setAltDirBehavior(AltDirBehavior altDirBehavior)394     public void setAltDirBehavior(AltDirBehavior altDirBehavior) {
395         mAltDirBehavior = altDirBehavior;
396     }
397 
398     /** Returns True if Apks will be cleaned up during tear down. */
isCleanUpEnabled()399     public boolean isCleanUpEnabled() {
400         return mCleanup;
401     }
402 
403     /**
404      * Attempt to install an package or split package on the device.
405      *
406      * @param testInfo the {@link TestInformation} for the invocation
407      * @param appFilesAndPackages The apks and their package to be installed.
408      */
installer(TestInformation testInfo, Map<File, String> appFilesAndPackages)409     protected void installer(TestInformation testInfo, Map<File, String> appFilesAndPackages)
410             throws TargetSetupError, DeviceNotAvailableException {
411         ITestDevice device = testInfo.getDevice();
412 
413         // TODO(hzalek): Consider changing resolveApkFiles's return to a Multimap to avoid building
414         // it here.
415         ImmutableListMultimap<String, File> packageToFiles =
416                 ImmutableListMultimap.copyOf(appFilesAndPackages.entrySet()).inverse();
417 
418         for (Map.Entry<String, List<File>> e : Multimaps.asMap(packageToFiles).entrySet()) {
419             installSinglePackage(device, e.getKey(), e.getValue());
420         }
421     }
422 
installSinglePackage( ITestDevice testDevice, String packageName, List<File> apkFiles)423     private void installSinglePackage(
424             ITestDevice testDevice, String packageName, List<File> apkFiles)
425             throws TargetSetupError, DeviceNotAvailableException {
426 
427         if (apkFiles.isEmpty()) {
428             return;
429         }
430 
431         CLog.d("Installing apk %s with %s ...", packageName, apkFiles);
432         String result = installPackage(testDevice, apkFiles);
433 
434         if (result != null) {
435             if (result.startsWith(INSTALL_FAILED_UPDATE_INCOMPATIBLE)) {
436                 // Try to uninstall package and reinstall.
437                 uninstallPackage(testDevice, packageName);
438                 result = installPackage(testDevice, apkFiles);
439             }
440         }
441 
442         if (result != null) {
443             throw new TargetSetupError(
444                     String.format(
445                             "Failed to install %s with %s on %s. Reason: '%s'",
446                             packageName, apkFiles, testDevice.getSerialNumber(), result),
447                     testDevice.getDeviceDescriptor(),
448                     DeviceErrorIdentifier.APK_INSTALLATION_FAILED);
449         }
450 
451         if (mCleanup) {
452             mPackagesInstalled.add(packageName);
453         }
454     }
455 
456     /** Helper to resolve some apk to their File and Package. */
resolveApkFiles(TestInformation testInfo, List<File> apkFiles)457     protected Map<File, String> resolveApkFiles(TestInformation testInfo, List<File> apkFiles)
458             throws TargetSetupError {
459         Map<File, String> appFiles = new LinkedHashMap<>();
460         ITestDevice device = testInfo.getDevice();
461         for (File apkFile : apkFiles) {
462             File testAppFile = null;
463             if (apkFile.isAbsolute()) {
464                 testAppFile = apkFile;
465             }
466             if (testAppFile == null) {
467                 testAppFile = getLocalPathForFilename(testInfo, apkFile.getName());
468             }
469             if (testAppFile == null) {
470                 if (mThrowIfNoFile) {
471                     throw new TargetSetupError(
472                             String.format("Test app %s was not found.", apkFile.getName()),
473                             device.getDeviceDescriptor(),
474                             InfraErrorIdentifier.ARTIFACT_NOT_FOUND);
475                 } else {
476                     CLog.d("Test app %s was not found.", apkFile.getName());
477                     continue;
478                 }
479             }
480             if (!testAppFile.canRead()) {
481                 if (mThrowIfNoFile) {
482                     throw new TargetSetupError(
483                             String.format("Could not read file %s.", testAppFile.toString()),
484                             device.getDeviceDescriptor());
485                 } else {
486                     CLog.d("Could not read file %s.", testAppFile.toString());
487                     continue;
488                 }
489             }
490 
491             appFiles.put(testAppFile, parsePackageName(testAppFile, device.getDeviceDescriptor()));
492         }
493         return appFiles;
494     }
495 
496     /**
497      * Returns the provided file if not a directory or all APK files contained in the directory tree
498      * rooted at the provided path otherwise.
499      */
findApkFiles(File fileOrDirectory, DeviceDescriptor deviceDescriptor)500     private List<File> findApkFiles(File fileOrDirectory, DeviceDescriptor deviceDescriptor)
501             throws TargetSetupError {
502 
503         if (!fileOrDirectory.isDirectory()) {
504             return ImmutableList.of(fileOrDirectory);
505         }
506 
507         List<File> apkFiles;
508 
509         try (Stream<Path> paths = Files.walk(fileOrDirectory.toPath())) {
510             apkFiles =
511                     paths.filter(p -> p.toString().endsWith(".apk"))
512                             .filter(Files::isRegularFile)
513                             .map(Path::toFile)
514                             .collect(Collectors.toList());
515         } catch (IOException e) {
516             throw new TargetSetupError(
517                     String.format(
518                             "Could not list files of specified directory: %s", fileOrDirectory),
519                     e,
520                     deviceDescriptor);
521         }
522 
523         if (mThrowIfNoFile && apkFiles.isEmpty()) {
524             throw new TargetSetupError(
525                     String.format(
526                             "Could not find any files in specified directory: %s", fileOrDirectory),
527                     deviceDescriptor);
528         }
529 
530         return apkFiles;
531     }
532 
533     /**
534      * Attempt to install a package or split package on the device.
535      *
536      * @param device the {@link ITestDevice} to install package
537      * @param appFiles List of Files. If apkFiles contains only one apk file, the app will be
538      *     installed as a whole package with single file. If apkFiles contains more than one name,
539      *     the app will be installed as split apk with multiple files.
540      */
installPackage(ITestDevice device, List<File> appFiles)541     private String installPackage(ITestDevice device, List<File> appFiles)
542             throws DeviceNotAvailableException {
543         // Handle the different install use cases (with or without a user)
544         if (mUserId == null) {
545             if (appFiles.size() == 1) {
546                 return device.installPackage(
547                         appFiles.get(0), true, mInstallArgs.toArray(new String[] {}));
548             } else {
549                 return device.installPackages(
550                         appFiles, true, mInstallArgs.toArray(new String[] {}));
551             }
552         } else if (mGrantPermission != null) {
553             if (appFiles.size() == 1) {
554                 return device.installPackageForUser(
555                         appFiles.get(0),
556                         true,
557                         mGrantPermission,
558                         mUserId,
559                         mInstallArgs.toArray(new String[] {}));
560             } else {
561                 return device.installPackagesForUser(
562                         appFiles,
563                         true,
564                         mGrantPermission,
565                         mUserId,
566                         mInstallArgs.toArray(new String[] {}));
567             }
568         } else {
569             if (appFiles.size() == 1) {
570                 return device.installPackageForUser(
571                         appFiles.get(0), true, mUserId, mInstallArgs.toArray(new String[] {}));
572             } else {
573                 return device.installPackagesForUser(
574                         appFiles, true, mUserId, mInstallArgs.toArray(new String[] {}));
575             }
576         }
577     }
578 
579     /** Attempt to remove the package from the device. */
uninstallPackage(ITestDevice device, String packageName)580     protected void uninstallPackage(ITestDevice device, String packageName)
581             throws DeviceNotAvailableException {
582         String msg = device.uninstallPackage(packageName);
583         if (msg != null) {
584             CLog.w(String.format("error uninstalling package '%s': %s", packageName, msg));
585         }
586     }
587 
588     /** Get the package name from the test app. */
parsePackageName(File testAppFile, DeviceDescriptor deviceDescriptor)589     protected String parsePackageName(File testAppFile, DeviceDescriptor deviceDescriptor)
590             throws TargetSetupError {
591         AaptParser parser = AaptParser.parse(testAppFile);
592         if (parser == null) {
593             throw new TargetSetupError(
594                     "apk installed but AaptParser failed",
595                     deviceDescriptor,
596                     DeviceErrorIdentifier.AAPT_PARSER_FAILED);
597         }
598         return parser.getPackageName();
599     }
600 }
601 
602