• 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 static com.android.tradefed.targetprep.UserHelper.RUN_TESTS_AS_USER_KEY;
19 import static com.android.tradefed.targetprep.VisibleBackgroundUserPreparer.INSTALL_TEST_APK_FOR_ALL_USERS;
20 
21 import com.android.annotations.VisibleForTesting;
22 import com.android.incfs.install.IncrementalInstallSession;
23 import com.android.incfs.install.IncrementalInstallSession.Builder;
24 import com.android.incfs.install.PendingBlock;
25 import com.android.incfs.install.adb.ddmlib.DeviceConnection;
26 import com.android.incfs.install.adb.ddmlib.DeviceLogger;
27 import com.android.tradefed.build.IBuildInfo;
28 import com.android.tradefed.build.IDeviceBuildInfo;
29 import com.android.tradefed.config.Option;
30 import com.android.tradefed.config.Option.Importance;
31 import com.android.tradefed.config.OptionClass;
32 import com.android.tradefed.device.DeviceNotAvailableException;
33 import com.android.tradefed.device.ITestDevice;
34 import com.android.tradefed.device.NativeDevice;
35 import com.android.tradefed.invoker.IInvocationContext;
36 import com.android.tradefed.invoker.InvocationContext;
37 import com.android.tradefed.invoker.TestInformation;
38 import com.android.tradefed.log.LogUtil.CLog;
39 import com.android.tradefed.observatory.IDiscoverDependencies;
40 import com.android.tradefed.result.error.DeviceErrorIdentifier;
41 import com.android.tradefed.result.error.InfraErrorIdentifier;
42 import com.android.tradefed.targetprep.incremental.ApkChangeDetector;
43 import com.android.tradefed.targetprep.incremental.IIncrementalSetup;
44 import com.android.tradefed.testtype.IAbi;
45 import com.android.tradefed.testtype.IAbiReceiver;
46 import com.android.tradefed.util.AaptParser;
47 import com.android.tradefed.util.AaptParser.AaptVersion;
48 import com.android.tradefed.util.AbiFormatter;
49 import com.android.tradefed.util.BuildTestsZipUtils;
50 import com.android.utils.StdLogger;
51 
52 import com.google.common.collect.ImmutableList;
53 import com.google.common.collect.ImmutableListMultimap;
54 import com.google.common.collect.Multimaps;
55 
56 import java.io.File;
57 import java.io.IOException;
58 import java.nio.file.Files;
59 import java.nio.file.Path;
60 import java.nio.file.Paths;
61 import java.security.SecureRandom;
62 import java.util.ArrayList;
63 import java.util.Arrays;
64 import java.util.Collection;
65 import java.util.HashMap;
66 import java.util.HashSet;
67 import java.util.LinkedHashMap;
68 import java.util.List;
69 import java.util.Map;
70 import java.util.Random;
71 import java.util.Set;
72 import java.util.concurrent.Executors;
73 import java.util.concurrent.TimeUnit;
74 import java.util.stream.Collectors;
75 import java.util.stream.Stream;
76 
77 /**
78  * A {@link ITargetPreparer} that installs one or more apps from a {@link
79  * IDeviceBuildInfo#getTestsDir()} folder onto device.
80  *
81  * <p>This preparer will look in alternate directories if the tests zip does not exist or does not
82  * contain the required apk. The search will go in order from the last alternative dir specified to
83  * the first.
84  */
85 @OptionClass(alias = "tests-zip-app")
86 public class TestAppInstallSetup extends BaseTargetPreparer
87         implements IAbiReceiver, IDiscoverDependencies, IIncrementalSetup {
88 
89     /** The mode the apk should be install in. */
90     private enum InstallMode {
91         FULL,
92         INSTANT,
93     }
94 
95     // An error message that occurs when a test APK is already present on the DUT,
96     // but cannot be updated. When this occurs, the package is removed from the
97     // device so that installation can continue like normal.
98     private static final String INSTALL_FAILED_UPDATE_INCOMPATIBLE =
99             "INSTALL_FAILED_UPDATE_INCOMPATIBLE";
100 
101     @VisibleForTesting static final String TEST_FILE_NAME_OPTION = "test-file-name";
102 
103     @Option(
104             name = TEST_FILE_NAME_OPTION,
105             description =
106                     "the name of an apk file to be installed on device. Can be repeated. Items "
107                             + "that are directories will have any APKs contained therein, "
108                             + "including subdirectories, grouped by package name and installed.",
109             importance = Importance.IF_UNSET)
110     private List<File> mTestFiles = new ArrayList<>();
111 
112     // A string made of split apk file names divided by ",".
113     // See "https://developer.android.com/studio/build/configure-apk-splits" on how to split
114     // apk to several files.
115     @Option(
116             name = "split-apk-file-names",
117             description =
118                     "the split apk file names separted by comma that will be installed on device."
119                         + " Can be repeated for multiple split apk sets. See"
120                         + " https://developer.android.com/studio/build/configure-apk-splits on how"
121                         + " to split apk to several files")
122     private List<String> mSplitApkFileNames = new ArrayList<>();
123 
124     @VisibleForTesting static final String THROW_IF_NOT_FOUND_OPTION = "throw-if-not-found";
125 
126     @Option(
127             name = THROW_IF_NOT_FOUND_OPTION,
128             description = "Throw exception if the specified file is not found.")
129     private boolean mThrowIfNoFile = true;
130 
131     @Option(
132             name = "pin-abi",
133             description = "Pin ABI of the installed app",
134             importance = Importance.IF_UNSET)
135     private String mPinApi = null;
136 
137     @Option(
138             name = AbiFormatter.FORCE_ABI_STRING,
139             description = AbiFormatter.FORCE_ABI_DESCRIPTION,
140             importance = Importance.IF_UNSET)
141     private String mForceAbiBitness = null;
142 
143     @Option(name = "install-arg",
144             description = "Additional arguments to be passed to install command, "
145                     + "including leading dash, e.g. \"-d\"")
146     private Collection<String> mInstallArgs = new ArrayList<>();
147 
148     @Option(
149             name = "force-queryable",
150             description = "Whether apks should be installed as force queryable.")
151     private Boolean mForceQueryable = null;
152 
153     @Option(
154             name = "cleanup-apks",
155             description =
156                     "Whether apks installed should be uninstalled after test. Note that the "
157                             + "preparer does not verify if the apks are successfully removed.")
158     private boolean mCleanup = true;
159 
160     @VisibleForTesting static final String CHECK_MIN_SDK_OPTION = "check-min-sdk";
161 
162     @Option(
163             name = CHECK_MIN_SDK_OPTION,
164             description =
165                     "check app's min sdk prior to install and skip if device api level is too low.")
166     private boolean mCheckMinSdk = false;
167 
168     /** @deprecated use test-file-name instead now that it is a File. */
169     @Deprecated
170     @Option(
171             name = "alt-dir",
172             description =
173                     "Alternate directory to look for the apk if the apk is not in the tests "
174                             + "zip file. For each alternate dir, will look in //, //data/app, "
175                             + "//DATA/app, //DATA/app/apk_name/ and //DATA/priv-app/apk_name/. "
176                             + "Can be repeated. Look for apks in last alt-dir first.")
177     private List<File> mAltDirs = new ArrayList<>();
178 
179     /** @deprecated goes in pair with alt-dir which is deprecated */
180     @Deprecated
181     @Option(
182             name = "alt-dir-behavior",
183             description =
184                     "The order of alternate directory to be used when searching for apks to "
185                             + "install")
186     private AltDirBehavior mAltDirBehavior = AltDirBehavior.FALLBACK;
187 
188     @Option(name = "instant-mode", description = "Whether or not to install apk in instant mode.")
189     private boolean mInstantMode = false;
190 
191     @Option(name = "aapt-version", description = "The version of AAPT for APK parsing.")
192     private AaptVersion mAaptVersion = AaptVersion.AAPT2;
193 
194     @Option(
195             name = "force-install-mode",
196             description =
197                     "Force the preparer to ignore instant-mode option, and install in the"
198                             + " requested mode.")
199     private InstallMode mInstallationMode = null;
200 
201     @Option(
202             name = "incremental",
203             description =
204                     "Performs an installation using incremental streaming. Given the"
205                             + " non-deterministic nature of an incremental installation, it is not"
206                             + " guaranteed that a test run with this option will yield the same"
207                             + " results of previous or future invocations.")
208     @VisibleForTesting
209     protected boolean mIncrementalInstallation = false;
210 
211     @Option(
212             name = "incremental-block-filter",
213             description =
214                     "Decimal representation of the percentage of data blocks"
215                             + " to be filtered out during an incremental"
216                             + " installation.")
217     protected double mBlockFilterPercentage = 0.0;
218 
219     @Option(
220             name = "incremental-install-timeout-secs",
221             description =
222                     "Specifies the maximum permitted duration of" + " an incremental installation.")
223     protected int mIncrementalInstallTimeout = 1800;
224 
225     private IAbi mAbi = null;
226     private Integer mUserId = null;
227     private Boolean mGrantPermission = null;
228     // TODO: b/367468564 - Remove this flag once we have fixed the tests so that installation
229     // for the system user is no longer required when conducting tests for
230     // the secondary_user_on_secondary_display user type.
231     private boolean mInstallForAllUsers  = false;
232 
233     private Set<String> mPackagesInstalled = new HashSet<>();
234     private TestInformation mTestInfo;
235     @VisibleForTesting protected IncrementalInstallSession incrementalInstallSession;
236     private ApkChangeDetector mApkChangeDetector = null;
237 
setTestInformation(TestInformation testInfo)238     protected void setTestInformation(TestInformation testInfo) {
239         mTestInfo = testInfo;
240     }
241 
242     /** Adds a file or directory to the list of apks to installed. */
addTestFile(File file)243     public void addTestFile(File file) {
244         mTestFiles.add(file);
245     }
246 
247     /** Adds a file name to the list of apks to installed. */
addTestFileName(String fileName)248     public void addTestFileName(String fileName) {
249         addTestFile(new File(fileName));
250     }
251 
252     /** Helper to parse an apk file with aapt. */
253     @VisibleForTesting
doAaptParse(File apkFile)254     AaptParser doAaptParse(File apkFile) {
255         return AaptParser.parse(apkFile, mAaptVersion);
256     }
257 
258     @VisibleForTesting
clearTestFile()259     void clearTestFile() {
260         mTestFiles.clear();
261     }
262 
263     /**
264      * Adds a set of file names divided by ',' in a string to be installed as split apks
265      *
266      * @param fileNames a string of file names divided by ','
267      */
addSplitApkFileNames(String fileNames)268     public void addSplitApkFileNames(String fileNames) {
269         mSplitApkFileNames.add(fileNames);
270     }
271 
272     @VisibleForTesting
clearSplitApkFileNames()273     void clearSplitApkFileNames() {
274         mSplitApkFileNames.clear();
275     }
276 
277     /** Returns a copy of the list of specified test apk names. */
getTestsFileName()278     public List<File> getTestsFileName() {
279         return mTestFiles;
280     }
281 
282     /** Sets whether or not the installed apk should be cleaned on tearDown */
setCleanApk(boolean shouldClean)283     public void setCleanApk(boolean shouldClean) {
284         mCleanup = shouldClean;
285     }
286 
287     /**
288      * If the apk should be installed for a particular user, sets the id of the user to install for.
289      */
setUserId(int userId)290     public void setUserId(int userId) {
291         mUserId = userId;
292     }
293 
294     /** If a userId is provided, grantPermission can be set for the apk installation. */
setShouldGrantPermission(boolean shouldGrant)295     public void setShouldGrantPermission(boolean shouldGrant) {
296         mGrantPermission = shouldGrant;
297     }
298 
299     /** Sets the version of AAPT for APK parsing. */
setAaptVersion(AaptVersion aaptVersion)300     public void setAaptVersion(AaptVersion aaptVersion) {
301         mAaptVersion = aaptVersion;
302     }
303 
304     /** Adds one apk installation arg to be used. */
addInstallArg(String arg)305     public void addInstallArg(String arg) {
306         mInstallArgs.add(arg);
307     }
308 
309     /**
310      * The default value of the force queryable is true. Update it to false if the apk to be
311      * installed should not be queryable.
312      */
setForceQueryable(boolean forceQueryable)313     public void setForceQueryable(boolean forceQueryable) {
314         mForceQueryable = forceQueryable;
315     }
316 
317     /**
318      * Resolve the actual apk path based on testing artifact information inside build info.
319      *
320      * @param testInfo The {@link TestInformation} for the invocation.
321      * @param apkFileName filename of the apk to install
322      * @return a {@link File} representing the physical apk file on host or {@code null} if the file
323      *     does not exist.
324      */
getLocalPathForFilename(TestInformation testInfo, String apkFileName)325     protected File getLocalPathForFilename(TestInformation testInfo, String apkFileName)
326             throws TargetSetupError {
327         try {
328             return BuildTestsZipUtils.getApkFile(
329                     testInfo.getBuildInfo(),
330                     apkFileName,
331                     mAltDirs,
332                     mAltDirBehavior,
333                     false /* use resource as fallback */,
334                     null /* device signing key */);
335         } catch (IOException ioe) {
336             throw new TargetSetupError(
337                     String.format(
338                             "failed to resolve apk path for apk %s in build %s",
339                             apkFileName, testInfo.getBuildInfo().toString()),
340                     ioe,
341                     testInfo.getDevice().getDeviceDescriptor(),
342                     InfraErrorIdentifier.CONFIGURED_ARTIFACT_NOT_FOUND);
343         }
344     }
345 
346     /** @deprecated Temporary backward compatible callback. */
347     @Deprecated
348     @Override
setUp(ITestDevice device, IBuildInfo buildInfo)349     public void setUp(ITestDevice device, IBuildInfo buildInfo)
350             throws TargetSetupError, BuildError, DeviceNotAvailableException {
351         IInvocationContext context = new InvocationContext();
352         context.addAllocatedDevice("device", device);
353         context.addDeviceBuildInfo("device", buildInfo);
354         TestInformation backwardCompatible =
355                 TestInformation.newBuilder().setInvocationContext(context).build();
356         setUp(backwardCompatible);
357     }
358 
359     /** {@inheritDoc} */
360     @Override
setUp(TestInformation testInfo)361     public void setUp(TestInformation testInfo)
362             throws TargetSetupError, BuildError, DeviceNotAvailableException {
363         mTestInfo = testInfo;
364         if (mTestFiles.isEmpty() && mSplitApkFileNames.isEmpty()) {
365             CLog.i("No test apps to install, skipping");
366             return;
367         }
368         // resolve abi flags
369         if (mAbi != null && mForceAbiBitness != null) {
370             throw new IllegalStateException("cannot specify both abi flags: --abi and --force-abi");
371         }
372 
373         // We are going to need several "ro.build" props, save some time (0.4 sec) by prefetching
374         if (getDevice() instanceof NativeDevice) {
375             ((NativeDevice) getDevice()).batchPrefetchStartupBuildProps();
376         }
377         String abiName = null;
378         if (mPinApi != null) {
379             CLog.d("Using abi %s from pin-abi option.", mPinApi);
380             abiName = mPinApi;
381         } else if (mAbi != null) {
382             CLog.d("Using abi %s from abi option.", mAbi.getName());
383             abiName = mAbi.getName();
384         } else if (mForceAbiBitness != null) {
385             CLog.d("Using abi %s from force-abi option.", AbiFormatter.getDefaultAbi(getDevice(), mForceAbiBitness));
386             abiName = AbiFormatter.getDefaultAbi(getDevice(), mForceAbiBitness);
387         }
388         // Set all the extra install args outside the loop to avoid adding them several times.
389         if (abiName != null && testInfo.getDevice().getApiLevel() > 20) {
390             mInstallArgs.add(String.format("--abi %s", abiName));
391         }
392         // Handle instant mode: if we are forced in one installation mode or not.
393         // Some preparer are locked in one installation mode or another, they ignore the
394         // 'instant-mode' option and stays in their mode.
395         if (mInstallationMode != null) {
396             if (InstallMode.INSTANT.equals(mInstallationMode)) {
397                 mInstallArgs.add("--instant");
398             }
399         } else {
400             if (mInstantMode) {
401                 mInstallArgs.add("--instant");
402             }
403         }
404 
405         if (mUserId == null && testInfo.properties().get(RUN_TESTS_AS_USER_KEY) != null) {
406             mUserId = Integer.parseInt(testInfo.properties().get(RUN_TESTS_AS_USER_KEY));
407             if (!testInfo.getDevice().getUserInfos().containsKey(mUserId)) {
408                 CLog.w("User requested: %s doesn't exist on device. Ignoring it.", mUserId);
409                 mUserId = null;
410             } else {
411                 CLog.d("Using user %s from testInfo properties.", mUserId);
412             }
413         }
414 
415         if (testInfo.properties().get(INSTALL_TEST_APK_FOR_ALL_USERS) != null) {
416             mInstallForAllUsers = testInfo.properties().get(INSTALL_TEST_APK_FOR_ALL_USERS)
417                     .equals("true");
418         }
419 
420         if (mForceQueryable == null) {
421             // Do not add --force-queryable if the device api level >= 34. Ideally,
422             // checkApiLevelAgainstNextRelease(34) should only return true for api 34 devices. But,
423             // it also returns true for branches like the tm-xx-plus-aosp. Adding another condition
424             // ro.build.id==TM to handle this special case.
425             mForceQueryable =
426                     !getDevice().checkApiLevelAgainstNextRelease(34)
427                             || "TM".equals(getDevice().getBuildAlias());
428         }
429         if (mForceQueryable && getDevice().isAppEnumerationSupported()) {
430             mInstallArgs.add("--force-queryable");
431         }
432 
433         // Add bypass flag for low target sdk apps when installing on U+ devices
434         if (getDevice().isBypassLowTargetSdkBlockSupported()) {
435             mInstallArgs.add("--bypass-low-target-sdk-block");
436         }
437 
438         for (File testAppName : mTestFiles) {
439             Map<File, String> appFilesAndPackages =
440                     resolveApkFiles(testInfo, findApkFiles(testAppName));
441             installer(testInfo, appFilesAndPackages);
442         }
443 
444         for (String testAppNames : mSplitApkFileNames) {
445             List<String> apkNames = Arrays.asList(testAppNames.split(","));
446             List<File> apkFileNames =
447                     apkNames.stream().map(a -> new File(a)).collect(Collectors.toList());
448             Map<File, String> appFilesAndPackages = resolveApkFiles(testInfo, apkFileNames);
449             installer(testInfo, appFilesAndPackages);
450         }
451     }
452 
453     /**
454      * Returns the device that the preparer should apply to.
455      *
456      * @throws TargetSetupError
457      */
getDevice()458     public ITestDevice getDevice() throws TargetSetupError {
459         return mTestInfo.getDevice();
460     }
461 
getTestInfo()462     public TestInformation getTestInfo() {
463         return mTestInfo;
464     }
465 
466     @Override
setAbi(IAbi abi)467     public void setAbi(IAbi abi) {
468         mAbi = abi;
469     }
470 
471     @Override
getAbi()472     public IAbi getAbi() {
473         return mAbi;
474     }
475 
476     /**
477      * Sets whether or not --instant should be used when installing the apk. Will have no effect if
478      * force-install-mode is set.
479      */
setInstantMode(boolean mode)480     public final void setInstantMode(boolean mode) {
481         mInstantMode = mode;
482     }
483 
484     /** Returns whether or not instant mode installation has been enabled. */
isInstantMode()485     public final boolean isInstantMode() {
486         return mInstantMode;
487     }
488 
489     /** {@inheritDoc} */
490     @Override
tearDown(TestInformation testInfo, Throwable e)491     public void tearDown(TestInformation testInfo, Throwable e) throws DeviceNotAvailableException {
492         mTestInfo = testInfo;
493         if (mCleanup && !(e instanceof DeviceNotAvailableException)) {
494             for (String packageName : mPackagesInstalled) {
495                 try {
496                     if (mApkChangeDetector != null
497                         && mApkChangeDetector.handlePackageCleanup(
498                             packageName, getDevice(), mUserId, mInstallForAllUsers)) {
499                         continue;
500                     }
501                     uninstallPackage(getDevice(), packageName);
502                 } catch (TargetSetupError tse) {
503                     CLog.e(tse);
504                 }
505             }
506         }
507     }
508 
509     /**
510      * Set an alternate directory.
511      */
setAltDir(File altDir)512     public void setAltDir(File altDir) {
513         mAltDirs.add(altDir);
514     }
515 
516     /**
517      * Set an alternate directory behaviors.
518      */
setAltDirBehavior(AltDirBehavior altDirBehavior)519     public void setAltDirBehavior(AltDirBehavior altDirBehavior) {
520         mAltDirBehavior = altDirBehavior;
521     }
522 
523     /** Returns True if Apks will be cleaned up during tear down. */
isCleanUpEnabled()524     public boolean isCleanUpEnabled() {
525         return mCleanup;
526     }
527 
528     /** {@inheritDoc} */
529     @Override
setIncrementalSetupEnabled(boolean shouldEnable)530     public void setIncrementalSetupEnabled(boolean shouldEnable) {
531         if (shouldEnable) {
532             mApkChangeDetector = new ApkChangeDetector();
533         } else {
534             mApkChangeDetector = null;
535         }
536     }
537 
538     /**
539      * Attempt to install an package or split package on the device.
540      *
541      * @param testInfo the {@link TestInformation} for the invocation
542      * @param appFilesAndPackages The apks and their package to be installed.
543      */
installer(TestInformation testInfo, Map<File, String> appFilesAndPackages)544     protected void installer(TestInformation testInfo, Map<File, String> appFilesAndPackages)
545             throws TargetSetupError, DeviceNotAvailableException {
546 
547         ITestDevice device = testInfo.getDevice();
548 
549         // TODO(hzalek): Consider changing resolveApkFiles's return to a Multimap to avoid building
550         // it here.
551         ImmutableListMultimap<String, File> packageToFiles =
552                 ImmutableListMultimap.copyOf(appFilesAndPackages.entrySet()).inverse();
553 
554         Builder builder = null;
555         if (mIncrementalInstallation) {
556             builder = getIncrementalInstallSessionBuilder();
557         }
558 
559         for (Map.Entry<String, List<File>> e : Multimaps.asMap(packageToFiles).entrySet()) {
560             if (mApkChangeDetector != null
561                 && mApkChangeDetector.handleTestAppsPreinstall(
562                     e.getKey(), e.getValue(), getDevice(), mUserId, mInstallForAllUsers)) {
563                 continue;
564             }
565 
566             if (mIncrementalInstallation) {
567                 CLog.d(
568                         "Performing incremental installation of apk %s with %s ...",
569                         e.getKey(), e.getValue());
570                 addPackageToIncrementalInstallSession(builder, e.getKey(), e.getValue());
571                 if (mCleanup) {
572                     mPackagesInstalled.add(e.getKey());
573                 }
574             } else {
575                 installSinglePackage(device, e.getKey(), e.getValue());
576             }
577         }
578 
579         if (mIncrementalInstallation && builder != null) {
580             installPackageIncrementally(builder);
581         }
582     }
583 
installSinglePackage( ITestDevice testDevice, String packageName, List<File> apkFiles)584     private void installSinglePackage(
585             ITestDevice testDevice, String packageName, List<File> apkFiles)
586             throws TargetSetupError, DeviceNotAvailableException {
587 
588         if (apkFiles.isEmpty()) {
589             return;
590         }
591 
592         CLog.d("Installing apk %s with %s ...", packageName, apkFiles);
593         String result = installPackage(testDevice, apkFiles);
594 
595         if (result != null) {
596             if (result.startsWith(INSTALL_FAILED_UPDATE_INCOMPATIBLE)) {
597                 // Try to uninstall package and reinstall.
598                 uninstallPackage(testDevice, packageName);
599                 result = installPackage(testDevice, apkFiles);
600             }
601         }
602 
603         if (result != null) {
604             throw new TargetSetupError(
605                     String.format(
606                             "Failed to install %s with %s on %s. Reason: '%s'",
607                             packageName, apkFiles, testDevice.getSerialNumber(), result),
608                     testDevice.getDeviceDescriptor(),
609                     DeviceErrorIdentifier.APK_INSTALLATION_FAILED);
610         }
611 
612         if (mCleanup) {
613             mPackagesInstalled.add(packageName);
614         }
615     }
616 
617     /** Helper to resolve some apk to their File and Package. */
618     @VisibleForTesting
resolveApkFiles(TestInformation testInfo, List<File> apkFiles)619     protected Map<File, String> resolveApkFiles(TestInformation testInfo, List<File> apkFiles)
620             throws TargetSetupError, DeviceNotAvailableException {
621         Map<File, String> appFiles = new LinkedHashMap<>();
622         ITestDevice device = testInfo.getDevice();
623         for (File apkFile : apkFiles) {
624             File testAppFile = null;
625             if (apkFile.isAbsolute()) {
626                 testAppFile = apkFile;
627             }
628             if (testAppFile == null) {
629                 testAppFile = getLocalPathForFilename(testInfo, apkFile.getName());
630             }
631             if (testAppFile == null) {
632                 if (mThrowIfNoFile) {
633                     throw new TargetSetupError(
634                             String.format("Test app %s was not found.", apkFile.getName()),
635                             device.getDeviceDescriptor(),
636                             InfraErrorIdentifier.CONFIGURED_ARTIFACT_NOT_FOUND);
637                 } else {
638                     CLog.d("Test app %s was not found.", apkFile.getName());
639                     continue;
640                 }
641             }
642             if (!testAppFile.canRead()) {
643                 if (mThrowIfNoFile) {
644                     throw new TargetSetupError(
645                             String.format("Could not read file %s.", testAppFile.toString()),
646                             device.getDeviceDescriptor());
647                 } else {
648                     CLog.d("Could not read file %s.", testAppFile.toString());
649                     continue;
650                 }
651             }
652 
653             if (mCheckMinSdk) {
654                 AaptParser aaptParser = doAaptParse(testAppFile);
655                 if (aaptParser == null) {
656                     throw new TargetSetupError(
657                             String.format(
658                                     "Failed to extract info from `%s` using "
659                                         + (mAaptVersion == AaptVersion.AAPT
660                                         ? "aapt" : "aapt2"),
661                                     testAppFile.getAbsoluteFile().getName()),
662                             device.getDeviceDescriptor());
663                 }
664                 if (device.getApiLevel() < aaptParser.getSdkVersion()) {
665                     CLog.w(
666                             "Skipping installing apk %s on device %s because "
667                                     + "SDK level require is %d, but device SDK level is %d",
668                             apkFile.toString(),
669                             device.getSerialNumber(),
670                             aaptParser.getSdkVersion(),
671                             device.getApiLevel());
672                 } else {
673                     appFiles.put(testAppFile, parsePackageName(testAppFile));
674                 }
675             } else {
676                 appFiles.put(testAppFile, parsePackageName(testAppFile));
677             }
678         }
679         return appFiles;
680     }
681 
682     /**
683      * Returns the provided file if not a directory or all APK files contained in the directory tree
684      * rooted at the provided path otherwise.
685      */
findApkFiles(File fileOrDirectory)686     private List<File> findApkFiles(File fileOrDirectory) throws TargetSetupError {
687 
688         if (!fileOrDirectory.isDirectory()) {
689             return ImmutableList.of(fileOrDirectory);
690         }
691 
692         List<File> apkFiles;
693 
694         try (Stream<Path> paths = Files.walk(fileOrDirectory.toPath())) {
695             apkFiles =
696                     paths.filter(p -> p.toString().endsWith(".apk"))
697                             .filter(Files::isRegularFile)
698                             .map(Path::toFile)
699                             .collect(Collectors.toList());
700         } catch (IOException e) {
701             throw new TargetSetupError(
702                     String.format(
703                             "Could not list files of specified directory: %s", fileOrDirectory),
704                     e,
705                     null,
706                     false,
707                     InfraErrorIdentifier.CONFIGURED_ARTIFACT_NOT_FOUND);
708         }
709 
710         if (mThrowIfNoFile && apkFiles.isEmpty()) {
711             throw new TargetSetupError(
712                     String.format(
713                             "Could not find any files in specified directory: %s", fileOrDirectory),
714                     null,
715                     null,
716                     false,
717                     InfraErrorIdentifier.CONFIGURED_ARTIFACT_NOT_FOUND);
718         }
719 
720         return apkFiles;
721     }
722 
723     /**
724      * Attempt to install a package or split package on the device.
725      *
726      * @param device the {@link ITestDevice} to install package
727      * @param appFiles List of Files. If apkFiles contains only one apk file, the app will be
728      *     installed as a whole package with single file. If apkFiles contains more than one name,
729      *     the app will be installed as split apk with multiple files.
730      */
installPackage(ITestDevice device, List<File> appFiles)731     private String installPackage(ITestDevice device, List<File> appFiles)
732             throws DeviceNotAvailableException {
733         // Handle the different install use cases (with or without a user)
734         if (mUserId == null || mInstallForAllUsers) {
735             if (appFiles.size() == 1) {
736                 return device.installPackage(
737                         appFiles.get(0), true, mInstallArgs.toArray(new String[] {}));
738             } else {
739                 return device.installPackages(
740                         appFiles, true, mInstallArgs.toArray(new String[] {}));
741             }
742         } else if (mGrantPermission != null) {
743             if (appFiles.size() == 1) {
744                 return device.installPackageForUser(
745                         appFiles.get(0),
746                         true,
747                         mGrantPermission,
748                         mUserId,
749                         mInstallArgs.toArray(new String[] {}));
750             } else {
751                 return device.installPackagesForUser(
752                         appFiles,
753                         true,
754                         mGrantPermission,
755                         mUserId,
756                         mInstallArgs.toArray(new String[] {}));
757             }
758         } else {
759             if (appFiles.size() == 1) {
760                 return device.installPackageForUser(
761                         appFiles.get(0), true, mUserId, mInstallArgs.toArray(new String[] {}));
762             } else {
763                 return device.installPackagesForUser(
764                         appFiles, true, mUserId, mInstallArgs.toArray(new String[] {}));
765             }
766         }
767     }
768 
769     /** Attempt to remove the package from the device. */
uninstallPackage(ITestDevice device, String packageName)770     protected void uninstallPackage(ITestDevice device, String packageName)
771             throws DeviceNotAvailableException {
772         String msg;
773         if (mUserId == null || mInstallForAllUsers) {
774             msg = device.uninstallPackage(packageName);
775         } else {
776             msg = device.uninstallPackageForUser(packageName, mUserId);
777         }
778         if (msg != null) {
779             CLog.w(String.format("error uninstalling package '%s': %s", packageName, msg));
780         }
781         if (mIncrementalInstallation) {
782             incrementalInstallSession.close();
783         }
784     }
785 
786     /** Get the package name from the test app. */
parsePackageName(File testAppFile)787     protected String parsePackageName(File testAppFile) throws TargetSetupError {
788         AaptParser parser = AaptParser.parse(testAppFile, mAaptVersion);
789         if (parser == null) {
790             throw new TargetSetupError(
791                     String.format(
792                             "AaptParser failed for file %s. The APK won't be installed",
793                             testAppFile.getName()),
794                     null,
795                     null,
796                     false, // Not device side error, doesn't need descriptor
797                     DeviceErrorIdentifier.AAPT_PARSER_FAILED);
798         }
799         return parser.getPackageName();
800     }
801 
802     /**
803      * Add APKs from package to incremental installation session builder object.
804      *
805      * @param builder The Builder object for the incremental install session.
806      * @param packageName The name of the package to be added.
807      * @param packageFiles List of files to be added to builder object.
808      * @throws TargetSetupError
809      */
addPackageToIncrementalInstallSession( Builder builder, String packageName, List<File> packageFiles)810     private void addPackageToIncrementalInstallSession(
811             Builder builder, String packageName, List<File> packageFiles) throws TargetSetupError {
812         for (File apk : packageFiles) {
813             Path apkPath = apk.toPath();
814             Path apkSignaturePath = Paths.get(String.format("%s.idsig", apkPath.toString()));
815             if (!apkSignaturePath.toFile().exists()) {
816                 throw new TargetSetupError(
817                         String.format(
818                                 "Unable to retrieve v4 signature for file: %s",
819                                 apkPath.getFileName()),
820                         getDevice().getDeviceDescriptor(),
821                         InfraErrorIdentifier.CONFIGURED_ARTIFACT_NOT_FOUND);
822             }
823             builder.addApk(apkPath, apkSignaturePath);
824         }
825     }
826 
827     /**
828      * Start the incremental installation session for a test app.
829      *
830      * @param builder The Builder object for the incremental install session.
831      * @throws TargetSetupError
832      */
833     @VisibleForTesting
installPackageIncrementally(Builder builder)834     protected void installPackageIncrementally(Builder builder) throws TargetSetupError {
835         try {
836             incrementalInstallSession = builder.build();
837             String deviceSerialNumber = getDevice().getSerialNumber();
838             DeviceConnection.Factory deviceConnection =
839                     DeviceConnection.getFactory(deviceSerialNumber);
840             incrementalInstallSession.start(Executors.newCachedThreadPool(), deviceConnection);
841             incrementalInstallSession.waitForInstallCompleted(
842                     mIncrementalInstallTimeout, TimeUnit.SECONDS);
843         } catch (InterruptedException | IOException e) {
844             throw new TargetSetupError(
845                     String.format("Failed to start incremental install session."),
846                     e,
847                     getDevice().getDeviceDescriptor(),
848                     DeviceErrorIdentifier.APK_INSTALLATION_FAILED);
849         }
850     }
851 
852     /** Initialize the session builder for installing a test app incrementally. */
853     @VisibleForTesting
getIncrementalInstallSessionBuilder()854     protected Builder getIncrementalInstallSessionBuilder() {
855         if (mGrantPermission != null && mGrantPermission) {
856             mInstallArgs.add("-g");
857         }
858 
859         if (mUserId != null) {
860             mInstallArgs.add("--user");
861             mInstallArgs.add(Integer.toString(mUserId));
862         }
863 
864         Builder incrementalInstallSessionBuilder =
865                 new Builder()
866                         .setLogger(new DeviceLogger(new StdLogger(StdLogger.Level.ERROR)))
867                         .addExtraArgs(mInstallArgs.toArray(new String[] {}));
868 
869         // Add block filter to installation if a block filter percentage is specified.
870         if (mBlockFilterPercentage > 0) {
871             long randomSeed = new SecureRandom().nextLong();
872             Random randomBlock = new Random(randomSeed);
873             Map<Path, Set<Integer>> apkBlockMappings = new HashMap<>();
874 
875             CLog.i("Block filter seed: %d.", randomSeed);
876 
877             incrementalInstallSessionBuilder.setBlockFilter(
878                     (PendingBlock b) -> {
879                         Path apkPath = b.getPath();
880                         synchronized (apkBlockMappings) {
881                             // Generate block indexs to filter for APK installation.
882                             if (!apkBlockMappings.containsKey(apkPath)) {
883                                 int blockCount = b.getFileBlockCount();
884                                 int numBlocks = (int) (blockCount * mBlockFilterPercentage);
885                                 Set<Integer> blocksToFilter = new HashSet<Integer>(numBlocks);
886                                 while (blocksToFilter.size() < numBlocks) {
887                                     int blockIndex = randomBlock.nextInt(blockCount);
888                                     blocksToFilter.add(blockIndex);
889                                 }
890                                 apkBlockMappings.put(apkPath, blocksToFilter);
891                             }
892 
893                             return !apkBlockMappings.get(apkPath).contains(b.getBlockIndex());
894                         }
895                     });
896         }
897 
898         return incrementalInstallSessionBuilder;
899     }
900 
901     @Override
reportDependencies()902     public Set<String> reportDependencies() {
903         Set<String> deps = new HashSet<String>();
904         for (File f : getTestsFileName()) {
905             if (!f.exists()) deps.add(f.getName());
906         }
907         for (String testAppNames : mSplitApkFileNames) {
908             List<String> apkNames = Arrays.asList(testAppNames.split(","));
909             deps.addAll(apkNames);
910         }
911         return deps;
912     }
913 }
914