1 /*
2  * Copyright (C) 2020 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.compatibility.targetprep;
18 
19 import static com.google.common.base.Preconditions.checkArgument;
20 
21 import com.android.csuite.core.SystemPackageUninstaller;
22 import com.android.tradefed.build.IBuildInfo;
23 import com.android.tradefed.config.ConfigurationException;
24 import com.android.tradefed.config.Option;
25 import com.android.tradefed.config.OptionSetter;
26 import com.android.tradefed.device.DeviceNotAvailableException;
27 import com.android.tradefed.device.ITestDevice;
28 import com.android.tradefed.invoker.TestInformation;
29 import com.android.tradefed.log.LogUtil.CLog;
30 import com.android.tradefed.targetprep.BuildError;
31 import com.android.tradefed.targetprep.ITargetPreparer;
32 import com.android.tradefed.targetprep.TargetSetupError;
33 import com.android.tradefed.targetprep.TestAppInstallSetup;
34 import com.android.tradefed.util.AaptParser.AaptVersion;
35 
36 import com.google.common.annotations.VisibleForTesting;
37 import com.google.common.util.concurrent.SimpleTimeLimiter;
38 import com.google.common.util.concurrent.TimeLimiter;
39 import com.google.common.util.concurrent.UncheckedTimeoutException;
40 
41 import java.io.File;
42 import java.time.Duration;
43 import java.util.ArrayList;
44 import java.util.List;
45 import java.util.concurrent.Executors;
46 import java.util.concurrent.TimeUnit;
47 
48 /** A Tradefed preparer that downloads and installs an app on the target device. */
49 public final class AppSetupPreparer implements ITargetPreparer {
50 
51     @VisibleForTesting
52     static final String OPTION_WAIT_FOR_DEVICE_AVAILABLE_SECONDS =
53             "wait-for-device-available-seconds";
54 
55     @VisibleForTesting
56     static final String OPTION_EXPONENTIAL_BACKOFF_MULTIPLIER_SECONDS =
57             "exponential-backoff-multiplier-seconds";
58 
59     @VisibleForTesting static final String OPTION_TEST_FILE_NAME = "test-file-name";
60     @VisibleForTesting static final String OPTION_INSTALL_ARG = "install-arg";
61     @VisibleForTesting static final String OPTION_SETUP_TIMEOUT_MILLIS = "setup-timeout-millis";
62     @VisibleForTesting static final String OPTION_MAX_RETRY = "max-retry";
63     @VisibleForTesting static final String OPTION_AAPT_VERSION = "aapt-version";
64     @VisibleForTesting static final String OPTION_INCREMENTAL_INSTALL = "incremental";
65 
66     @Option(name = "package-name", description = "Package name of testing app.")
67     private String mPackageName;
68 
69     @Option(
70             name = OPTION_TEST_FILE_NAME,
71             description = "the name of an apk file to be installed on device. Can be repeated.")
72     private final List<File> mTestFiles = new ArrayList<>();
73 
74     @Option(name = OPTION_AAPT_VERSION, description = "The version of AAPT for APK parsing.")
75     private AaptVersion mAaptVersion = AaptVersion.AAPT2;
76 
77     @Option(
78             name = OPTION_INSTALL_ARG,
79             description =
80                     "Additional arguments to be passed to install command, "
81                             + "including leading dash, e.g. \"-d\"")
82     private final List<String> mInstallArgs = new ArrayList<>();
83 
84     @Option(
85             name = OPTION_INCREMENTAL_INSTALL,
86             description = "Enable packages to be installed incrementally.")
87     private boolean mIncrementalInstallation = false;
88 
89     @Option(name = OPTION_MAX_RETRY, description = "Max number of retries upon TargetSetupError.")
90     private int mMaxRetry = 0;
91 
92     @Option(
93             name = OPTION_EXPONENTIAL_BACKOFF_MULTIPLIER_SECONDS,
94             description =
95                     "The exponential backoff multiplier for retries in seconds. "
96                             + "A value n means the preparer will wait for n^(retry_count) "
97                             + "seconds between retries.")
98     private int mExponentialBackoffMultiplierSeconds = 0;
99 
100     @Option(
101             name = OPTION_WAIT_FOR_DEVICE_AVAILABLE_SECONDS,
102             description =
103                     "Timeout value for waiting for device available in seconds. "
104                             + "A negative value means not to check device availability.")
105     private int mWaitForDeviceAvailableSeconds = -1;
106 
107     @Option(
108             name = OPTION_SETUP_TIMEOUT_MILLIS,
109             description =
110                     "Timeout value for a setUp operation. "
111                             + "Note that the timeout is not a global timeout and will "
112                             + "be applied to each retry attempt.")
113     private long mSetupOnceTimeoutMillis = TimeUnit.MINUTES.toMillis(10);
114 
115     private final TestAppInstallSetup mTestAppInstallSetup;
116     private final Sleeper mSleeper;
117     private final TimeLimiter mTimeLimiter =
118             SimpleTimeLimiter.create(Executors.newCachedThreadPool());
119 
AppSetupPreparer()120     public AppSetupPreparer() {
121         this(new TestAppInstallSetup(), Sleepers.DefaultSleeper.INSTANCE);
122     }
123 
124     @VisibleForTesting
AppSetupPreparer(TestAppInstallSetup testAppInstallSetup, Sleeper sleeper)125     public AppSetupPreparer(TestAppInstallSetup testAppInstallSetup, Sleeper sleeper) {
126         mTestAppInstallSetup = testAppInstallSetup;
127         mSleeper = sleeper;
128     }
129 
130     /** {@inheritDoc} */
131     @Override
setUp(ITestDevice device, IBuildInfo buildInfo)132     public void setUp(ITestDevice device, IBuildInfo buildInfo)
133             throws DeviceNotAvailableException, BuildError, TargetSetupError {
134         checkArgumentNonNegative(mMaxRetry, OPTION_MAX_RETRY);
135         checkArgumentNonNegative(
136                 mExponentialBackoffMultiplierSeconds,
137                 OPTION_EXPONENTIAL_BACKOFF_MULTIPLIER_SECONDS);
138         checkArgumentNonNegative(mSetupOnceTimeoutMillis, OPTION_SETUP_TIMEOUT_MILLIS);
139 
140         int runCount = 0;
141         while (true) {
142             TargetSetupError currentException;
143             try {
144                 runCount++;
145 
146                 ITargetPreparer handler =
147                         mTimeLimiter.newProxy(
148                                 new ITargetPreparer() {
149                                     @Override
150                                     public void setUp(ITestDevice device, IBuildInfo buildInfo)
151                                             throws DeviceNotAvailableException, BuildError,
152                                                     TargetSetupError {
153                                         setUpOnce(device, buildInfo);
154                                     }
155                                 },
156                                 ITargetPreparer.class,
157                                 mSetupOnceTimeoutMillis,
158                                 TimeUnit.MILLISECONDS);
159                 handler.setUp(device, buildInfo);
160 
161                 break;
162             } catch (TargetSetupError e) {
163                 currentException = e;
164             } catch (UncheckedTimeoutException e) {
165                 currentException = new TargetSetupError(e.getMessage(), e);
166             }
167 
168             waitForDeviceAvailable(device);
169             if (runCount > mMaxRetry) {
170                 throw currentException;
171             }
172             CLog.w("setUp failed: %s. Run count: %d. Retrying...", currentException, runCount);
173 
174             try {
175                 mSleeper.sleep(
176                         Duration.ofSeconds(
177                                 (int) Math.pow(mExponentialBackoffMultiplierSeconds, runCount)));
178             } catch (InterruptedException e) {
179                 Thread.currentThread().interrupt();
180                 throw new TargetSetupError(e.getMessage(), e);
181             }
182         }
183     }
184 
setUpOnce(ITestDevice device, IBuildInfo buildInfo)185     private void setUpOnce(ITestDevice device, IBuildInfo buildInfo)
186             throws DeviceNotAvailableException, BuildError, TargetSetupError {
187         mTestAppInstallSetup.setAaptVersion(mAaptVersion);
188 
189         try {
190             OptionSetter setter = new OptionSetter(mTestAppInstallSetup);
191             setter.setOptionValue("incremental", String.valueOf(mIncrementalInstallation));
192         } catch (ConfigurationException e) {
193             throw new TargetSetupError(e.getMessage(), e);
194         }
195 
196         if (mPackageName != null) {
197             SystemPackageUninstaller.uninstallPackage(mPackageName, device);
198         }
199 
200         for (File testFile : mTestFiles) {
201             mTestAppInstallSetup.addTestFile(testFile);
202         }
203 
204         for (String installArg : mInstallArgs) {
205             mTestAppInstallSetup.addInstallArg(installArg);
206         }
207 
208         mTestAppInstallSetup.setUp(device, buildInfo);
209     }
210 
211     /** {@inheritDoc} */
212     @Override
tearDown(TestInformation testInfo, Throwable e)213     public void tearDown(TestInformation testInfo, Throwable e) throws DeviceNotAvailableException {
214         mTestAppInstallSetup.tearDown(testInfo, e);
215     }
216 
waitForDeviceAvailable(ITestDevice device)217     private void waitForDeviceAvailable(ITestDevice device) throws DeviceNotAvailableException {
218         if (mWaitForDeviceAvailableSeconds < 0) {
219             return;
220         }
221 
222         device.waitForDeviceAvailable(1000L * mWaitForDeviceAvailableSeconds);
223     }
224 
checkArgumentNonNegative(long val, String name)225     private void checkArgumentNonNegative(long val, String name) {
226         checkArgument(val >= 0, "%s (%s) must not be negative", name, val);
227     }
228 
229     @VisibleForTesting
230     interface Sleeper {
sleep(Duration duration)231         void sleep(Duration duration) throws InterruptedException;
232     }
233 
234     static class Sleepers {
235         enum DefaultSleeper implements Sleeper {
236             INSTANCE;
237 
238             @Override
sleep(Duration duration)239             public void sleep(Duration duration) throws InterruptedException {
240                 Thread.sleep(duration.toMillis());
241             }
242         }
243 
Sleepers()244         private Sleepers() {}
245     }
246 }
247