1 /*
2  * Copyright (C) 2021 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 
17 package com.android.csuite.core;
18 
19 import com.android.csuite.core.DeviceUtils.DeviceTimestamp;
20 import com.android.csuite.core.DeviceUtils.DropboxEntry;
21 import com.android.csuite.core.TestUtils.RoboscriptSignal;
22 import com.android.csuite.core.TestUtils.TestUtilsException;
23 import com.android.tradefed.device.DeviceNotAvailableException;
24 import com.android.tradefed.invoker.TestInformation;
25 import com.android.tradefed.log.LogUtil.CLog;
26 import com.android.tradefed.result.LogDataType;
27 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestLogData;
28 import com.android.tradefed.util.CommandResult;
29 import com.android.tradefed.util.CommandStatus;
30 import com.android.tradefed.util.IRunUtil;
31 import com.android.tradefed.util.RunUtil;
32 import com.android.tradefed.util.ZipUtil;
33 
34 import com.google.common.annotations.VisibleForTesting;
35 import com.google.common.base.Preconditions;
36 import com.google.common.io.MoreFiles;
37 
38 import org.junit.Assert;
39 
40 import java.io.File;
41 import java.io.IOException;
42 import java.nio.charset.Charset;
43 import java.nio.file.FileSystem;
44 import java.nio.file.FileSystems;
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.List;
50 import java.util.Optional;
51 import java.util.concurrent.atomic.AtomicReference;
52 import java.util.regex.Matcher;
53 import java.util.regex.Pattern;
54 import java.util.stream.Collectors;
55 import java.util.stream.Stream;
56 
57 import javax.annotation.Nullable;
58 
59 /** A tester that interact with an app crawler during testing. */
60 public final class AppCrawlTester {
61     @VisibleForTesting Path mOutput;
62     private final RunUtilProvider mRunUtilProvider;
63     private final TestUtils mTestUtils;
64     private final String mPackageName;
65     private boolean mRecordScreen = false;
66     private boolean mCollectGmsVersion = false;
67     private boolean mCollectAppVersion = false;
68     private boolean mUiAutomatorMode = false;
69     private int mTimeoutSec;
70     private String mCrawlControllerEndpoint;
71     private Path mApkRoot;
72     private Path mRoboscriptFile;
73     private Path mCrawlGuidanceProtoFile;
74     private Path mLoginConfigDir;
75     private FileSystem mFileSystem;
76     private DeviceTimestamp mScreenRecordStartTime;
77 
78     /**
79      * Creates an {@link AppCrawlTester} instance.
80      *
81      * @param packageName The package name of the apk files.
82      * @param testInformation The TradeFed test information.
83      * @param testLogData The TradeFed test output receiver.
84      * @return an {@link AppCrawlTester} instance.
85      */
newInstance( String packageName, TestInformation testInformation, TestLogData testLogData)86     public static AppCrawlTester newInstance(
87             String packageName, TestInformation testInformation, TestLogData testLogData) {
88         return new AppCrawlTester(
89                 packageName,
90                 TestUtils.getInstance(testInformation, testLogData),
91                 () -> new RunUtil(),
92                 FileSystems.getDefault());
93     }
94 
95     @VisibleForTesting
AppCrawlTester( String packageName, TestUtils testUtils, RunUtilProvider runUtilProvider, FileSystem fileSystem)96     AppCrawlTester(
97             String packageName,
98             TestUtils testUtils,
99             RunUtilProvider runUtilProvider,
100             FileSystem fileSystem) {
101         mRunUtilProvider = runUtilProvider;
102         mPackageName = packageName;
103         mTestUtils = testUtils;
104         mFileSystem = fileSystem;
105     }
106 
107     /** An exception class representing crawler test failures. */
108     public static final class CrawlerException extends Exception {
109         /**
110          * Constructs a new {@link CrawlerException} with a meaningful error message.
111          *
112          * @param message A error message describing the cause of the error.
113          */
CrawlerException(String message)114         private CrawlerException(String message) {
115             super(message);
116         }
117 
118         /**
119          * Constructs a new {@link CrawlerException} with a meaningful error message, and a cause.
120          *
121          * @param message A detailed error message.
122          * @param cause A {@link Throwable} capturing the original cause of the CrawlerException.
123          */
CrawlerException(String message, Throwable cause)124         private CrawlerException(String message, Throwable cause) {
125             super(message, cause);
126         }
127 
128         /**
129          * Constructs a new {@link CrawlerException} with a cause.
130          *
131          * @param cause A {@link Throwable} capturing the original cause of the CrawlerException.
132          */
CrawlerException(Throwable cause)133         private CrawlerException(Throwable cause) {
134             super(cause);
135         }
136     }
137 
138     /**
139      * Starts crawling the app and throw AssertionError if app crash is detected.
140      *
141      * @throws DeviceNotAvailableException When device because unavailable.
142      */
startAndAssertAppNoCrash()143     public void startAndAssertAppNoCrash() throws DeviceNotAvailableException {
144         DeviceTimestamp startTime = mTestUtils.getDeviceUtils().currentTimeMillis();
145 
146         CrawlerException crawlerException = null;
147         try {
148             start();
149         } catch (CrawlerException e) {
150             crawlerException = e;
151         }
152         DeviceTimestamp endTime = mTestUtils.getDeviceUtils().currentTimeMillis();
153 
154         ArrayList<String> failureMessages = new ArrayList<>();
155 
156         try {
157 
158             List<DropboxEntry> crashEntries =
159                     mTestUtils
160                             .getDeviceUtils()
161                             .getDropboxEntries(
162                                     DeviceUtils.DROPBOX_APP_CRASH_TAGS,
163                                     mPackageName,
164                                     startTime,
165                                     endTime);
166             String dropboxCrashLog =
167                     mTestUtils.compileTestFailureMessage(
168                             mPackageName, crashEntries, true, mScreenRecordStartTime);
169 
170             if (dropboxCrashLog != null) {
171                 // Put dropbox crash log on the top of the failure messages.
172                 failureMessages.add(dropboxCrashLog);
173             }
174         } catch (IOException e) {
175             failureMessages.add("Error while getting dropbox crash log: " + e.getMessage());
176         }
177 
178         if (crawlerException != null) {
179             failureMessages.add(crawlerException.getMessage());
180         }
181 
182         if (!failureMessages.isEmpty()) {
183             Assert.fail(
184                     String.join(
185                             "\n============\n",
186                             failureMessages.toArray(new String[failureMessages.size()])));
187         }
188     }
189 
190     /**
191      * Starts a crawler run on the configured app.
192      *
193      * @throws CrawlerException When the crawler was not set up correctly or the crawler run command
194      *     failed.
195      * @throws DeviceNotAvailableException When device because unavailable.
196      */
start()197     public void start() throws CrawlerException, DeviceNotAvailableException {
198         if (!AppCrawlTesterHostPreparer.isReady(mTestUtils.getTestInformation())) {
199             throw new CrawlerException(
200                     "The "
201                             + AppCrawlTesterHostPreparer.class.getName()
202                             + " is not ready. Please check whether "
203                             + AppCrawlTesterHostPreparer.class.getName()
204                             + " was included in the test plan and completed successfully.");
205         }
206 
207         if (mOutput != null) {
208             throw new CrawlerException(
209                     "The crawler has already run. Multiple runs in the same "
210                             + AppCrawlTester.class.getName()
211                             + " instance are not supported.");
212         }
213 
214         try {
215             mOutput = Files.createTempDirectory("crawler");
216         } catch (IOException e) {
217             throw new CrawlerException("Failed to create temp directory for output.", e);
218         }
219 
220         IRunUtil runUtil = mRunUtilProvider.get();
221         AtomicReference<String[]> command = new AtomicReference<>();
222         AtomicReference<CommandResult> commandResult = new AtomicReference<>();
223 
224         CLog.d("Start to crawl package: %s.", mPackageName);
225 
226         Path bin =
227                 mFileSystem.getPath(
228                         AppCrawlTesterHostPreparer.getCrawlerBinPath(
229                                 mTestUtils.getTestInformation()));
230         boolean isUtpClient = false;
231         if (Files.exists(bin.resolve("utp-cli-android_deploy.jar"))) {
232             command.set(createUtpCrawlerRunCommand(mTestUtils.getTestInformation()));
233             runUtil.setEnvVariable(
234                     "ANDROID_SDK",
235                     AppCrawlTesterHostPreparer.getSdkPath(mTestUtils.getTestInformation())
236                             .toString());
237             isUtpClient = true;
238         } else if (Files.exists(bin.resolve("crawl_launcher_deploy.jar"))) {
239             command.set(createCrawlerRunCommand(mTestUtils.getTestInformation()));
240             runUtil.setEnvVariable(
241                     "GOOGLE_APPLICATION_CREDENTIALS",
242                     AppCrawlTesterHostPreparer.getCredentialPath(mTestUtils.getTestInformation())
243                             .toString());
244         } else {
245             throw new CrawlerException(
246                     "Crawler executable binaries not found in " + bin.toString());
247         }
248 
249         if (mCollectGmsVersion) {
250             mTestUtils.collectGmsVersion(mPackageName);
251         }
252 
253         // Minimum timeout 3 minutes plus crawl test timeout.
254         long commandTimeout = 3 * 60 * 1000 + mTimeoutSec * 1000;
255 
256         // TODO(yuexima): When the obb_file option is supported in espresso mode, the timeout need
257         // to be extended.
258         if (mRecordScreen) {
259             mTestUtils.collectScreenRecord(
260                     () -> {
261                         commandResult.set(runUtil.runTimedCmd(commandTimeout, command.get()));
262                     },
263                     mPackageName,
264                     deviceTime -> mScreenRecordStartTime = deviceTime);
265         } else {
266             commandResult.set(runUtil.runTimedCmd(commandTimeout, command.get()));
267         }
268 
269         // Must be done after the crawler run because the app is installed by the crawler.
270         if (mCollectAppVersion) {
271             mTestUtils.collectAppVersion(mPackageName);
272         }
273 
274         collectOutputZip();
275         collectCrawlStepScreenshots(isUtpClient);
276         createCrawlerRoboscriptSignal(isUtpClient);
277 
278         if (!commandResult.get().getStatus().equals(CommandStatus.SUCCESS)
279                 || commandResult.get().getStdout().contains("Unknown options:")) {
280             throw new CrawlerException("Crawler command failed: " + commandResult.get());
281         }
282 
283         CLog.i("Completed crawling the package %s. Outputs: %s", mPackageName, commandResult.get());
284     }
285 
286     /** Copys the step screenshots into test outputs for easier access. */
collectCrawlStepScreenshots(boolean isUtpClient)287     private void collectCrawlStepScreenshots(boolean isUtpClient) {
288         if (mOutput == null) {
289             CLog.e("Output directory is not created yet. Skipping collecting step screenshots.");
290             return;
291         }
292 
293         Path subDir = getClientCrawlerOutputSubDir(isUtpClient);
294         if (!Files.exists(subDir)) {
295             CLog.e(
296                     "The crawler output directory is not complete, skipping collecting step"
297                             + " screenshots.");
298             return;
299         }
300 
301         try (Stream<Path> files = Files.list(subDir)) {
302             files.filter(path -> path.getFileName().toString().toLowerCase().endsWith(".png"))
303                     .forEach(
304                             path -> {
305                                 mTestUtils
306                                         .getTestArtifactReceiver()
307                                         .addTestArtifact(
308                                                 mPackageName
309                                                         + "-crawl_step_screenshot_"
310                                                         + path.getFileName(),
311                                                 LogDataType.PNG,
312                                                 path.toFile());
313                             });
314         } catch (IOException e) {
315             CLog.e(e);
316         }
317     }
318 
319     /**
320      * Reads the crawler output and creates an artifact with the success signal for a Roboscript
321      * that has been executed by the crawler.
322      */
createCrawlerRoboscriptSignal(boolean isUtpClient)323     private void createCrawlerRoboscriptSignal(boolean isUtpClient) {
324         if (mOutput == null) {
325             CLog.e("Output directory is not created yet. Skipping collecting crawler signal.");
326             return;
327         }
328 
329         Path subDir = getClientCrawlerOutputSubDir(isUtpClient);
330         if (!Files.exists(subDir)) {
331             CLog.e(
332                     "The crawler output directory is not complete, skipping collecting crawler"
333                             + " signal.");
334             return;
335         }
336 
337         try (Stream<Path> files = Files.list(subDir)) {
338             Optional<Path> roboOutputFile =
339                     files.filter(
340                                     path ->
341                                             path.getFileName()
342                                                     .toString()
343                                                     .toLowerCase()
344                                                     .endsWith("crawl_outputs.txt"))
345                             .findFirst();
346             if (roboOutputFile.isPresent()) {
347                 generateRoboscriptSignalFile(roboOutputFile.get(), mPackageName);
348             }
349         } catch (IOException e) {
350             CLog.e(e);
351         }
352     }
353 
354     /**
355      * Generates an artifact text file with a name indicating whether the Roboscript was successful.
356      *
357      * @param roboOutputFile - the file containing the Robo crawler output.
358      * @param packageName - the android package name of the app for which the signal file is being
359      *     generated.
360      */
generateRoboscriptSignalFile(Path roboOutputFile, String packageName)361     private void generateRoboscriptSignalFile(Path roboOutputFile, String packageName) {
362         try {
363             File signalFile =
364                     Files.createTempFile(
365                                     packageName
366                                             + "_roboscript_"
367                                             + getRoboscriptSignal(Optional.of(roboOutputFile))
368                                                     .toString()
369                                                     .toLowerCase(),
370                                     ".txt")
371                             .toFile();
372             mTestUtils
373                     .getTestArtifactReceiver()
374                     .addTestArtifact(signalFile.getName(), LogDataType.HOST_LOG, signalFile);
375         } catch (IOException e) {
376             CLog.e(e);
377         }
378     }
379 
380     /**
381      * Computes whether the Robosript was successful based on the output file, and returns the
382      * success signal.
383      *
384      * @param roboOutput
385      * @return Roboscript success signal
386      */
getRoboscriptSignal(Optional<Path> roboOutput)387     public RoboscriptSignal getRoboscriptSignal(Optional<Path> roboOutput) {
388         if (!roboOutput.isPresent()) {
389             return RoboscriptSignal.UNKNOWN;
390         }
391         Pattern totalActionsPattern =
392                 Pattern.compile("(?:robo_script_execution(?:.|\\n)*)total_actions.\\s(\\d*)");
393         Pattern successfulActionsPattern =
394                 Pattern.compile("(?:robo_script_execution(?:.|\\n)*)successful_actions.\\s(\\d*)");
395         final String outputFile;
396         try {
397             outputFile =
398                     String.join("", Files.readAllLines(roboOutput.get(), Charset.defaultCharset()));
399         } catch (IOException e) {
400             CLog.e(e);
401             return RoboscriptSignal.UNKNOWN;
402         }
403         int totalActions = 0;
404         int successfulActions = 0;
405         Matcher mTotal = totalActionsPattern.matcher(outputFile);
406         Matcher mSuccessful = successfulActionsPattern.matcher(outputFile);
407         if (mTotal.find() && mSuccessful.find()) {
408             totalActions = Integer.parseInt(mTotal.group(1));
409             successfulActions = Integer.parseInt(mSuccessful.group(1));
410             if (totalActions == 0) {
411                 return RoboscriptSignal.FAIL;
412             }
413             return successfulActions / totalActions < 1
414                     ? RoboscriptSignal.FAIL
415                     : RoboscriptSignal.SUCCESS;
416         }
417         return RoboscriptSignal.UNKNOWN;
418     }
419 
420     /** Based on the type of Robo client, resolves the Path for its output directory. */
getClientCrawlerOutputSubDir(boolean isUtpClient)421     private Path getClientCrawlerOutputSubDir(boolean isUtpClient) {
422         return isUtpClient
423                 ? mOutput.resolve("output").resolve("artifacts")
424                 : mOutput.resolve("app_firebase_test_lab");
425     }
426 
427     /** Puts the zipped crawler output files into test output. */
collectOutputZip()428     private void collectOutputZip() {
429         if (mOutput == null) {
430             CLog.e("Output directory is not created yet. Skipping collecting output.");
431             return;
432         }
433 
434         // Compress the crawler output directory and add it to test outputs.
435         try {
436             File outputZip = ZipUtil.createZip(mOutput.toFile());
437             mTestUtils
438                     .getTestArtifactReceiver()
439                     .addTestArtifact(mPackageName + "-crawler_output", LogDataType.ZIP, outputZip);
440         } catch (IOException e) {
441             CLog.e("Failed to zip the output directory: " + e);
442         }
443     }
444 
445     @VisibleForTesting
createUtpCrawlerRunCommand(TestInformation testInfo)446     String[] createUtpCrawlerRunCommand(TestInformation testInfo) throws CrawlerException {
447 
448         Path bin =
449                 mFileSystem.getPath(
450                         AppCrawlTesterHostPreparer.getCrawlerBinPath(
451                                 mTestUtils.getTestInformation()));
452         ArrayList<String> cmd = new ArrayList<>();
453         cmd.addAll(
454                 Arrays.asList(
455                         "java",
456                         "-jar",
457                         bin.resolve("utp-cli-android_deploy.jar").toString(),
458                         "android",
459                         "robo",
460                         "--device-id",
461                         testInfo.getDevice().getSerialNumber(),
462                         "--app-id",
463                         mPackageName,
464                         "--controller-endpoint",
465                         "PROD",
466                         "--utp-binaries-dir",
467                         bin.toString(),
468                         "--key-file",
469                         AppCrawlTesterHostPreparer.getCredentialPath(
470                                         mTestUtils.getTestInformation())
471                                 .toString(),
472                         "--base-crawler-apk",
473                         bin.resolve("crawler_app.apk").toString(),
474                         "--stub-crawler-apk",
475                         bin.resolve("crawler_stubapp_androidx.apk").toString(),
476                         "--tmp-dir",
477                         mOutput.toString()));
478 
479         if (mTimeoutSec > 0) {
480             cmd.add("--crawler-flag");
481             cmd.add("crawlDurationSec=" + Integer.toString(mTimeoutSec));
482         }
483 
484         if (mUiAutomatorMode) {
485             cmd.addAll(Arrays.asList("--ui-automator-mode", "--app-installed-on-device"));
486         } else {
487             Preconditions.checkNotNull(
488                     mApkRoot, "Apk file path is required when not running in UIAutomator mode");
489 
490             try {
491                 TestUtils.listApks(mApkRoot)
492                         .forEach(
493                                 path -> {
494                                     String nameLowercase =
495                                             path.getFileName().toString().toLowerCase();
496                                     if (nameLowercase.endsWith(".apk")) {
497                                         cmd.add("--apks-to-crawl");
498                                         cmd.add(path.toString());
499                                     } else if (nameLowercase.endsWith(".obb")) {
500                                         cmd.add("--files-to-push");
501                                         cmd.add(
502                                                 String.format(
503                                                         "%s=/sdcard/Android/obb/%s/%s",
504                                                         path.toString(),
505                                                         mPackageName,
506                                                         path.getFileName().toString()));
507                                     } else {
508                                         CLog.d("Skipping unrecognized file %s", path.toString());
509                                     }
510                                 });
511             } catch (TestUtilsException e) {
512                 throw new CrawlerException(e);
513             }
514         }
515 
516         if (mRoboscriptFile != null) {
517             Assert.assertTrue(
518                     "Please provide a valid roboscript file.",
519                     Files.isRegularFile(mRoboscriptFile));
520             cmd.add("--crawler-asset");
521             cmd.add("robo.script=" + mRoboscriptFile.toString());
522         }
523 
524         if (mCrawlGuidanceProtoFile != null) {
525             Assert.assertTrue(
526                     "Please provide a valid CrawlGuidance file.",
527                     Files.isRegularFile(mCrawlGuidanceProtoFile));
528             cmd.add("--crawl-guidance-proto-path");
529             cmd.add(mCrawlGuidanceProtoFile.toString());
530         }
531 
532         if (mLoginConfigDir != null) {
533             RoboLoginConfigProvider configProvider = new RoboLoginConfigProvider(mLoginConfigDir);
534             cmd.addAll(configProvider.findConfigFor(mPackageName, true).getLoginArgs());
535         }
536 
537         return cmd.toArray(new String[cmd.size()]);
538     }
539 
540     @VisibleForTesting
createCrawlerRunCommand(TestInformation testInfo)541     String[] createCrawlerRunCommand(TestInformation testInfo) throws CrawlerException {
542 
543         Path bin =
544                 mFileSystem.getPath(
545                         AppCrawlTesterHostPreparer.getCrawlerBinPath(
546                                 mTestUtils.getTestInformation()));
547         ArrayList<String> cmd = new ArrayList<>();
548         cmd.addAll(
549                 Arrays.asList(
550                         "java",
551                         "-jar",
552                         bin.resolve("crawl_launcher_deploy.jar").toString(),
553                         "--android-sdk-path",
554                         AppCrawlTesterHostPreparer.getSdkPath(testInfo).toString(),
555                         "--device-serial-code",
556                         testInfo.getDevice().getSerialNumber(),
557                         "--output-dir",
558                         mOutput.toString(),
559                         "--key-store-file",
560                         // Using the publicly known default file name of the debug keystore.
561                         bin.resolve("debug.keystore").toString(),
562                         "--key-store-password",
563                         // Using the publicly known default password of the debug keystore.
564                         "android"));
565 
566         if (mCrawlControllerEndpoint != null && mCrawlControllerEndpoint.length() > 0) {
567             cmd.addAll(Arrays.asList("--endpoint", mCrawlControllerEndpoint));
568         }
569 
570         if (mUiAutomatorMode) {
571             cmd.addAll(Arrays.asList("--ui-automator-mode", "--app-package-name", mPackageName));
572         } else {
573             Preconditions.checkNotNull(
574                     mApkRoot, "Apk file path is required when not running in UIAutomator mode");
575 
576             List<Path> apks;
577             try {
578                 apks =
579                         TestUtils.listApks(mApkRoot).stream()
580                                 .filter(
581                                         path ->
582                                                 path.getFileName()
583                                                         .toString()
584                                                         .toLowerCase()
585                                                         .endsWith(".apk"))
586                                 .collect(Collectors.toList());
587             } catch (TestUtilsException e) {
588                 throw new CrawlerException(e);
589             }
590 
591             cmd.add("--apk-file");
592             cmd.add(apks.get(0).toString());
593 
594             for (int i = 1; i < apks.size(); i++) {
595                 cmd.add("--split-apk-files");
596                 cmd.add(apks.get(i).toString());
597             }
598         }
599 
600         if (mTimeoutSec > 0) {
601             cmd.add("--timeout-sec");
602             cmd.add(Integer.toString(mTimeoutSec));
603         }
604 
605         if (mRoboscriptFile != null) {
606             Assert.assertTrue(
607                     "Please provide a valid roboscript file.",
608                     Files.isRegularFile(mRoboscriptFile));
609             cmd.addAll(Arrays.asList("--robo-script-file", mRoboscriptFile.toString()));
610         }
611 
612         if (mCrawlGuidanceProtoFile != null) {
613             Assert.assertTrue(
614                     "Please provide a valid CrawlGuidance file.",
615                     Files.isRegularFile(mCrawlGuidanceProtoFile));
616             cmd.addAll(Arrays.asList("--text-guide-file", mCrawlGuidanceProtoFile.toString()));
617         }
618 
619         if (mLoginConfigDir != null) {
620             RoboLoginConfigProvider configProvider = new RoboLoginConfigProvider(mLoginConfigDir);
621             cmd.addAll(configProvider.findConfigFor(mPackageName, false).getLoginArgs());
622         }
623 
624         return cmd.toArray(new String[cmd.size()]);
625     }
626 
627     /** Cleans up the crawler output directory. */
cleanUp()628     public void cleanUp() {
629         if (mOutput == null) {
630             return;
631         }
632 
633         try {
634             MoreFiles.deleteRecursively(mOutput);
635         } catch (IOException e) {
636             CLog.e("Failed to clean up the crawler output directory: " + e);
637         }
638     }
639 
640     /** Sets the option of whether to record the device screen during crawling. */
setRecordScreen(boolean recordScreen)641     public void setRecordScreen(boolean recordScreen) {
642         mRecordScreen = recordScreen;
643     }
644 
645     /** Sets the option of whether to collect GMS version in test artifacts. */
setCollectGmsVersion(boolean collectGmsVersion)646     public void setCollectGmsVersion(boolean collectGmsVersion) {
647         mCollectGmsVersion = collectGmsVersion;
648     }
649 
650     /** Sets the option of whether to collect the app version in test artifacts. */
setCollectAppVersion(boolean collectAppVersion)651     public void setCollectAppVersion(boolean collectAppVersion) {
652         mCollectAppVersion = collectAppVersion;
653     }
654 
655     /** Sets the option of whether to run the crawler with UIAutomator mode. */
setUiAutomatorMode(boolean uiAutomatorMode)656     public void setUiAutomatorMode(boolean uiAutomatorMode) {
657         mUiAutomatorMode = uiAutomatorMode;
658     }
659 
660     /** Sets the value of the "timeout-sec" param for the crawler launcher. */
setTimeoutSec(int timeoutSec)661     public void setTimeoutSec(int timeoutSec) {
662         mTimeoutSec = timeoutSec;
663     }
664 
665     /** Sets the robo crawler controller endpoint (optional). */
setCrawlControllerEndpoint(String crawlControllerEndpoint)666     public void setCrawlControllerEndpoint(String crawlControllerEndpoint) {
667         mCrawlControllerEndpoint = crawlControllerEndpoint;
668     }
669 
670     /**
671      * Sets the apk file path. Required when not running in UIAutomator mode.
672      *
673      * @param apkRoot The root path for an apk or a directory that contains apk files for a package.
674      */
setApkPath(Path apkRoot)675     public void setApkPath(Path apkRoot) {
676         mApkRoot = apkRoot;
677     }
678 
679     /**
680      * Sets the option of the Roboscript file to be used by the crawler. Null can be passed to
681      * remove the reference to the file.
682      */
setRoboscriptFile(@ullable Path roboscriptFile)683     public void setRoboscriptFile(@Nullable Path roboscriptFile) {
684         mRoboscriptFile = roboscriptFile;
685     }
686 
687     /**
688      * Sets the option of the CrawlGuidance file to be used by the crawler. Null can be passed to
689      * remove the reference to the file.
690      */
setCrawlGuidanceProtoFile(@ullable Path crawlGuidanceProtoFile)691     public void setCrawlGuidanceProtoFile(@Nullable Path crawlGuidanceProtoFile) {
692         mCrawlGuidanceProtoFile = crawlGuidanceProtoFile;
693     }
694 
695     /** Sets the option of the directory that contains configuration for login. */
setLoginConfigDir(@ullable Path loginFilesDir)696     public void setLoginConfigDir(@Nullable Path loginFilesDir) {
697         mLoginConfigDir = loginFilesDir;
698     }
699 
700     @VisibleForTesting
701     interface RunUtilProvider {
get()702         IRunUtil get();
703     }
704 }
705