• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 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 android.packageinstaller.criticaluserjourney.cts;
18 
19 import static android.Manifest.permission.CHANGE_COMPONENT_ENABLED_STATE;
20 import static android.view.WindowInsets.Type.displayCutout;
21 import static android.view.WindowInsets.Type.systemBars;
22 
23 import static com.google.common.truth.Truth.assertThat;
24 import static com.google.common.truth.Truth.assertWithMessage;
25 
26 import static org.junit.Assert.fail;
27 import static org.junit.Assume.assumeFalse;
28 
29 import android.app.Instrumentation;
30 import android.content.ComponentName;
31 import android.content.Context;
32 import android.content.Intent;
33 import android.content.pm.PackageInfo;
34 import android.content.pm.PackageManager;
35 import android.content.pm.ResolveInfo;
36 import android.graphics.Insets;
37 import android.graphics.Rect;
38 import android.net.Uri;
39 import android.provider.DeviceConfig;
40 import android.util.DisplayMetrics;
41 import android.util.Log;
42 import android.view.WindowManager;
43 
44 import androidx.annotation.NonNull;
45 import androidx.annotation.Nullable;
46 import androidx.test.platform.app.InstrumentationRegistry;
47 import androidx.test.uiautomator.By;
48 import androidx.test.uiautomator.BySelector;
49 import androidx.test.uiautomator.UiDevice;
50 import androidx.test.uiautomator.UiObject2;
51 import androidx.test.uiautomator.UiScrollable;
52 import androidx.test.uiautomator.UiSelector;
53 import androidx.test.uiautomator.Until;
54 
55 import com.android.compatibility.common.util.DisableAnimationRule;
56 import com.android.compatibility.common.util.FeatureUtil;
57 import com.android.compatibility.common.util.SystemUtil;
58 
59 import org.junit.After;
60 import org.junit.AssumptionViolatedException;
61 import org.junit.Before;
62 import org.junit.ClassRule;
63 
64 import java.io.ByteArrayOutputStream;
65 import java.io.File;
66 import java.io.IOException;
67 import java.nio.charset.StandardCharsets;
68 import java.util.regex.Pattern;
69 
70 /**
71  * The test base to test PackageInstaller CUJs.
72  */
73 public class PackageInstallerCujTestBase {
74     public static final String TAG = "PackageInstallerCujTestBase";
75 
76     public static final String AUTHORITY_NAME = ".fileprovider";
77     public static final String DEVICE_ADMIN_APK_NAME = "CtsPackageInstallerDeviceAdminApp.apk";
78     public static final String DEVICE_ADMIN_APP_PACKAGE_LABEL =
79             "Cts Package Installer Device Admin App";
80     public static final String DEVICE_ADMIN_APP_PACKAGE_NAME =
81             "android.packageinstaller.cts.deviceadminapp";
82     public static final String DEVICE_ADMIN_APP_RECEIVER_NAME =
83             "android.packageinstaller.cts.deviceadminapp.TestDeviceAdminReceiver";
84     public static final String INSTALLER_APK_NAME = "CtsInstallerCujTestInstaller.apk";
85     public static final String INSTALLER_APK_V2_NAME = "CtsInstallerCujTestInstallerV2.apk";
86     public static final String INSTALLER_LABEL = "CTS CUJ Installer";
87     public static final String INSTALLER_PACKAGE_NAME =
88             "android.packageinstaller.cts.cuj.installer";
89     public static final String TEST_APK_LOCATION = "/data/local/tmp/cts/packageinstaller/cuj";
90     public static final String TEST_APK_NAME = "CtsInstallerCujTestApp.apk";
91     public static final String TEST_APK_V2_NAME = "CtsInstallerCujTestAppV2.apk";
92     public static final String TEST_APP_LABEL = "Installer CUJ Test App";
93     public static final String TEST_APP_PACKAGE_NAME =
94             "android.packageinstaller.cts.cuj.app";
95     public static final String TEST_NO_LAUNCHER_ACTIVITY_APK_NAME =
96             "CtsInstallerCujTestNoLauncherActivityApp.apk";
97     public static final String TEST_NO_LAUNCHER_ACTIVITY_APK_V2_NAME =
98             "CtsInstallerCujTestNoLauncherActivityAppV2.apk";
99 
100     public static final String APP_INSTALLED_LABEL = "App installed";
101     public static final String BUTTON_CANCEL_LABEL = "Cancel";
102     public static final String BUTTON_DONE_LABEL = "Done";
103     public static final String BUTTON_GPP_MORE_DETAILS_LABEL = "More details";
104     public static final String BUTTON_GPP_INSTALL_WITHOUT_SCANNING_LABEL =
105             "Install without scanning";
106     public static final String BUTTON_INSTALL_LABEL = "Install";
107     public static final String BUTTON_OK_LABEL = "OK";
108     public static final String BUTTON_OPEN_LABEL = "Open";
109     public static final String BUTTON_SETTINGS_LABEL = "Settings";
110     public static final String BUTTON_UPDATE_LABEL = "Update";
111     public static final String BUTTON_UPDATE_ANYWAY_LABEL = "Update anyway";
112     public static final String CLONE_LABEL = "Clone";
113     public static final String DELETE_LABEL = "delete";
114     public static final String INSTALLING_LABEL = "Installing";
115     public static final String TOGGLE_ALLOW_LABEL = "allow";
116     public static final String TOGGLE_ALLOW_FROM_LABEL = "Allow from";
117     public static final String TOGGLE_ALLOW_PERMISSION_LABEL = "allow permission";
118     public static final String TOGGLE_INSTALL_UNKNOWN_APPS_LABEL = "install unknown apps";
119     public static final String UNINSTALL_LABEL = "uninstall";
120     public static final String WORK_PROFILE_LABEL = "work profile";
121     public static final String TEXTVIEW_WIDGET_CLASSNAME = "android.widget.TextView";
122 
123     public static final long FIND_OBJECT_TIMEOUT_MS = 20 * 1000L;
124     private static final long WAIT_OBJECT_GONE_TIMEOUT_MS = 3 * 1000L;
125 
126     private static final long TEST_APK_VERSION = 1;
127     private static final long TEST_APK_V2_VERSION = 2;
128 
129     private static final ComponentName TEST_APP_ACTIVITY_COMPONENT = new ComponentName(
130             TEST_APP_PACKAGE_NAME, "android.packageinstaller.cts.cuj.app.MainActivity");
131 
132     private static boolean sUsePiaV2 = false;
133 
134     @ClassRule
135     public static final DisableAnimationRule sDisableAnimationRule = new DisableAnimationRule();
136 
137     private static String sPackageInstallerPackageName = null;
138 
139     private static double sSwipeDeadZonePercentage = 0f;
140 
getInstrumentation()141     public static Instrumentation getInstrumentation() {
142         return InstrumentationRegistry.getInstrumentation();
143     }
144 
getContext()145     public static Context getContext() {
146         return getInstrumentation().getContext();
147     }
148 
getPackageManager()149     public static PackageManager getPackageManager() {
150         return getContext().getPackageManager();
151     }
152 
getUiDevice()153     public static UiDevice getUiDevice() {
154         return UiDevice.getInstance(getInstrumentation());
155     }
156 
157     @Before
setup()158     public void setup() throws Exception {
159         setupTestEnvironment();
160     }
161 
162     /**
163      * 1. Assume the device is running on the supported device and the system has the installed
164      * system package Installer.
165      * 2. Uninstall the {@link #TEST_APP_PACKAGE_NAME} and assert it is not installed.
166      */
setupTestEnvironment()167     public static void setupTestEnvironment() {
168         assumeFalse("The device is not supported", isNotSupportedDevice());
169 
170         assumeFalse("The device doesn't have package installer",
171                 getPackageInstallerPackageName() == null);
172 
173         uninstallTestPackage();
174         assertTestPackageNotInstalled();
175     }
176 
177     @After
tearDown()178     public void tearDown() throws Exception {
179         uninstallTestPackage();
180         // to avoid any UI is still on the screen
181         pressBack();
182     }
183 
184     /**
185      * Assert the test package that is the version 1 is installed.
186      */
assertTestPackageInstalled()187     public static void assertTestPackageInstalled() {
188         assertThat(isInstalledAndVerifyVersionCode(
189                 TEST_APP_PACKAGE_NAME, TEST_APK_VERSION)).isTrue();
190     }
191 
192     /**
193      * Assert the test package that is the version 2 is installed.
194      */
assertTestPackageVersion2Installed()195     public static void assertTestPackageVersion2Installed() {
196         assertThat(isTestPackageVersion2Installed()).isTrue();
197     }
198 
199     /**
200      * Assert the test package is NOT installed.
201      */
assertTestPackageNotInstalled()202     public static void assertTestPackageNotInstalled() {
203         assertThat(isTestPackageInstalled()).isFalse();
204     }
205 
206     /**
207      * Wait for the device idle.
208      */
waitForUiIdle()209     public static void waitForUiIdle() {
210         // Make sure the application is idle and input windows is up-to-date.
211         getInstrumentation().getUiAutomation().syncInputTransactions();
212         getUiDevice().waitForIdle();
213     }
214 
215     /**
216      * Press the back key.
217      */
pressBack()218     public static void pressBack() {
219         getUiDevice().pressBack();
220         waitForUiIdle();
221     }
222 
223     /** Use UiScrollable to scroll forward. */
scrollForward()224     public static void scrollForward() throws Exception {
225         new UiScrollable(new UiSelector().scrollable(true))
226                 .setSwipeDeadZonePercentage(getSwipeDeadZonePercentage())
227                 .scrollForward();
228     }
229 
230     /**
231      * Get the swipe dead zone percentage on the current device. E.g. If display height * 0.1 is
232      * larger than the inset of the system bar + gap buffer, return 0.1f. Otherwise, if the display
233      * height * 0.2 is larger than the inset of the system bar + gap buffer, then return 0.2f. The
234      * maximum value is 0.3f. If the percentage is larger than 0.3f, the range is too small to
235      * scroll.
236      */
getSwipeDeadZonePercentage()237     private static double getSwipeDeadZonePercentage() {
238         if (sSwipeDeadZonePercentage != 0f) {
239             return sSwipeDeadZonePercentage;
240         }
241 
242         DisplayMetrics displayMetrics = getContext().getResources().getDisplayMetrics();
243         // the gap buffer is 24 * dp
244         int gapBuffer = (int) (24 * displayMetrics.density);
245 
246         // Get the insets of system bars and displayCutOut
247         WindowManager wm = getContext().getSystemService(WindowManager.class);
248         Insets insets = wm.getCurrentWindowMetrics().getWindowInsets()
249                 .getInsets(displayCutout() | systemBars());
250 
251         double percentage = 0.1;
252         while (displayMetrics.heightPixels * percentage < insets.bottom + gapBuffer
253                 && percentage < 0.3) {
254             percentage += 0.05;
255         }
256 
257         sSwipeDeadZonePercentage = percentage;
258         Log.d(TAG, "sSwipeDeadZonePercentage = " + sSwipeDeadZonePercentage);
259         return sSwipeDeadZonePercentage;
260     }
261 
262     /**
263      * Click the object and wait for the new window content is changed
264      */
clickAndWaitForNewWindow(UiObject2 uiObject2)265     public static void clickAndWaitForNewWindow(UiObject2 uiObject2) {
266         uiObject2.clickAndWait(Until.newWindow(), WAIT_OBJECT_GONE_TIMEOUT_MS);
267     }
268 
269     /**
270      * Assert the title of the install dialog is {@link #TEST_APP_LABEL}.
271      */
assertTitleIsTestAppLabel()272     public static void assertTitleIsTestAppLabel() throws Exception {
273         findPackageInstallerObject(TEST_APP_LABEL);
274     }
275 
276     /**
277      * Assert the content includes the installer label {@link #INSTALLER_LABEL}.
278      */
assertContentIncludesInstallerLabel()279     public static void assertContentIncludesInstallerLabel() throws Exception {
280         findPackageInstallerObject(By.textContains(INSTALLER_LABEL), /* checkNull= */ true);
281     }
282 
283     /**
284      * Assert the title of the install dialog is {@link #INSTALLER_LABEL}.
285      */
assertTitleIsInstallerLabel()286     public static void assertTitleIsInstallerLabel() throws Exception {
287         findPackageInstallerObject(INSTALLER_LABEL);
288     }
289 
290     /**
291      * Click the Cancel button and wait for the dialog to disappear.
292      */
clickCancelButton()293     public static void clickCancelButton() throws Exception {
294         clickAndWaitForNewWindow(findPackageInstallerObject(BUTTON_CANCEL_LABEL));
295     }
296 
297     /**
298      * Touch outside of the PackageInstaller dialog.
299      */
touchOutside()300     public static void touchOutside() throws Exception {
301         Rect bound = getPackageInstallerDialogBound();
302         DisplayMetrics displayMetrics = getContext().getResources().getDisplayMetrics();
303 
304         // Get the insets of system bars and displayCutOut
305         WindowManager wm = getContext().getSystemService(WindowManager.class);
306         Insets insets = wm.getCurrentWindowMetrics().getWindowInsets().getInsets(
307                 displayCutout() | systemBars());
308 
309         // the minimum of top is the maximum of (display height / 10) and
310         // the top of the insets + 24 * dp
311         int gapBuffer = (int) (24 * displayMetrics.density);
312         int minTop = Math.max(insets.top + gapBuffer, displayMetrics.heightPixels / 10);
313         int maxTop = bound.top - gapBuffer;
314 
315         Log.d(TAG, "touchOutside heightPixels = " + displayMetrics.heightPixels
316                 + ", displayMetrics.density = " + displayMetrics.density + ", insets = " + insets
317                 + ", minTop = " + minTop + ", maxTop = " + maxTop);
318 
319         // x is the center of the dialog
320         int x = (bound.left + bound.right) / 2;
321         // The default value of y is the (minTop + maxTop) / 2
322         int y = (minTop + maxTop) / 2;
323         if (minTop > maxTop) {
324             // the maximum of bottom is the minimum of (display height * 9 / 10) and
325             // the display height - the bottom of the insets - 24 * dp
326             int maxBottom = Math.min(displayMetrics.heightPixels - insets.bottom - gapBuffer,
327                     displayMetrics.heightPixels * 9 / 10);
328             int minBottom = bound.bottom + gapBuffer;
329             Log.d(TAG, "minBottom = " + minBottom + ", maxBottom = " + maxBottom);
330             if (minBottom > maxBottom) {
331                 // close the dialog
332                 pressBack();
333                 throw new AssumptionViolatedException("There is no space to touch outside!");
334             }
335             y = (minBottom + maxBottom) / 2;
336         }
337 
338         Log.d(TAG, "touchOutside x = " + x + ", y = " + y);
339         getUiDevice().click(x, y);
340         waitForUiIdle();
341     }
342 
getPackageInstallerDialogBound()343     private static Rect getPackageInstallerDialogBound() {
344         UiObject2 object = getUiDevice().findObject(By.pkg(getPackageInstallerPackageName()));
345         UiObject2 parent = object.getParent();
346         while (parent != null) {
347             object = parent;
348             parent = object.getParent();
349         }
350         logUiObject(object);
351         return object.getVisibleBounds();
352     }
353 
354     /**
355      * Log some values about the {@code uiObject}
356      */
logUiObject(@onNull UiObject2 uiObject)357     public static void logUiObject(@NonNull UiObject2 uiObject) {
358         Log.d(TAG, "Found bounds: " + uiObject.getVisibleBounds()
359                 + " of object: " + uiObject + ", text: " + uiObject.getText()
360                 + ", package: " + uiObject.getApplicationPackage() + ", className: "
361                 + uiObject.getClassName());
362     }
363 
364     /**
365      * Get the new BySelector with the package name is {@link #getPackageInstallerPackageName()}.
366      */
getPackageInstallerBySelector(BySelector bySelector)367     public static BySelector getPackageInstallerBySelector(BySelector bySelector) {
368         return bySelector.pkg(getPackageInstallerPackageName());
369     }
370 
371     /**
372      * Find the UiObject2 with the {@code name} and the object's package name is
373      * {@link #getPackageInstallerPackageName()}.
374      */
findPackageInstallerObject(String name)375     public static UiObject2 findPackageInstallerObject(String name) throws Exception {
376         final Pattern namePattern = Pattern.compile(name, Pattern.CASE_INSENSITIVE);
377         return findPackageInstallerObject(By.text(namePattern), /* checkNull= */ true);
378     }
379 
380     /**
381      * Find the UiObject2 with the {@code name} and the object's package name is
382      * {@link #getPackageInstallerPackageName()}. If {@code checkNull} is true, also check the
383      * object is not null.
384      */
findPackageInstallerObject(BySelector bySelector, boolean checkNull)385     public static UiObject2 findPackageInstallerObject(BySelector bySelector, boolean checkNull)
386             throws Exception {
387         return findObject(getPackageInstallerBySelector(bySelector), checkNull);
388     }
389 
390     /**
391      * Find the UiObject2 with the {@code name}.
392      */
findObject(String name)393     public static UiObject2 findObject(String name) throws Exception {
394         final Pattern namePattern = Pattern.compile(name, Pattern.CASE_INSENSITIVE);
395         return findObject(By.text(namePattern), /* checkNull= */ true);
396     }
397 
398     /**
399      * Find the UiObject2 with the {@code bySelector}. If {@code checkNull} is true, also
400      * check the object is not null.
401      */
402     @Nullable
findObject(BySelector bySelector, boolean checkNull)403     public static UiObject2 findObject(BySelector bySelector, boolean checkNull) throws Exception {
404         return findObject(bySelector, checkNull, FIND_OBJECT_TIMEOUT_MS);
405     }
406 
407     /**
408      * Find the UiObject2 with the {@code bySelector}. If {@code checkNull} is true, also
409      * check the object is not null. The {@code timeoutMs} is the value for waiting time.
410      */
411     @Nullable
findObject(BySelector bySelector, boolean checkNull, long timeoutMs)412     public static UiObject2 findObject(BySelector bySelector, boolean checkNull, long timeoutMs)
413             throws Exception {
414         waitForUiIdle();
415 
416         UiObject2 object = null;
417         long startTime = System.currentTimeMillis();
418         while (startTime + timeoutMs > System.currentTimeMillis()) {
419             try {
420                 waitForUiIdle();
421                 object = getUiDevice().wait(Until.findObject(bySelector), /* timeout= */ 2 * 1000);
422                 if (object != null) {
423                     Log.d(TAG, "Found bounds: " + object.getVisibleBounds()
424                             + " of object: " + bySelector + ", text: " + object.getText()
425                             + " package: " + object.getApplicationPackage() + ", enabled: "
426                             + object.isEnabled() + ", clickable: " + object.isClickable()
427                             + ", contentDescription: " + object.getContentDescription()
428                             + ", resourceName: " + object.getResourceName() + ", visibleCenter: "
429                             + object.getVisibleCenter());
430                     return object;
431                 } else {
432                     // Maybe the screen is small. Scroll forward.
433                     scrollForward();
434                 }
435             } catch (Exception ignored) {
436                 // do nothing
437             }
438         }
439 
440         // dump window hierarchy for debug
441         if (object == null) {
442             dumpWindowHierarchy();
443         }
444 
445         if (checkNull) {
446             assertWithMessage("Can't find object " + bySelector).that(object).isNotNull();
447         }
448         return object;
449     }
450 
451     /**
452      * Wait for the UiObject2 with the {@code bySelector} is gone.
453      */
waitUntilObjectGone(BySelector bySelector)454     public static void waitUntilObjectGone(BySelector bySelector) throws Exception {
455         if (!getUiDevice().wait(Until.gone(bySelector), WAIT_OBJECT_GONE_TIMEOUT_MS)) {
456             // dump window hierarchy for debug
457             dumpWindowHierarchy();
458             fail("The Object: " + bySelector + "did not disappear within "
459                     + WAIT_OBJECT_GONE_TIMEOUT_MS + " milliseconds");
460         }
461         waitForUiIdle();
462     }
463 
464     /**
465      * Dump current window hierarchy to help debug UI
466      */
dumpWindowHierarchy()467     public static void dumpWindowHierarchy() throws InterruptedException, IOException {
468         ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
469         getUiDevice().dumpWindowHierarchy(outputStream);
470         String windowHierarchy = outputStream.toString(StandardCharsets.UTF_8.name());
471 
472         Log.w(TAG, "Window hierarchy:");
473         for (String line : windowHierarchy.split("\n")) {
474             Thread.sleep(10);
475             Log.w(TAG, line);
476         }
477     }
478 
479     /**
480      * Uninstall the test package {@link #TEST_APP_PACKAGE_NAME}.
481      */
uninstallTestPackage()482     public static void uninstallTestPackage() {
483         uninstallPackage(TEST_APP_PACKAGE_NAME);
484     }
485 
486     /**
487      * Uninstall the package with {@code packageName}.
488      */
uninstallPackage(String packageName)489     public static void uninstallPackage(String packageName) {
490         SystemUtil.runShellCommand(String.format("pm uninstall %s", packageName));
491     }
492 
493     /**
494      * Install the test apk with update-ownership.
495      */
installTestPackageWithUpdateOwnership()496     public static void installTestPackageWithUpdateOwnership() throws Exception {
497         SystemUtil.runShellCommand(String.format("pm install -t  --update-ownership -i %s %s",
498                 getContext().getPackageName(),
499                 new File(TEST_APK_LOCATION, TEST_APK_NAME).getCanonicalPath()));
500         assertTestPackageInstalled();
501 
502         // assert the updateOwner package name is getContext().getPackageName()
503         final String updateOwnerPackageName = getPackageManager().getInstallSourceInfo(
504                 TEST_APP_PACKAGE_NAME).getUpdateOwnerPackageName();
505         assertThat(updateOwnerPackageName).isEqualTo(getContext().getPackageName());
506     }
507 
508     /**
509      * Install the test apk {@link #TEST_APK_NAME} and set the installer to be
510      * the package name of the test case.
511      */
installTestPackageWithInstallerPackageName()512     public static void installTestPackageWithInstallerPackageName() throws IOException {
513         installPackage(TEST_APK_NAME, getContext().getPackageName());
514         assertTestPackageInstalled();
515     }
516 
517     /**
518      * Install the test apk {@link #TEST_APK_NAME}.
519      */
installTestPackage()520     public static void installTestPackage() throws IOException {
521         installPackage(TEST_APK_NAME);
522         assertTestPackageInstalled();
523     }
524 
525     /**
526      * Install the test apk that has no launcher activity
527      * {@link #TEST_NO_LAUNCHER_ACTIVITY_APK_NAME}.
528      */
installNoLauncherActivityTestPackage()529     public static void installNoLauncherActivityTestPackage() throws IOException {
530         installPackage(TEST_NO_LAUNCHER_ACTIVITY_APK_NAME);
531         assertTestPackageInstalled();
532     }
533 
534     /**
535      * Return the value of the device config of the {@code name} in PackageManagerService
536      * namespace.
537      */
538     @Nullable
getPackageManagerDeviceProperty(@onNull String name)539     public static String getPackageManagerDeviceProperty(@NonNull String name)
540             throws Exception {
541         return SystemUtil.callWithShellPermissionIdentity(() ->
542                 DeviceConfig.getProperty(DeviceConfig.NAMESPACE_PACKAGE_MANAGER_SERVICE, name));
543     }
544 
545     /**
546      * Set the {@code value} to the device config with the {@code name} in PackageManagerService
547      * namespace.
548      */
setPackageManagerDeviceProperty(@onNull String name, @Nullable String value)549     public static void setPackageManagerDeviceProperty(@NonNull String name,
550             @Nullable String value) throws Exception {
551         SystemUtil.callWithShellPermissionIdentity(() -> DeviceConfig.setProperty(
552                 DeviceConfig.NAMESPACE_PACKAGE_MANAGER_SERVICE, name, value,
553                 /* makeDefault= */ false));
554     }
555 
556     /**
557      * Install the test apk {@code apkName} and set the installer is {@code installerPackageName}.
558      */
installPackage(@onNull String apkName, @NonNull String installerPackageName)559     public static void installPackage(@NonNull String apkName, @NonNull String installerPackageName)
560             throws IOException {
561         Log.d(TAG, "installPackage(): apkName= " + apkName + " installerPackageName= "
562                 + installerPackageName);
563         SystemUtil.runShellCommand(String.format("pm install -i %s -t %s", installerPackageName,
564                 new File(TEST_APK_LOCATION, apkName).getCanonicalPath()));
565     }
566 
567     /**
568      * Install the test apk {@code apkName} for all users.
569      */
installPackage(@onNull String apkName)570     public static void installPackage(@NonNull String apkName) throws IOException {
571         Log.d(TAG, "installPackage(): apkName= " + apkName);
572         SystemUtil.runShellCommand("pm install -t "
573                 + new File(TEST_APK_LOCATION, apkName).getCanonicalPath());
574     }
575 
576     /**
577      * Install the installed {@code packageName} on the user {@code user}.
578      */
installExistingPackageOnUser(String packageName, int userId)579     public static void installExistingPackageOnUser(String packageName, int userId) {
580         Log.d(TAG, "installExistingPackageAsUser(): packageName= " + packageName
581                 + ", userId= " + userId);
582         assertThat(SystemUtil.runShellCommand(
583                 String.format("pm install-existing --user %s %s", userId, packageName)))
584                 .isEqualTo(
585                         String.format("Package %s installed for user: %s\n", packageName, userId));
586     }
587 
588     /**
589      * If the test package {@link #TEST_APP_PACKAGE_NAME} is installed, return true. Otherwise,
590      * return false.
591      */
isTestPackageInstalled()592     public static boolean isTestPackageInstalled() {
593         return isInstalled(TEST_APP_PACKAGE_NAME);
594     }
595 
596     /**
597      * If the test package {@link #TEST_APP_PACKAGE_NAME} is installed on the {@code userContext},
598      * return true. Otherwise, return false.
599      */
isTestPackageInstalledOnUser(@onNull Context userContext)600     public static boolean isTestPackageInstalledOnUser(@NonNull Context userContext) {
601         return isInstalled(userContext, TEST_APP_PACKAGE_NAME);
602     }
603 
604     /**
605      * If the test package {@code packageName} is installed, return true. Otherwise,
606      * return false.
607      */
isInstalled(@onNull String packageName)608     public static boolean isInstalled(@NonNull String packageName) {
609         return isInstalled(getContext(), packageName);
610     }
611 
612     /**
613      * If the test package {@code packageName} is installed on the {@code context},
614      * return true. Otherwise, return false.
615      */
isInstalled(@onNull Context context, @NonNull String packageName)616     public static boolean isInstalled(@NonNull Context context, @NonNull String packageName) {
617         Log.d(TAG, "Testing if package " + packageName + " is installed for user "
618                 + context.getUser());
619         try {
620             context.getPackageManager().getPackageInfo(packageName, /* flags= */ 0);
621             return true;
622         } catch (PackageManager.NameNotFoundException e) {
623             Log.v(TAG, "Package " + packageName + " not installed for user "
624                     + context.getUser() + ": " + e);
625             return false;
626         }
627     }
628 
629     /**
630      * If the test package {@link #TEST_APP_PACKAGE_NAME} with version {@link #TEST_APK_V2_VERSION}
631      * is installed, return true. Otherwise, return false.
632      */
isTestPackageVersion2Installed()633     public static boolean isTestPackageVersion2Installed() {
634         return isInstalledAndVerifyVersionCode(TEST_APP_PACKAGE_NAME, TEST_APK_V2_VERSION);
635     }
636 
637     /**
638      * If the test package {@code packageName} with version {@code versionCode}
639      * is installed, return true. Otherwise, return false.
640      */
isInstalledAndVerifyVersionCode(@onNull String packageName, long versionCode)641     public static boolean isInstalledAndVerifyVersionCode(@NonNull String packageName,
642             long versionCode) {
643         Log.d(TAG, "Testing if package " + packageName + " is installed for user "
644                 + getContext().getUser() + ", with version code " + versionCode);
645         try {
646             PackageInfo packageInfo = getPackageManager().getPackageInfo(packageName,
647                     /* flags= */ 0);
648             return packageInfo.getLongVersionCode() == versionCode;
649         } catch (PackageManager.NameNotFoundException e) {
650             Log.v(TAG, "Package " + packageName + " not installed for user "
651                     + getContext().getUser() + ": " + e);
652             return false;
653         }
654     }
655 
656     /**
657      * Disable the launcher activity of the test app.
658      */
disableTestPackageLauncherActivity()659     public static void disableTestPackageLauncherActivity() {
660         SystemUtil.runWithShellPermissionIdentity(
661                 () -> getPackageManager().setComponentEnabledSetting(TEST_APP_ACTIVITY_COMPONENT,
662                         PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
663                         PackageManager.DONT_KILL_APP), CHANGE_COMPONENT_ENABLED_STATE);
664     }
665 
666     @Nullable
getPackageInstallerPackageName()667     public static String getPackageInstallerPackageName() {
668         if (sPackageInstallerPackageName != null) {
669             return sPackageInstallerPackageName;
670         }
671         final Intent intent = new Intent(
672                 Intent.ACTION_INSTALL_PACKAGE).setData(Uri.parse("content:"));
673         final ResolveInfo ri = getPackageManager().resolveActivity(intent, /* flags= */ 0);
674         sPackageInstallerPackageName = ri != null ? ri.activityInfo.packageName : null;
675         Log.d(TAG, "sPackageInstallerPackageName = " + sPackageInstallerPackageName);
676         return sPackageInstallerPackageName;
677     }
678 
isNotSupportedDevice()679     private static boolean isNotSupportedDevice() {
680         return FeatureUtil.isArc()
681                 || FeatureUtil.isAutomotive()
682                 || FeatureUtil.isTV()
683                 || FeatureUtil.isWatch()
684                 || FeatureUtil.isVrHeadset();
685     }
686 }
687