1 /*
2  * Copyright (C) 2019 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.cts.install.lib;
18 
19 import static com.google.common.truth.Truth.assertThat;
20 
21 import android.content.Intent;
22 import android.content.pm.PackageInstaller;
23 import android.os.Build;
24 import android.text.TextUtils;
25 
26 import com.android.compatibility.common.util.ApiLevelUtil;
27 import com.android.compatibility.common.util.SystemUtil;
28 import com.android.modules.utils.build.SdkLevel;
29 
30 import java.io.IOException;
31 import java.io.InputStream;
32 import java.io.OutputStream;
33 import java.util.concurrent.TimeUnit;
34 
35 /**
36  * Builder class for installing test apps and creating install sessions.
37  */
38 public class Install {
39     // The collection of apps to be installed with parameters inherited from parent Install object.
40     private final TestApp[] mTestApps;
41     // The collection of apps to be installed with parameters independent of parent Install object.
42     private final Install[] mChildInstalls;
43     // Indicates whether Install represents a multiPackage install.
44     private final boolean mIsMultiPackage;
45     // PackageInstaller.Session parameters.
46     private String mPackageName = null;
47     private boolean mIsStaged = false;
48     private boolean mIsDowngrade = false;
49     private boolean mEnableRollback = false;
50     private int mRollbackDataPolicy = 0;
51     private long mLifetimeMillis = 0;
52     private int mRollbackImpactLevel = 0;
53     private int mSessionMode = PackageInstaller.SessionParams.MODE_FULL_INSTALL;
54     private int mInstallFlags = 0;
55     private boolean mBypassAllowedApexUpdateCheck = true;
56     private boolean mBypassStagedInstallerCheck = true;
57     private long mTimeoutMillis = TimeUnit.MINUTES.toMillis(5);
58 
59     private boolean mDisableVerifier = true;
60 
Install(boolean isMultiPackage, TestApp... testApps)61     private Install(boolean isMultiPackage, TestApp... testApps) {
62         mIsMultiPackage = isMultiPackage;
63         mTestApps = testApps;
64         mChildInstalls = new Install[0];
65     }
66 
Install(boolean isMultiPackage, Install... installs)67     private Install(boolean isMultiPackage, Install... installs) {
68         mIsMultiPackage = isMultiPackage;
69         mTestApps = new TestApp[0];
70         mChildInstalls = installs;
71     }
72 
73     /**
74      * Creates an Install builder to install a single package.
75      */
single(TestApp testApp)76     public static Install single(TestApp testApp) {
77         return new Install(false, testApp);
78     }
79 
80     /**
81      * Creates an Install builder to install using multiPackage.
82      */
multi(TestApp... testApps)83     public static Install multi(TestApp... testApps) {
84         return new Install(true, testApps);
85     }
86 
87     /**
88      * Creates an Install builder from separate Install builders. The newly created builder
89      * will be responsible for building the parent session, while each one of the other builders
90      * will be responsible for building one of the child sessions.
91      *
92      * <p>Modifications to the parent install are not propagated to the child installs,
93      * and vice versa. This gives more control over a multi install session,
94      * e.g. can setStaged on a subset of the child sessions or setStaged on a child session but
95      * not on the parent session.
96      *
97      * <p>It's encouraged to use {@link #multi} that receives {@link TestApp}s
98      * instead of {@link Install}s. This variation of {@link #multi} should be used only if it's
99      * necessary to modify parameters in a subset of the installed sessions.
100      */
multi(Install... installs)101     public static Install multi(Install... installs) {
102         for (Install childInstall : installs) {
103             assertThat(childInstall.isMultiPackage()).isFalse();
104         }
105         Install install = new Install(true, installs);
106         return install;
107     }
108 
109     /**
110      * Sets package name to the session params.
111      */
setPackageName(String packageName)112     public Install setPackageName(String packageName) {
113         mPackageName = packageName;
114         return this;
115     }
116 
117     /**
118      * Makes the install a staged install.
119      */
setStaged()120     public Install setStaged() {
121         mIsStaged = true;
122         return this;
123     }
124 
125     /**
126      * Marks the install as a downgrade.
127      */
setRequestDowngrade()128     public Install setRequestDowngrade() {
129         mIsDowngrade = true;
130         return this;
131     }
132 
133     /**
134      * Enables rollback for the install.
135      */
setEnableRollback()136     public Install setEnableRollback() {
137         mEnableRollback = true;
138         return this;
139     }
140 
141     /**
142      * Enables rollback for the install with specified rollback data policy.
143      */
setEnableRollback(int dataPolicy)144     public Install setEnableRollback(int dataPolicy) {
145         mEnableRollback = true;
146         mRollbackDataPolicy = dataPolicy;
147         return this;
148     }
149 
150     /**
151      * Sets lifetimeMillis for rollback expiration.
152      */
setRollbackLifetimeMillis(long lifetimeMillis)153     public Install setRollbackLifetimeMillis(long lifetimeMillis) {
154         mLifetimeMillis = lifetimeMillis;
155         return this;
156     }
157 
158     /**
159      * Sets rollbackImpactLevel for the install.
160      */
setRollbackImpactLevel(int impactLevel)161     public Install setRollbackImpactLevel(int impactLevel) {
162         mRollbackImpactLevel = impactLevel;
163         return this;
164     }
165 
166     /**
167      * Sets the session mode {@link PackageInstaller.SessionParams#MODE_INHERIT_EXISTING}.
168      * If it's not set, then the default session mode is
169      * {@link PackageInstaller.SessionParams#MODE_FULL_INSTALL}
170      */
setSessionMode(int sessionMode)171     public Install setSessionMode(int sessionMode) {
172         mSessionMode = sessionMode;
173         return this;
174     }
175 
176     /**
177      * Sets the session params.
178      */
addInstallFlags(int installFlags)179     public Install addInstallFlags(int installFlags) {
180         mInstallFlags |= installFlags;
181         return this;
182     }
183 
184     /**
185      * Sets whether to call {@code pm bypass-allowed-apex-update-check true} when creating install
186      * session.
187      */
setBypassAllowedApexUpdateCheck(boolean bypassAllowedApexUpdateCheck)188     public Install setBypassAllowedApexUpdateCheck(boolean bypassAllowedApexUpdateCheck) {
189         mBypassAllowedApexUpdateCheck = bypassAllowedApexUpdateCheck;
190         return this;
191     }
192 
193     /**
194      * Sets whether to call {@code pm bypass-staged-installer-check true} when creating install
195      * session.
196      */
setBypassStangedInstallerCheck(boolean bypassStagedInstallerCheck)197     public Install setBypassStangedInstallerCheck(boolean bypassStagedInstallerCheck) {
198         mBypassStagedInstallerCheck = bypassStagedInstallerCheck;
199         return this;
200     }
201 
202     /**
203      * Sets the installation timeout. {@link #commit()} will fail if install doesn't
204      * complete within the timeout. The default is 5 minutes.
205      */
setTimeout(long timeoutMillis)206     public Install setTimeout(long timeoutMillis) {
207         mTimeoutMillis = timeoutMillis;
208         return this;
209     }
210 
211     /**
212      * Enable verifier for testing purpose. The default is to disable the verifier.
213      */
enableVerifier()214     public Install enableVerifier() {
215         mDisableVerifier = false;
216         return this;
217     }
218 
219     /**
220      * Commits the install.
221      *
222      * @return the session id of the install session, if the session is successful.
223      * @throws AssertionError if the install doesn't succeed.
224      */
commit()225     public int commit() throws IOException, InterruptedException {
226         int sessionId = createSession();
227         try (PackageInstaller.Session session =
228                      InstallUtils.openPackageInstallerSession(sessionId)) {
229             LocalIntentSender sender = new LocalIntentSender();
230             session.commit(sender.getIntentSender());
231             Intent result = sender.pollResult(mTimeoutMillis, TimeUnit.MILLISECONDS);
232             if (result == null) {
233                 throw new AssertionError("Install timeout, sessionId=" + sessionId);
234             }
235             InstallUtils.assertStatusSuccess(result);
236             return sessionId;
237         }
238     }
239 
240     /**
241      * Kicks off an install flow by creating an install session
242      * and, in the case of a multiPackage install, child install sessions.
243      *
244      * @return the session id of the install session, if the session is successful.
245      */
createSession()246     public int createSession() throws IOException {
247         int sessionId;
248         if (isMultiPackage()) {
249             sessionId = createEmptyInstallSession(/*multiPackage*/ true, /*isApex*/false);
250             try (PackageInstaller.Session session =
251                          InstallUtils.openPackageInstallerSession(sessionId)) {
252                 for (Install subInstall : mChildInstalls) {
253                     session.addChildSessionId(subInstall.createSession());
254                 }
255                 for (TestApp testApp : mTestApps) {
256                     session.addChildSessionId(createSingleInstallSession(testApp));
257                 }
258             }
259         } else {
260             assert mTestApps.length == 1;
261             sessionId = createSingleInstallSession(mTestApps[0]);
262         }
263         return sessionId;
264     }
265 
266     /**
267      * Creates an empty install session with appropriate install params set.
268      *
269      * @return the session id of the newly created session
270      */
createEmptyInstallSession(boolean multiPackage, boolean isApex)271     private int createEmptyInstallSession(boolean multiPackage, boolean isApex)
272             throws IOException {
273         if ((mIsStaged || isApex) && mBypassStagedInstallerCheck) {
274             SystemUtil.runShellCommandForNoOutput("pm bypass-staged-installer-check true");
275         }
276         if (isApex && mBypassAllowedApexUpdateCheck) {
277             SystemUtil.runShellCommandForNoOutput("pm bypass-allowed-apex-update-check true");
278         }
279         if (mDisableVerifier && ApiLevelUtil.isAfter(Build.VERSION_CODES.TIRAMISU)) {
280             // This command is only available in U and later
281             SystemUtil.runShellCommandForNoOutput("pm disable-verification-for-uid "
282                     + android.os.Process.myUid());
283         }
284         try {
285             PackageInstaller.SessionParams params =
286                     new PackageInstaller.SessionParams(mSessionMode);
287             if (!TextUtils.isEmpty(mPackageName)) {
288                 params.setAppPackageName(mPackageName);
289             }
290             if (multiPackage) {
291                 params.setMultiPackage();
292             }
293             if (isApex) {
294                 params.setInstallAsApex();
295             }
296             if (mIsStaged) {
297                 params.setStaged();
298             }
299             if (SdkLevel.isAtLeastS()) {
300                 params.setInstallFlagAllowTest();
301             }
302             params.setRequestDowngrade(mIsDowngrade);
303             params.setEnableRollback(mEnableRollback, mRollbackDataPolicy);
304             if (mEnableRollback && mLifetimeMillis > 0) {
305                 params.setRollbackLifetimeMillis(mLifetimeMillis);
306             }
307             if (mEnableRollback && mRollbackImpactLevel >= 0) {
308                 params.setRollbackImpactLevel(mRollbackImpactLevel);
309             }
310             if (mInstallFlags != 0) {
311                 InstallUtils.mutateInstallFlags(params, mInstallFlags);
312             }
313             PackageInstaller installer = InstallUtils.getPackageInstaller();
314             if (installer == null) {
315                 // installer may be null, eg. instant app
316                 throw new IllegalStateException("PackageInstaller not found");
317             }
318             return installer.createSession(params);
319         } finally {
320             if ((mIsStaged || isApex) && mBypassStagedInstallerCheck) {
321                 SystemUtil.runShellCommandForNoOutput("pm bypass-staged-installer-check false");
322             }
323             if (isApex && mBypassAllowedApexUpdateCheck) {
324                 SystemUtil.runShellCommandForNoOutput("pm bypass-allowed-apex-update-check false");
325             }
326         }
327     }
328 
329     /**
330      * Creates an install session for the given test app.
331      *
332      * @return the session id of the newly created session.
333      */
createSingleInstallSession(TestApp app)334     private int createSingleInstallSession(TestApp app) throws IOException {
335         int sessionId = createEmptyInstallSession(/*multiPackage*/false, app.isApex());
336         try (PackageInstaller.Session session =
337                      InstallUtils.getPackageInstaller().openSession(sessionId)) {
338             for (String resourceName : app.getResourceNames()) {
339                 try (OutputStream os = session.openWrite(resourceName, 0, -1);
340                      InputStream is = app.getResourceStream(resourceName);) {
341                     if (is == null) {
342                         throw new IOException("Resource " + resourceName + " not found");
343                     }
344                     byte[] buffer = new byte[4096];
345                     int n;
346                     while ((n = is.read(buffer)) >= 0) {
347                         os.write(buffer, 0, n);
348                     }
349                 }
350             }
351             return sessionId;
352         }
353     }
354 
isMultiPackage()355     private boolean isMultiPackage() {
356         return mIsMultiPackage;
357     }
358 
359 }
360