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