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.app.AppOpsManager.MODE_ALLOWED;
20 import static android.app.AppOpsManager.OPSTR_REQUEST_INSTALL_PACKAGES;
21 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
22 import static android.content.pm.PackageInstaller.EXTRA_STATUS;
23 import static android.content.pm.PackageInstaller.STATUS_FAILURE_ABORTED;
24 import static android.content.pm.PackageInstaller.STATUS_PENDING_USER_ACTION;
25 import static android.content.pm.PackageInstaller.STATUS_SUCCESS;
26 
27 import static com.google.common.truth.Truth.assertThat;
28 import static com.google.common.truth.Truth.assertWithMessage;
29 
30 import static org.junit.Assert.fail;
31 import static org.junit.Assume.assumeFalse;
32 
33 import android.app.Activity;
34 import android.app.Instrumentation;
35 import android.content.BroadcastReceiver;
36 import android.content.Context;
37 import android.content.Intent;
38 import android.content.IntentFilter;
39 import android.content.pm.PackageInfo;
40 import android.content.pm.PackageManager;
41 import android.content.pm.ResolveInfo;
42 import android.graphics.Rect;
43 import android.net.Uri;
44 import android.text.TextUtils;
45 import android.util.DisplayMetrics;
46 import android.util.Log;
47 
48 import androidx.annotation.NonNull;
49 import androidx.annotation.Nullable;
50 import androidx.core.content.FileProvider;
51 import androidx.test.platform.app.InstrumentationRegistry;
52 import androidx.test.uiautomator.By;
53 import androidx.test.uiautomator.BySelector;
54 import androidx.test.uiautomator.UiDevice;
55 import androidx.test.uiautomator.UiObject2;
56 import androidx.test.uiautomator.UiScrollable;
57 import androidx.test.uiautomator.UiSelector;
58 import androidx.test.uiautomator.Until;
59 
60 import com.android.compatibility.common.util.AppOpsUtils;
61 import com.android.compatibility.common.util.DisableAnimationRule;
62 import com.android.compatibility.common.util.FeatureUtil;
63 import com.android.compatibility.common.util.SystemUtil;
64 
65 import org.junit.After;
66 import org.junit.AfterClass;
67 import org.junit.Before;
68 import org.junit.BeforeClass;
69 import org.junit.ClassRule;
70 
71 import java.io.File;
72 import java.io.FileInputStream;
73 import java.io.FileOutputStream;
74 import java.io.IOException;
75 import java.io.InputStream;
76 import java.io.OutputStream;
77 import java.util.List;
78 import java.util.Locale;
79 import java.util.concurrent.CompletableFuture;
80 import java.util.concurrent.TimeUnit;
81 import java.util.regex.Pattern;
82 
83 /**
84  * The test base to test PackageInstaller CUJs.
85  */
86 public class PackageInstallerCujTestBase {
87     private static final String TAG = "PackageInstallerCujTestBase";
88 
89     private static final String CONTENT_AUTHORITY =
90             "android.packageinstaller.criticaluserjourney.cts.fileprovider";
91     private static final String TEST_APK_LABEL = "Empty Test App";
92     private static final String TEST_APK_NAME = "CtsEmptyTestApp.apk";
93     private static final String TEST_APK_V2_LABEL = "Empty Test App V2";
94     private static final String TEST_APK_V2_NAME = "CtsEmptyTestAppV2.apk";
95     private static final String TEST_APK_PACKAGE_NAME = "android.packageinstaller.emptytestapp.cts";
96     private static final String TEST_INSTALLER_PACKAGE_NAME =
97             "android.packageinstaller.cts.cujinstaller";
98     private static final String TEST_INSTALLER_APK_NAME = "CtsCujInstallerTestApp.apk";
99     private static final String TEST_APK_LOCATION = "/data/local/tmp/cts/packageinstaller/cuj";
100     private static final String APP_INSTALLED_LABEL = "App installed";
101     private static final String BUTTON_CANCEL_LABEL = "Cancel";
102     private static final String BUTTON_DONE_LABEL = "Done";
103     private static final String BUTTON_GPP_MORE_DETAILS_LABEL = "More details";
104     private static final String BUTTON_GPP_INSTALL_ANYWAY_LABEL = "Install anyway";
105     private static final String BUTTON_INSTALL_LABEL = "Install";
106     private static final String BUTTON_SETTINGS_LABEL = "Settings";
107     private static final String BUTTON_UPDATE_LABEL = "Update";
108     private static final String TOGGLE_ALLOW_LABEL = "allow";
109     private static final String TOGGLE_ALLOW_FROM_LABEL = "Allow from";
110     private static final String TOGGLE_ALLOW_PERMISSION_LABEL = "allow permission";
111     private static final String TOGGLE_INSTALL_UNKNOWN_APPS_LABEL = "install unknown apps";
112     private static final String INSTALLING_LABEL = "Installing";
113     private static final String TEXTVIEW_WIDGET_CLASSNAME = "android.widget.TextView";
114 
115     private static final String ACTION_LAUNCH_INSTALLER =
116             "android.packageinstaller.cts.cujinstaller.action.LAUNCH_INSTALLER";
117 
118     private static final String ACTION_REQUEST_INSTALLER =
119             "android.packageinstaller.cts.cujinstaller.action.REQUEST_INSTALLER";
120 
121     private static final String ACTION_RESPONSE_INSTALLER =
122             "android.packageinstaller.cts.cujinstaller.action.RESPONSE_INSTALLER";
123 
124     private static final String EXTRA_EVENT = "extra_event";
125     private static final String EXTRA_TEST_APK_URI = "extra_test_apk_uri";
126     private static final String EXTRA_TEST_APK_V2_URI = "extra_test_apk_v2_uri";
127     private static final String EXTRA_USE_APK_V2 = "extra_use_apk_v2";
128 
129     private static final int EVENT_REQUEST_INSTALLER_CLEAN_UP = -1;
130     private static final int EVENT_REQUEST_INSTALLER_SESSION = 0;
131     private static final int EVENT_REQUEST_INSTALLER_INTENT = 1;
132     private static final int EVENT_REQUEST_INSTALLER_INTENT_FOR_RESULT = 2;
133     private static final int EVENT_REQUEST_INSTALLER_INTENT_WITH_PACKAGE_URI = 3;
134     private static final int EVENT_REQUEST_INSTALLER_INTENT_WITH_PACKAGE_URI_FOR_RESULT = 4;
135 
136     private static final int STATUS_CUJ_INSTALLER_READY = 1000;
137     private static final int STATUS_CUJ_INSTALLER_START_ACTIVITY_READY = 1001;
138 
139     private static final long FIND_OBJECT_TIMEOUT_MS = 30 * 1000L;
140     private static final long WAIT_OBJECT_GONE_TIMEOUT_MS = 3 * 1000L;
141 
142     @ClassRule
143     public static final DisableAnimationRule sDisableAnimationRule = new DisableAnimationRule();
144 
145     private static Context sContext;
146     private static PackageManager sPackageManager;
147     private static InstallerResponseReceiver sInstallerResponseReceiver;
148     private static Instrumentation sInstrumentation;
149     private static UiDevice sUiDevice;
150     private static String sToggleLabel = null;
151     private static String sPackageInstallerPackageName = null;
152 
153     @BeforeClass
setUpClass()154     public static void setUpClass() throws Exception {
155         sInstrumentation = InstrumentationRegistry.getInstrumentation();
156         sContext = sInstrumentation.getTargetContext();
157         sPackageManager = sContext.getPackageManager();
158         sInstallerResponseReceiver = new InstallerResponseReceiver();
159         sPackageInstallerPackageName = getPackageInstallerPackageName();
160         Log.d(TAG, "sPackageInstallerPackageName = " + sPackageInstallerPackageName);
161 
162         copyTestFiles();
163 
164         // Unblock UI
165         sUiDevice = UiDevice.getInstance(sInstrumentation);
166         if (!sUiDevice.isScreenOn()) {
167             sUiDevice.wakeUp();
168         }
169         sUiDevice.executeShellCommand("wm dismiss-keyguard");
170 
171         sContext.registerReceiver(sInstallerResponseReceiver,
172                 new IntentFilter(ACTION_RESPONSE_INSTALLER), Context.RECEIVER_EXPORTED);
173     }
174 
175     @Before
setup()176     public void setup() throws Exception {
177         assumeFalse("The device is not supported", isNotSupportedDevice());
178 
179         assumeFalse("The device doesn't have package installer",
180                 sPackageInstallerPackageName == null);
181 
182         uninstallTestPackage();
183         assertTestPackageNotInstalled();
184 
185         uninstallInstallerPackage();
186         assertInstallerNotInstalled();
187 
188         sInstallerResponseReceiver.resetResult();
189 
190         // install the test installer before the test case is running everytime to make sure the
191         // AppOps permission mode is the default mode.
192         installPackage(TEST_INSTALLER_APK_NAME);
193         assertThat(isInstalled(TEST_INSTALLER_PACKAGE_NAME)).isTrue();
194         startInstallerActivity();
195 
196         waitForUiIdle();
197         assertCUJInstallerReady();
198     }
199 
200     @After
tearDown()201     public void tearDown() throws Exception {
202         requestInstallerCleanUp();
203 
204         uninstallTestPackage();
205         uninstallInstallerPackage();
206         // to avoid any UI is still on the screen
207         pressBack();
208     }
209 
210     @AfterClass
tearDownClass()211     public static void tearDownClass() throws Exception {
212         sInstallerResponseReceiver.unregisterReceiver(sContext);
213         sInstallerResponseReceiver = null;
214         sPackageManager = null;
215         sContext = null;
216         sUiDevice = null;
217         sInstrumentation = null;
218     }
219 
220     /**
221      * Grant the REQUEST_INSTALL_PACKAGES AppOps permission to the CUJ Installer.
222      */
grantRequestInstallPackagesPermission()223     public static void grantRequestInstallPackagesPermission() throws Exception {
224         AppOpsUtils.setOpMode(TEST_INSTALLER_PACKAGE_NAME, OPSTR_REQUEST_INSTALL_PACKAGES,
225                 MODE_ALLOWED);
226     }
227 
copyTestFiles()228     private static void copyTestFiles() throws Exception {
229         final File apkFile = new File(TEST_APK_LOCATION, TEST_APK_NAME);
230         final File dstFile = new File(sContext.getFilesDir(), TEST_APK_NAME);
231         copyFile(apkFile, dstFile);
232 
233         final File apkV2File = new File(TEST_APK_LOCATION, TEST_APK_V2_NAME);
234         final File dstV2File = new File(sContext.getFilesDir(), TEST_APK_V2_NAME);
235         copyFile(apkV2File, dstV2File);
236     }
237 
copyFile(File src, File dst)238     private static void copyFile(File src, File dst) throws Exception {
239         try (InputStream source = new FileInputStream(src);
240                 OutputStream target = new FileOutputStream(dst)) {
241             byte[] buffer = new byte[1024];
242             for (int len = source.read(buffer); len > 0; len = source.read(buffer)) {
243                 target.write(buffer, 0, len);
244             }
245         }
246     }
247 
startInstallerActivity()248     private static void startInstallerActivity() {
249         final File apkFile = new File(sContext.getFilesDir(), TEST_APK_NAME);
250         final File apkV2File = new File(sContext.getFilesDir(), TEST_APK_V2_NAME);
251         final Intent intent = new Intent();
252         intent.setPackage(TEST_INSTALLER_PACKAGE_NAME);
253         intent.setFlags(FLAG_ACTIVITY_NEW_TASK);
254         intent.setAction(ACTION_LAUNCH_INSTALLER);
255         Uri testApkUri = FileProvider.getUriForFile(sContext, CONTENT_AUTHORITY, apkFile);
256         Uri testApkV2Uri = FileProvider.getUriForFile(sContext, CONTENT_AUTHORITY, apkV2File);
257         intent.putExtra(EXTRA_TEST_APK_URI, testApkUri.toString());
258         intent.putExtra(EXTRA_TEST_APK_V2_URI, testApkV2Uri.toString());
259 
260         // grant read uri permission to the installer
261         sContext.grantUriPermission(TEST_INSTALLER_PACKAGE_NAME, testApkUri,
262                 Intent.FLAG_GRANT_READ_URI_PERMISSION);
263         sContext.grantUriPermission(TEST_INSTALLER_PACKAGE_NAME, testApkV2Uri,
264                 Intent.FLAG_GRANT_READ_URI_PERMISSION);
265         sContext.startActivity(intent);
266     }
267 
requestInstallerCleanUp()268     private static void requestInstallerCleanUp() throws Exception {
269         sendRequestInstallerBroadcast(EVENT_REQUEST_INSTALLER_CLEAN_UP);
270     }
271 
272     /**
273      * Start the installation via PackageInstaller.Session APIs.
274      */
startInstallationViaPackageInstallerSession()275     public static void startInstallationViaPackageInstallerSession() throws Exception {
276         sendRequestInstallerBroadcast(EVENT_REQUEST_INSTALLER_SESSION);
277         assertInstallPendingUserAction();
278     }
279 
280     /**
281      * Start the installation to update the test apk to V2 version via
282      * PackageInstaller.Session APIs.
283      */
startInstallationUpdateViaPackageInstallerSession()284     public static void startInstallationUpdateViaPackageInstallerSession() throws Exception {
285         sendRequestInstallerBroadcast(EVENT_REQUEST_INSTALLER_SESSION, /* useV2= */ true);
286         assertInstallPendingUserAction();
287     }
288 
sendRequestInstallerBroadcast(int event)289     private static void sendRequestInstallerBroadcast(int event) throws Exception {
290         sendRequestInstallerBroadcast(event, /* useV2= */ false);
291     }
292 
293     /**
294      * Start the installation via startActivity.
295      */
startInstallationViaIntent()296     public static void startInstallationViaIntent() throws Exception {
297         sendRequestInstallerBroadcast(EVENT_REQUEST_INSTALLER_INTENT);
298         assertCUJInstallerStartActivityReady();
299     }
300 
301     /**
302      * Start the installation to update the test apk to label V2 version via startActivity.
303      */
startInstallationUpdateViaIntent()304     public static void startInstallationUpdateViaIntent() throws Exception {
305         sendRequestInstallerBroadcast(EVENT_REQUEST_INSTALLER_INTENT, /* useV2= */ true);
306         assertCUJInstallerStartActivityReady();
307     }
308 
309     /**
310      * Start the installation via startActivity with Package uri.
311      */
startInstallationViaIntentWithPackageUri()312     public static void startInstallationViaIntentWithPackageUri() throws Exception {
313         sendRequestInstallerBroadcast(EVENT_REQUEST_INSTALLER_INTENT_WITH_PACKAGE_URI);
314         assertCUJInstallerStartActivityReady();
315     }
316 
317     /**
318      * Start the installation via startActivityForResult.
319      */
startInstallationViaIntentForResult()320     public static void startInstallationViaIntentForResult() throws Exception {
321         sendRequestInstallerBroadcast(EVENT_REQUEST_INSTALLER_INTENT_FOR_RESULT);
322         assertCUJInstallerStartActivityReady();
323     }
324 
325     /**
326      * Start the installation to update the test apk to label V2 version via startActivityForResult.
327      */
startInstallationUpdateViaIntentForResult()328     public static void startInstallationUpdateViaIntentForResult() throws Exception {
329         sendRequestInstallerBroadcast(EVENT_REQUEST_INSTALLER_INTENT_FOR_RESULT,
330                 /* useV2= */ true);
331         assertCUJInstallerStartActivityReady();
332     }
333 
334     /**
335      * Start the installation via startActivityForResult with Package uri.
336      */
startInstallationViaIntentWithPackageUriForResult()337     public static void startInstallationViaIntentWithPackageUriForResult() throws Exception {
338         sendRequestInstallerBroadcast(EVENT_REQUEST_INSTALLER_INTENT_WITH_PACKAGE_URI_FOR_RESULT);
339         assertCUJInstallerStartActivityReady();
340     }
341 
sendRequestInstallerBroadcast(int event, boolean useV2)342     private static void sendRequestInstallerBroadcast(int event, boolean useV2) throws Exception {
343         final Intent intent = new Intent(ACTION_REQUEST_INSTALLER);
344         intent.setPackage(TEST_INSTALLER_PACKAGE_NAME);
345         intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
346         intent.putExtra(EXTRA_EVENT, event);
347         intent.putExtra(EXTRA_USE_APK_V2, useV2);
348         sContext.sendBroadcast(intent);
349     }
350 
getInstallerResponseResult()351     private static int getInstallerResponseResult() throws Exception {
352         return sInstallerResponseReceiver.getInstallerResponseResult();
353     }
354 
355     /**
356      * Assert the install status is Activity#RESULT_OK).
357      */
assertInstallerResponseActivityResultOK()358     public static void assertInstallerResponseActivityResultOK() throws Exception {
359         assertThat(getInstallerResponseResult()).isEqualTo(Activity.RESULT_OK);
360         sInstallerResponseReceiver.resetResult();
361     }
362 
363     /**
364      * Assert the install status is Activity#RESULT_CANCELED.
365      */
assertInstallerResponseActivityResultCanceled()366     public static void assertInstallerResponseActivityResultCanceled() throws Exception {
367         assertThat(getInstallerResponseResult()).isEqualTo(Activity.RESULT_CANCELED);
368         sInstallerResponseReceiver.resetResult();
369     }
370 
371     /**
372      * Assert the install status is PackageInstaller#STATUS_SUCCESS.
373      */
assertInstallSuccess()374     public static void assertInstallSuccess() throws Exception {
375         assertThat(getInstallerResponseResult()).isEqualTo(STATUS_SUCCESS);
376         sInstallerResponseReceiver.resetResult();
377     }
378 
379     /**
380      * Assert the install status is PackageInstaller#STATUS_FAILURE_ABORTED.
381      */
assertInstallFailureAborted()382     public static void assertInstallFailureAborted() throws Exception {
383         assertThat(getInstallerResponseResult()).isEqualTo(STATUS_FAILURE_ABORTED);
384         sInstallerResponseReceiver.resetResult();
385     }
386 
assertInstallPendingUserAction()387     private static void assertInstallPendingUserAction() throws Exception {
388         assertThat(getInstallerResponseResult()).isEqualTo(STATUS_PENDING_USER_ACTION);
389         sInstallerResponseReceiver.resetResult();
390     }
391 
assertCUJInstallerReady()392     private static void assertCUJInstallerReady() throws Exception {
393         assertThat(getInstallerResponseResult()).isEqualTo(STATUS_CUJ_INSTALLER_READY);
394         sInstallerResponseReceiver.resetResult();
395     }
396 
assertCUJInstallerStartActivityReady()397     private static void assertCUJInstallerStartActivityReady() throws Exception {
398         assertThat(getInstallerResponseResult()).isEqualTo(
399                 STATUS_CUJ_INSTALLER_START_ACTIVITY_READY);
400         sInstallerResponseReceiver.resetResult();
401     }
402 
403     /**
404      * Assert the test package is installed.
405      */
assertTestPackageInstalled()406     public static void assertTestPackageInstalled() {
407         assertThat(isInstalledAndVerifyAppName(TEST_APK_PACKAGE_NAME, TEST_APK_LABEL)).isTrue();
408     }
409 
410     /**
411      * Assert the test package has the V2 label is installed.
412      */
assertTestPackageLabelV2Installed()413     public static void assertTestPackageLabelV2Installed() {
414         assertThat(isTestPackageLabelV2Installed()).isTrue();
415     }
416 
417     /**
418      * Assert the test package is NOT installed.
419      */
assertTestPackageNotInstalled()420     public static void assertTestPackageNotInstalled() {
421         assertThat(isTestPackageInstalled()).isFalse();
422     }
423 
assertInstallerNotInstalled()424     private static void assertInstallerNotInstalled() {
425         assertThat(isInstallerInstalled()).isFalse();
426     }
427 
428     /**
429      * Wait for the device idle.
430      */
waitForUiIdle()431     public static void waitForUiIdle() {
432         sUiDevice.waitForIdle();
433     }
434 
435     /**
436      * Press the back key.
437      */
pressBack()438     public static void pressBack() {
439         sUiDevice.pressBack();
440         waitForUiIdle();
441     }
442 
clickAndWaitForNewWindow(UiObject2 uiObject2)443     private static void clickAndWaitForNewWindow(UiObject2 uiObject2) {
444         uiObject2.clickAndWait(Until.newWindow(), WAIT_OBJECT_GONE_TIMEOUT_MS);
445     }
446 
allowInstallIfGPPDialogExists()447     private static void allowInstallIfGPPDialogExists() {
448         final Pattern morePattern = Pattern.compile(BUTTON_GPP_MORE_DETAILS_LABEL,
449                 Pattern.CASE_INSENSITIVE);
450         UiObject2 more = sUiDevice.findObject(By.text(morePattern));
451         if (more != null) {
452             more.click();
453             waitForUiIdle();
454 
455             BySelector installAnyWaySelector = By.textContains(BUTTON_GPP_INSTALL_ANYWAY_LABEL);
456             UiObject2 installAnyway = findObject(installAnyWaySelector, /* checkNull= */ false);
457             if (installAnyway != null) {
458                 Rect rect = installAnyway.getVisibleBounds();
459                 sUiDevice.click(rect.left, rect.bottom - 10);
460                 // wait for the dialog disappear
461                 waitUntilObjectGone(installAnyWaySelector);
462             }
463         }
464         waitForUiIdle();
465     }
466 
467     /**
468      * Assert the install success dialog and click the Done button.
469      */
assertInstallSuccessDialogAndClickDoneButton()470     public static void assertInstallSuccessDialogAndClickDoneButton() throws Exception {
471         findPackageInstallerObject(By.textContains(APP_INSTALLED_LABEL), /* checkNull= */ true);
472         clickAndWaitForNewWindow(findPackageInstallerObject(BUTTON_DONE_LABEL));
473     }
474 
475     /**
476      * Click the Install button and wait for the dialog to disappear. Also allow install if the
477      * GPP dialog exists.
478      */
clickInstallButton()479     public static void clickInstallButton() {
480         clickInstallButton(/* checkInstallingDialog= */ false);
481     }
482 
483     /**
484      * Click the Install button and wait for the dialog to disappear. Also allow install if the
485      * GPP dialog exists. If {@code checkInstallingDialog} is true, check the Installing dialog.
486      * Otherwise, don't check the Installing dialog. E.g. The installation via intent triggers
487      * the Installing dialog.
488      */
clickInstallButton(boolean checkInstallingDialog)489     public static void clickInstallButton(boolean checkInstallingDialog) {
490         clickAndWaitForNewWindow(findPackageInstallerObject(BUTTON_INSTALL_LABEL));
491 
492         if (checkInstallingDialog) {
493             waitForInstallingDialogGone();
494         }
495 
496         if (!isTestPackageInstalled()) {
497             allowInstallIfGPPDialogExists();
498         }
499     }
500 
501     /**
502      * Click the Update button and wait for the dialog to disappear. Also allow install if the
503      * GPP dialog exists.
504      */
clickUpdateButton()505     public static void clickUpdateButton() {
506         clickUpdateButton(/* checkInstallingDialog= */ false);
507     }
508 
509     /**
510      * Click the Update button and wait for the dialog to disappear. Also allow install if the
511      * GPP dialog exists. If {@code checkInstallingDialog} is true, check the Installing dialog.
512      * Otherwise, don't check the Installing dialog. E.g. The installation via intent triggers
513      * the Installing dialog.
514      */
clickUpdateButton(boolean checkInstallingDialog)515     public static void clickUpdateButton(boolean checkInstallingDialog) {
516         clickUpdateButton(checkInstallingDialog, /* checkGPPDialog= */ true);
517     }
518 
519     /**
520      * Click the Update button and wait for the dialog to disappear. If
521      * {@code checkInstallingDialog} is true, check the Installing dialog. Otherwise, don't
522      * check the Installing dialog. E.g. The installation via intent triggers Installing dialog.
523      * If {@code checkGPPDialog} is true, check the GPP dialog. Otherwise, don't check the GPP
524      * dialog. E.g. The installation via intent with package uri doesn't trigger the GPP dialog.
525      */
clickUpdateButton(boolean checkInstallingDialog, boolean checkGPPDialog)526     public static void clickUpdateButton(boolean checkInstallingDialog, boolean checkGPPDialog) {
527         clickAndWaitForNewWindow(findPackageInstallerObject(BUTTON_UPDATE_LABEL));
528 
529         if (checkInstallingDialog) {
530             waitForInstallingDialogGone();
531         }
532 
533         if (checkGPPDialog && !isTestPackageLabelV2Installed()) {
534             allowInstallIfGPPDialogExists();
535         }
536     }
537 
538     /**
539      * Click the Cancel button and wait for the dialog to disappear.
540      */
clickCancelButton()541     public static void clickCancelButton() {
542         clickAndWaitForNewWindow(findPackageInstallerObject(BUTTON_CANCEL_LABEL));
543     }
544 
545     /**
546      * Click the Settings button and wait for the dialog to disappear.
547      */
clickSettingsButton()548     public static void clickSettingsButton() {
549         clickAndWaitForNewWindow(findPackageInstallerObject(BUTTON_SETTINGS_LABEL));
550     }
551 
552     /**
553      * Toggle to grant the AppOps permission REQUEST_INSTALL_PACKAGES to the CUJ Installer.
554      */
toggleToGrantRequestInstallPackagesPermission()555     public static void toggleToGrantRequestInstallPackagesPermission() {
556         // Already know which toggle label on the device, find it and click it directly
557         if (sToggleLabel != null) {
558             clickAndWaitForNewWindow(findObject(sToggleLabel));
559             return;
560         }
561 
562         // Start to find the objects, find the checkable items first
563         final List<UiObject2> uiObjects = sUiDevice.wait(
564                 Until.findObjects(By.checkable(true).checked(false)), FIND_OBJECT_TIMEOUT_MS);
565 
566         if (uiObjects == null || uiObjects.isEmpty()) {
567             fail("No toggle to grant permission");
568         }
569 
570         Log.d(TAG, "The count of checkable objects is " + uiObjects.size());
571 
572         // Only one item, find the text object
573         if (uiObjects.size() == 1) {
574             UiObject2 toggle = uiObjects.get(0);
575             logUiObject(toggle);
576             UiObject2 text = findSiblingTextObject(toggle);
577             if (text != null) {
578                 sToggleLabel = text.getText();
579                 clickAndWaitForNewWindow(text);
580                 return;
581             }
582             clickAndWaitForNewWindow(toggle);
583             return;
584         }
585 
586         UiObject2 text = null;
587         for (int i = 0; i < uiObjects.size(); i++) {
588             UiObject2 toggle = uiObjects.get(i);
589             text = findSiblingTextObject(toggle);
590             if (text != null) {
591                 break;
592             }
593         }
594         if (text != null) {
595             sToggleLabel = text.getText();
596             clickAndWaitForNewWindow(text);
597         } else {
598             fail("Do NOT find the suitable toggle to grant permission!");
599         }
600     }
601 
602     /**
603      * Exit the grant permission settings and wait for it to disappear.
604      */
exitGrantPermissionSettings()605     public static void exitGrantPermissionSettings() {
606         pressBack();
607         waitForUiIdle();
608         if (sToggleLabel != null) {
609             // wait for exiting the grant permission settings
610             waitUntilObjectGone(By.text(sToggleLabel));
611         }
612     }
613 
614     /**
615      * Touch outside of the PackageInstaller dialog.
616      */
touchOutside()617     public static void touchOutside() {
618         DisplayMetrics displayMetrics = sContext.getResources().getDisplayMetrics();
619         sUiDevice.click(displayMetrics.widthPixels / 3, displayMetrics.heightPixels / 10);
620         waitForUiIdle();
621     }
622 
waitForInstallingDialogGone()623     private static void waitForInstallingDialogGone() {
624         BySelector installingSelector =
625                 getPackageInstallerBySelector(By.textContains(INSTALLING_LABEL));
626         UiObject2 installing = sUiDevice.findObject(installingSelector);
627         if (installing != null) {
628             waitUntilObjectGone(installingSelector);
629         }
630     }
631 
632     @Nullable
findSiblingTextObject(@onNull UiObject2 uiObject)633     private static UiObject2 findSiblingTextObject(@NonNull UiObject2 uiObject) {
634         UiObject2 parent = uiObject.getParent();
635         if (parent == null) {
636             return null;
637         }
638 
639         // If the child count is 1, it means the parent object only has the uiObject.
640         // Try to find the parent's parent that has more than two children.
641         while (parent.getChildCount() <= 1) {
642             parent = parent.getParent();
643             if (parent == null) {
644                 return null;
645             }
646         }
647 
648         // Find all TextViews to match the label
649         final List<UiObject2> uiObjects = parent.findObjects(By.clazz(TEXTVIEW_WIDGET_CLASSNAME));
650         Log.d(TAG, "The count of findSiblingTextObject objects is " + uiObjects.size());
651         for (int i = 0; i < uiObjects.size(); i++) {
652             UiObject2 uiObject2 = uiObjects.get(i);
653             if (uiObject2 != null) {
654                 logUiObject(uiObject2);
655                 if (uiObject2.getText() != null) {
656                     String label = uiObject2.getText().toLowerCase(Locale.ROOT);
657                     if (label.contains(TOGGLE_ALLOW_FROM_LABEL)
658                             || label.contains(TOGGLE_ALLOW_PERMISSION_LABEL)
659                             || label.contains(TOGGLE_INSTALL_UNKNOWN_APPS_LABEL)
660                             || label.contains(TOGGLE_ALLOW_LABEL)) {
661                         return uiObject2;
662                     }
663                 }
664             }
665         }
666         return null;
667     }
668 
logUiObject(@onNull UiObject2 uiObject)669     private static void logUiObject(@NonNull UiObject2 uiObject) {
670         Log.d(TAG, "Found bounds: " + uiObject.getVisibleBounds()
671                 + " of object: " + uiObject + ", text: " + uiObject.getText()
672                 + ", package: " + uiObject.getApplicationPackage() + ", className: "
673                 + uiObject.getClassName());
674     }
675 
getPackageInstallerBySelector(BySelector bySelector)676     private static BySelector getPackageInstallerBySelector(BySelector bySelector) {
677         return bySelector.pkg(sPackageInstallerPackageName);
678     }
679 
findPackageInstallerObject(String name)680     private static UiObject2 findPackageInstallerObject(String name) {
681         final Pattern namePattern = Pattern.compile(name, Pattern.CASE_INSENSITIVE);
682         return findPackageInstallerObject(By.text(namePattern), /* checkNull= */ true);
683     }
684 
findPackageInstallerObject(BySelector bySelector, boolean checkNull)685     private static UiObject2 findPackageInstallerObject(BySelector bySelector, boolean checkNull) {
686         return findObject(getPackageInstallerBySelector(bySelector), checkNull);
687     }
688 
findObject(String name)689     private static UiObject2 findObject(String name) {
690         final Pattern namePattern = Pattern.compile(name, Pattern.CASE_INSENSITIVE);
691         return findObject(By.text(namePattern), /* checkNull= */ true);
692     }
693 
694     @Nullable
findObject(BySelector bySelector, boolean checkNull)695     private static UiObject2 findObject(BySelector bySelector, boolean checkNull) {
696         return findObject(bySelector, checkNull, FIND_OBJECT_TIMEOUT_MS);
697     }
698 
699     @Nullable
findObject(BySelector bySelector, boolean checkNull, long timeoutMs)700     private static UiObject2 findObject(BySelector bySelector, boolean checkNull, long timeoutMs) {
701         waitForUiIdle();
702 
703         UiObject2 object = null;
704         long startTime = System.currentTimeMillis();
705         while (startTime + timeoutMs > System.currentTimeMillis()) {
706             try {
707                 object = sUiDevice.wait(Until.findObject(bySelector), /* timeout= */ 10 * 1000);
708                 if (object != null) {
709                     Log.d(TAG, "Found bounds: " + object.getVisibleBounds()
710                             + " of object: " + bySelector + ", text: " + object.getText()
711                             + " package: " + object.getApplicationPackage());
712                     return object;
713                 } else {
714                     // Maybe the screen is small. Scroll forward and attempt to click
715                     new UiScrollable(new UiSelector().scrollable(true)).scrollForward();
716                 }
717             } catch (Exception ignored) {
718                 // do nothing
719             }
720         }
721         if (checkNull) {
722             assertWithMessage("Can't find object " + bySelector).that(object).isNotNull();
723         }
724         return object;
725     }
726 
waitUntilObjectGone(BySelector bySelector)727     private static void waitUntilObjectGone(BySelector bySelector) {
728         if (!sUiDevice.wait(Until.gone(bySelector), WAIT_OBJECT_GONE_TIMEOUT_MS)) {
729             fail("The Object: " + bySelector + "did not disappear within "
730                     + WAIT_OBJECT_GONE_TIMEOUT_MS + " milliseconds");
731         }
732         waitForUiIdle();
733     }
734 
uninstallPackage(String packageName)735     private static void uninstallPackage(String packageName) {
736         SystemUtil.runShellCommand(String.format("pm uninstall %s", packageName));
737     }
738 
uninstallTestPackage()739     private static void uninstallTestPackage() {
740         uninstallPackage(TEST_APK_PACKAGE_NAME);
741     }
742 
uninstallInstallerPackage()743     private static void uninstallInstallerPackage() {
744         uninstallPackage(TEST_INSTALLER_PACKAGE_NAME);
745     }
746 
747     /**
748      * Install the test apk.
749      */
installTestPackage()750     public static void installTestPackage() throws IOException {
751         installPackage(TEST_APK_NAME);
752     }
753 
installPackage(@onNull String apkName)754     private static void installPackage(@NonNull String apkName) throws IOException {
755         Log.d(TAG, "installPackage(): apkName= " + apkName);
756         SystemUtil.runShellCommand("pm install -t "
757                 + new File(TEST_APK_LOCATION, apkName).getCanonicalPath());
758     }
759 
isTestPackageInstalled()760     private static boolean isTestPackageInstalled() {
761         return isInstalled(TEST_APK_PACKAGE_NAME);
762     }
763 
isInstallerInstalled()764     private static boolean isInstallerInstalled() {
765         return isInstalled(TEST_INSTALLER_PACKAGE_NAME);
766     }
767 
isInstalled(@onNull String packageName)768     private static boolean isInstalled(@NonNull String packageName) {
769         Log.d(TAG, "Testing if package " + packageName + " is installed for user "
770                 + sContext.getUser());
771         try {
772             sPackageManager.getPackageInfo(packageName, /* flags= */ 0);
773             return true;
774         } catch (PackageManager.NameNotFoundException e) {
775             Log.v(TAG, "Package " + packageName + " not installed for user "
776                     + sContext.getUser() + ": " + e);
777             return false;
778         }
779     }
780 
isTestPackageLabelV2Installed()781     private static boolean isTestPackageLabelV2Installed() {
782         return isInstalledAndVerifyAppName(TEST_APK_PACKAGE_NAME, TEST_APK_V2_LABEL);
783     }
784 
isInstalledAndVerifyAppName(@onNull String packageName, @NonNull String expectedAppLabel)785     private static boolean isInstalledAndVerifyAppName(@NonNull String packageName,
786             @NonNull String expectedAppLabel) {
787         Log.d(TAG, "Testing if package " + packageName + " is installed for user "
788                 + sContext.getUser() + ", with app label " + expectedAppLabel);
789         try {
790             PackageInfo packageInfo = sPackageManager.getPackageInfo(
791                     packageName, /* flags= */ 0);
792             CharSequence appLabel = packageInfo.applicationInfo.loadLabel(sPackageManager);
793             return TextUtils.equals(appLabel, expectedAppLabel);
794         } catch (PackageManager.NameNotFoundException e) {
795             Log.v(TAG, "Package " + packageName + " not installed for user "
796                     + sContext.getUser() + ": " + e);
797             return false;
798         }
799     }
800 
801     private static class InstallerResponseReceiver extends BroadcastReceiver {
802         private CompletableFuture<Integer> mInstallerResponseResult = new CompletableFuture<>();
803 
804         @Override
onReceive(Context context, Intent intent)805         public void onReceive(Context context, Intent intent) {
806             final int status = intent.getIntExtra(EXTRA_STATUS, -1);
807             Log.i(TAG, "InstallerResponseReceiver received status: " + status);
808             mInstallerResponseResult.complete(status);
809         }
810 
unregisterReceiver(Context context)811         public void unregisterReceiver(Context context) {
812             context.unregisterReceiver(this);
813         }
getInstallerResponseResult()814         public int getInstallerResponseResult() throws Exception {
815             return mInstallerResponseResult.get(10, TimeUnit.SECONDS);
816         }
817 
resetResult()818         public void resetResult() {
819             mInstallerResponseResult = new CompletableFuture();
820         }
821     }
822 
823     @Nullable
getPackageInstallerPackageName()824     private static String getPackageInstallerPackageName() {
825         final Intent intent = new Intent(
826                 Intent.ACTION_INSTALL_PACKAGE).setData(Uri.parse("content:"));
827         final ResolveInfo ri = sPackageManager.resolveActivity(intent, /* flags= */ 0);
828         return ri != null ? ri.activityInfo.packageName : null;
829     }
830 
isNotSupportedDevice()831     private static boolean isNotSupportedDevice() {
832         return FeatureUtil.isArc()
833                 || FeatureUtil.isAutomotive()
834                 || FeatureUtil.isTV()
835                 || FeatureUtil.isWatch()
836                 || FeatureUtil.isVrHeadset();
837     }
838 }
839