1 /*
<lambda>null2  * 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 android.hibernation.cts
18 
19 import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_TOP_SLEEPING
20 import android.app.Instrumentation
21 import android.content.Context
22 import android.content.Intent
23 import android.content.Intent.ACTION_AUTO_REVOKE_PERMISSIONS
24 import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK
25 import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
26 import android.content.pm.PackageManager
27 import android.content.pm.PackageManager.PERMISSION_DENIED
28 import android.content.pm.PackageManager.PERMISSION_GRANTED
29 import android.content.res.Resources
30 import android.net.Uri
31 import android.os.Build
32 import android.os.Process
33 import android.os.UserHandle
34 import android.platform.test.annotations.AppModeFull
35 import android.provider.DeviceConfig
36 import android.safetycenter.SafetyCenterIssue
37 import android.safetycenter.SafetyCenterManager
38 import android.view.accessibility.AccessibilityNodeInfo
39 import androidx.test.InstrumentationRegistry
40 import androidx.test.filters.FlakyTest
41 import androidx.test.filters.SdkSuppress
42 import androidx.test.runner.AndroidJUnit4
43 import androidx.test.uiautomator.By
44 import androidx.test.uiautomator.BySelector
45 import androidx.test.uiautomator.UiObject2
46 import androidx.test.uiautomator.UiObjectNotFoundException
47 import com.android.compatibility.common.util.ApiTest
48 import com.android.compatibility.common.util.CddTest
49 import com.android.compatibility.common.util.DeviceConfigStateChangerRule
50 import com.android.compatibility.common.util.DisableAnimationRule
51 import com.android.compatibility.common.util.FreezeRotationRule
52 import com.android.compatibility.common.util.MatcherUtils.hasTextThat
53 import com.android.compatibility.common.util.SystemUtil
54 import com.android.compatibility.common.util.SystemUtil.callWithShellPermissionIdentity
55 import com.android.compatibility.common.util.SystemUtil.eventually
56 import com.android.compatibility.common.util.SystemUtil.getEventually
57 import com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow
58 import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
59 import com.android.compatibility.common.util.ThrowingSupplier
60 import com.android.compatibility.common.util.UI_ROOT
61 import com.android.compatibility.common.util.click
62 import com.android.compatibility.common.util.depthFirstSearch
63 import com.android.compatibility.common.util.uiDump
64 import com.android.modules.utils.build.SdkLevel
65 import com.android.safetycenter.internaldata.SafetyCenterIds
66 import com.android.safetycenter.internaldata.SafetyCenterIssueId
67 import com.android.safetycenter.internaldata.SafetyCenterIssueKey
68 import java.lang.reflect.Modifier
69 import java.util.concurrent.TimeUnit
70 import java.util.concurrent.atomic.AtomicReference
71 import java.util.regex.Pattern
72 import org.hamcrest.CoreMatchers.containsString
73 import org.hamcrest.CoreMatchers.containsStringIgnoringCase
74 import org.hamcrest.CoreMatchers.equalTo
75 import org.hamcrest.Matcher
76 import org.hamcrest.Matchers.greaterThan
77 import org.junit.After
78 import org.junit.Assert.assertEquals
79 import org.junit.Assert.assertFalse
80 import org.junit.Assert.assertThat
81 import org.junit.Assert.assertTrue
82 import org.junit.Assume.assumeFalse
83 import org.junit.Assume.assumeTrue
84 import org.junit.Before
85 import org.junit.BeforeClass
86 import org.junit.Ignore
87 import org.junit.Rule
88 import org.junit.Test
89 import org.junit.runner.RunWith
90 
91 /**
92  * Test for auto revoke
93  */
94 @RunWith(AndroidJUnit4::class)
95 @CddTest(requirements = ["3.5.2"])
96 class AutoRevokeTest {
97     private val context: Context = InstrumentationRegistry.getTargetContext()
98     private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
99 
100     private val mPermissionControllerResources: Resources = context.createPackageContext(
101             context.packageManager.permissionControllerPackageName, 0).resources
102 
103     private lateinit var supportedApkPath: String
104     private lateinit var supportedAppPackageName: String
105     private lateinit var preMinVersionApkPath: String
106     private lateinit var preMinVersionAppPackageName: String
107 
108     @Rule
109     @JvmField
110     val storeExactTimeRule = DeviceConfigStateChangerRule(context,
111         DeviceConfig.NAMESPACE_PERMISSIONS, STORE_EXACT_TIME_KEY, "true")
112 
113     companion object {
114         const val LOG_TAG = "AutoRevokeTest"
115         private const val STORE_EXACT_TIME_KEY = "permission_changes_store_exact_time"
116         private const val UNUSED_APPS_SOURCE_ID = "AndroidPermissionAutoRevoke"
117         private const val UNUSED_APPS_ISSUE_ID = "unused_apps_issue"
118         private const val PERMISSION_USER_SENSITIVE_FLAG =
119                 PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED or
120                 PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_DENIED
121 
122         private const val READ_CALENDAR = "android.permission.READ_CALENDAR"
123         private const val BLUETOOTH_CONNECT = "android.permission.BLUETOOTH_CONNECT"
124 
125         @JvmStatic
126         @BeforeClass
127         fun beforeAllTests() {
128             runBootCompleteReceiver(InstrumentationRegistry.getTargetContext(), LOG_TAG)
129         }
130     }
131 
132     @get:Rule
133     val disableAnimationRule = DisableAnimationRule()
134 
135     @get:Rule
136     val freezeRotationRule = FreezeRotationRule()
137 
138     @Before
139     fun setup() {
140         // Collapse notifications
141         assertThat(
142                 runShellCommandOrThrow("cmd statusbar collapse"),
143                 equalTo(""))
144         clearNotifications()
145         // Wake up the device
146         runShellCommandOrThrow("input keyevent KEYCODE_WAKEUP")
147         if ("false".equals(runShellCommandOrThrow("cmd lock_settings get-disabled"))) {
148             // Unlock screen only when it's lock settings enabled to prevent showing "wallpaper
149             // picker" which may cover another UI elements on freeform window configuration.
150             runShellCommandOrThrow("input keyevent 82")
151         }
152         runShellCommandOrThrow("am broadcast -a android.intent.action.CLOSE_SYSTEM_DIALOGS")
153         resetJob(context)
154 
155         if (isAutomotiveDevice()) {
156             supportedApkPath = APK_PATH_S_APP
157             supportedAppPackageName = APK_PACKAGE_NAME_S_APP
158             preMinVersionApkPath = APK_PATH_R_APP
159             preMinVersionAppPackageName = APK_PACKAGE_NAME_R_APP
160         } else {
161             supportedApkPath = APK_PATH_R_APP
162             supportedAppPackageName = APK_PACKAGE_NAME_R_APP
163             preMinVersionApkPath = APK_PATH_Q_APP
164             preMinVersionAppPackageName = APK_PACKAGE_NAME_Q_APP
165         }
166     }
167 
168     @After
169     fun cleanUp() {
170         goHome()
171     }
172 
173     @AppModeFull(reason = "Uses separate apps for testing")
174     @Test
175     @FlakyTest(bugId = 293171897)
176     @CddTest(requirement = "3.5.2/C-1-2")
177     fun testUnusedApp_getsPermissionRevoked() {
178         assumeFalse(
179                 "Watch doesn't provide a unified way to check notifications. it depends on UX",
180                 hasFeatureWatch())
181         withUnusedThresholdMs(3L) {
182             withDummyApp {
183                 // Setup
184                 setupApp()
185                 Thread.sleep(5) // wait longer than the unused threshold
186 
187                 // Run
188                 runAppHibernationJob(context, LOG_TAG)
189 
190                 // Verify
191                 assertPermission(PERMISSION_DENIED)
192 
193                 if (hasFeatureTV()) {
194                     // Skip checking unused apps screen because it may be unavailable on TV
195                     return
196                 }
197                 openUnusedAppsNotification()
198 
199                 waitFindObject(By.text(supportedAppPackageName))
200                 waitFindObject(By.text("Calendar permission removed"))
201                 goBack()
202             }
203         }
204     }
205 
206     @AppModeFull(reason = "Uses separate apps for testing")
207     @FlakyTest(bugId = 293171897)
208     @Test
209     @CddTest(requirement = "3.5.1/C-1-1")
210     // TODO(b/238677038): In addition to flaking in general, this test also fails on R base image
211     fun testUnusedApp_uninstallApp() {
212         assumeFalse(
213             "Unused apps screen may be unavailable on TV",
214             hasFeatureTV())
215         withUnusedThresholdMs(3L) {
216             withDummyAppNoUninstallAssertion {
217                 // Setup
218                 setupApp()
219                 Thread.sleep(5) // wait longer than the unused threshold
220 
221                 // Run
222                 runAppHibernationJob(context, LOG_TAG)
223 
224                 // Verify
225                 openUnusedAppsNotification()
226                 waitFindObject(By.text(supportedAppPackageName))
227 
228                 assertTrue(isPackageInstalled(supportedAppPackageName))
229                 clickUninstallIcon()
230                 clickUninstallOk()
231 
232                 eventually {
233                     assertFalse(isPackageInstalled(supportedAppPackageName))
234                 }
235 
236                 goBack()
237             }
238         }
239     }
240 
241     @AppModeFull(reason = "Uses separate apps for testing")
242     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
243     @Test
244     fun testUnusedApp_doesntGetSplitPermissionRevoked() {
245         assumeFalse(
246             "Auto doesn't support hibernation for pre-S apps",
247             isAutomotiveDevice())
248         withUnusedThresholdMs(3L) {
249             withDummyApp(APK_PATH_R_APP, APK_PACKAGE_NAME_R_APP) {
250                 // Setup
251                 startApp(APK_PACKAGE_NAME_R_APP)
252                 assertPermission(PERMISSION_GRANTED, APK_PACKAGE_NAME_R_APP, BLUETOOTH_CONNECT)
253                 killDummyApp(APK_PACKAGE_NAME_R_APP)
254                 Thread.sleep(500)
255 
256                 // Run
257                 runAppHibernationJob(context, LOG_TAG)
258 
259                 // Verify
260                 assertPermission(PERMISSION_GRANTED, APK_PACKAGE_NAME_R_APP, BLUETOOTH_CONNECT)
261             }
262         }
263     }
264 
265     @AppModeFull(reason = "Uses separate apps for testing")
266     @CddTest(requirement = "3.5.2/C-1-2")
267     @Test
268     fun testUsedApp_doesntGetPermissionRevoked() {
269         withUnusedThresholdMs(100_000L) {
270             withDummyApp {
271                 // Setup
272                 setupApp()
273                 Thread.sleep(5)
274 
275                 // Run
276                 runAppHibernationJob(context, LOG_TAG)
277                 Thread.sleep(1000)
278 
279                 // Verify
280                 assertPermission(PERMISSION_GRANTED)
281             }
282         }
283     }
284 
285     @AppModeFull(reason = "Uses separate apps for testing")
286     @FlakyTest(bugId = 293171897)
287     @Test
288     fun testAppWithPermissionsChangedRecently_doesNotGetPermissionRevoked() {
289         val unusedThreshold = 15_000L
290         withUnusedThresholdMs(unusedThreshold) {
291             withDummyApp {
292                 // Setup
293                 // Ensure app is considered unused and then change permission
294                 Thread.sleep(unusedThreshold)
295                 goToPermissions()
296                 click("Calendar")
297                 click("Allow")
298                 goBack()
299                 goBack()
300                 goBack()
301 
302                 // Run
303                 runAppHibernationJob(context, LOG_TAG)
304 
305                 // Verify that permission is not revoked because the permission was changed
306                 // within the unused threshold even though the app itself is unused
307                 assertPermission(PERMISSION_GRANTED)
308             }
309         }
310     }
311 
312     @AppModeFull(reason = "Uses separate apps for testing")
313     @FlakyTest(bugId = 293171897)
314     @Test
315     fun testPermissionEventCleanupService_scrubsEvents() {
316         val unusedThreshold = 15_000L
317         withUnusedThresholdMs(unusedThreshold) {
318             withDummyApp {
319                 // Setup
320                 // Ensure app is considered unused
321                 Thread.sleep(unusedThreshold)
322                 goToPermissions()
323                 click("Calendar")
324                 click("Allow")
325                 goBack()
326                 goBack()
327                 goBack()
328 
329                 // Run with threshold where events would be cleaned up
330                 withUnusedThresholdMs(0) {
331                     runPermissionEventCleanupJob(context)
332                     Thread.sleep(3000L)
333                 }
334 
335                 runAppHibernationJob(context, LOG_TAG)
336 
337                 // Verify that permission is revoked because there are no recent permission changes
338                 assertPermission(PERMISSION_DENIED)
339             }
340         }
341     }
342 
343     @AppModeFull(reason = "Uses separate apps for testing")
344     @Test
345     fun testPreMinAutoRevokeVersionUnusedApp_doesntGetPermissionRevoked() {
346         assumeFalse(isHibernationEnabledForPreSApps())
347         withUnusedThresholdMs(3L) {
348             withDummyApp(preMinVersionApkPath, preMinVersionAppPackageName) {
349                 grantPermission(preMinVersionAppPackageName)
350                 assertPermission(PERMISSION_GRANTED, preMinVersionAppPackageName)
351                 startApp(preMinVersionAppPackageName)
352                 killDummyApp(preMinVersionAppPackageName)
353                 Thread.sleep(20)
354 
355                 // Run
356                 runAppHibernationJob(context, LOG_TAG)
357                 Thread.sleep(500)
358 
359                 // Verify
360                 assertPermission(PERMISSION_GRANTED, preMinVersionAppPackageName)
361             }
362         }
363     }
364 
365     @AppModeFull(reason = "Uses separate apps for testing")
366     @CddTest(requirements = ["3.5.1/C-1-2,C-1-4"])
367     @Test
368     fun testAutoRevoke_userAllowlisting() {
369         assumeFalse(context.packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE))
370         withUnusedThresholdMs(4L) {
371             withDummyApp {
372                 // Setup
373                 val pm = context.packageManager
374                 grantPermission()
375                 assertPermission(PERMISSION_GRANTED)
376                 runWithShellPermissionIdentity {
377                     assertFalse(pm.isAutoRevokeWhitelisted(supportedAppPackageName))
378                 }
379 
380                 // Verify
381                 goToPermissions()
382                 val autoRevokeEnabledToggle = getAllowlistToggle()
383                 assertTrue(autoRevokeEnabledToggle.isChecked())
384 
385                 // Grant allowlist
386                 autoRevokeEnabledToggle.click()
387                 eventually {
388                     assertFalse(getAllowlistToggle().isChecked())
389                 }
390 
391                 // Run
392                 goBack()
393                 goBack()
394                 goBack()
395                 runAppHibernationJob(context, LOG_TAG)
396                 Thread.sleep(500L)
397 
398                 // Verify
399                 runWithShellPermissionIdentity {
400                     assertTrue(pm.isAutoRevokeWhitelisted(supportedAppPackageName))
401                 }
402                 assertPermission(PERMISSION_GRANTED)
403             }
404         }
405     }
406 
407     @AppModeFull(reason = "Uses separate apps for testing")
408     @Test
409     fun testInstallGrants_notRevokedImmediately() {
410         withUnusedThresholdMs(TimeUnit.DAYS.toMillis(30)) {
411             withDummyApp {
412                 // Setup
413                 grantPermission()
414                 assertPermission(PERMISSION_GRANTED)
415 
416                 // Run
417                 runAppHibernationJob(context, LOG_TAG)
418                 Thread.sleep(500)
419 
420                 // Verify
421                 assertPermission(PERMISSION_GRANTED)
422             }
423         }
424     }
425 
426     @AppModeFull(reason = "Uses separate apps for testing")
427     @ApiTest(apis = ["android.content.pm.PackageManager#isAutoRevokeWhitelisted",
428         "android.content.pm.PackageManager#setAutoRevokeWhitelisted"])
429     @Test
430     fun testAutoRevoke_allowlistingApis() {
431         withDummyApp {
432             val pm = context.packageManager
433             runWithShellPermissionIdentity {
434                 assertFalse(pm.isAutoRevokeWhitelisted(supportedAppPackageName))
435             }
436 
437             runWithShellPermissionIdentity {
438                 assertTrue(pm.setAutoRevokeWhitelisted(supportedAppPackageName, true))
439             }
440             eventually {
441                 runWithShellPermissionIdentity {
442                     assertTrue(pm.isAutoRevokeWhitelisted(supportedAppPackageName))
443                 }
444             }
445 
446             runWithShellPermissionIdentity {
447                 assertTrue(pm.setAutoRevokeWhitelisted(supportedAppPackageName, false))
448             }
449             eventually {
450                 runWithShellPermissionIdentity {
451                     assertFalse(pm.isAutoRevokeWhitelisted(supportedAppPackageName))
452                 }
453             }
454         }
455     }
456 
457     @AppModeFull(reason = "Uses separate apps for testing")
458     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu")
459     @FlakyTest(bugId = 293171897)
460     @Test
461     fun testAutoRevoke_showsUpInSafetyCenter() {
462         assumeTrue(deviceSupportsSafetyCenter())
463         withSafetyCenterEnabled {
464             withUnusedThresholdMs(3L) {
465                 withDummyApp {
466                     setupApp()
467 
468                     // Run
469                     runAppHibernationJob(context, LOG_TAG)
470 
471                     // Verify
472                     val safetyCenterManager =
473                         context.getSystemService(SafetyCenterManager::class.java)!!
474                     eventually {
475                         val issues = ArrayList<SafetyCenterIssue>()
476                         runWithShellPermissionIdentity {
477                             val safetyCenterData = safetyCenterManager!!.safetyCenterData
478                             issues.addAll(safetyCenterData.issues)
479                         }
480                         val issueId = SafetyCenterIds.encodeToString(
481                                 SafetyCenterIssueId.newBuilder()
482                                         .setSafetyCenterIssueKey(SafetyCenterIssueKey.newBuilder()
483                                                 .setSafetySourceId(UNUSED_APPS_SOURCE_ID)
484                                                 .setSafetySourceIssueId(UNUSED_APPS_ISSUE_ID)
485                                                 .setUserId(UserHandle.myUserId())
486                                                 .build())
487                                         .setIssueTypeId(UNUSED_APPS_ISSUE_ID)
488                                         .build())
489                         assertTrue(issues.any { it.id == issueId })
490                     }
491                 }
492             }
493         }
494     }
495 
496     @AppModeFull(reason = "Uses separate apps for testing")
497     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu")
498     @Test
499     @Ignore
500     fun testAutoRevoke_goToUnusedAppsPage_removesSafetyCenterIssue() {
501         assumeTrue(deviceSupportsSafetyCenter())
502         withSafetyCenterEnabled {
503             withUnusedThresholdMs(3L) {
504                 withDummyApp {
505                     setupApp()
506 
507                     // Run
508                     runAppHibernationJob(context, LOG_TAG)
509 
510                     // Go to unused apps page
511                     openUnusedAppsNotification()
512                     waitFindObject(By.text(supportedAppPackageName))
513 
514                     // Verify
515                     val safetyCenterManager =
516                         context.getSystemService(SafetyCenterManager::class.java)!!
517                     eventually {
518                         val issues = ArrayList<SafetyCenterIssue>()
519                         runWithShellPermissionIdentity {
520                             val safetyCenterData = safetyCenterManager!!.safetyCenterData
521                             issues.addAll(safetyCenterData.issues)
522                         }
523                         val issueId = SafetyCenterIds.encodeToString(
524                                 SafetyCenterIssueId.newBuilder()
525                                         .setSafetyCenterIssueKey(SafetyCenterIssueKey.newBuilder()
526                                                 .setSafetySourceId(UNUSED_APPS_SOURCE_ID)
527                                                 .setSafetySourceIssueId(UNUSED_APPS_ISSUE_ID)
528                                                 .setUserId(UserHandle.myUserId())
529                                                 .build())
530                                         .setIssueTypeId(UNUSED_APPS_ISSUE_ID)
531                                         .build())
532                         assertFalse(issues.any { it.id == issueId })
533                     }
534                 }
535             }
536         }
537     }
538 
539     private fun isHibernationEnabledForPreSApps(): Boolean {
540         return runWithShellPermissionIdentity(
541             ThrowingSupplier {
542                 DeviceConfig.getBoolean(
543                     DeviceConfig.NAMESPACE_APP_HIBERNATION,
544                     "app_hibernation_targets_pre_s_apps",
545                     false
546                 )
547             }
548         )
549     }
550 
551     private fun isAutomotiveDevice(): Boolean {
552         return context.packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)
553     }
554 
555     private fun deviceSupportsSafetyCenter(): Boolean {
556         return context.resources.getBoolean(
557             Resources.getSystem().getIdentifier("config_enableSafetyCenter", "bool", "android"))
558     }
559 
560     private fun installApp() {
561         installApk(supportedApkPath)
562     }
563 
564     private fun isPackageInstalled(packageName: String): Boolean {
565         val pm = context.packageManager
566 
567         return callWithShellPermissionIdentity {
568             try {
569                 pm.getPackageInfo(packageName, 0)
570                 true
571             } catch (e: PackageManager.NameNotFoundException) {
572                 false
573             }
574         }
575     }
576 
577     private fun uninstallApp() {
578         uninstallApp(supportedAppPackageName)
579     }
580 
581     /**
582      * Grants the calendar permission and then uses the app
583      */
584     private fun setupApp() {
585         grantPermission()
586         assertPermission(PERMISSION_GRANTED)
587         startApp()
588         killDummyApp()
589         SystemUtil.waitForBroadcasts()
590     }
591 
592     private fun startApp() {
593         startApp(supportedAppPackageName)
594     }
595 
596     private fun goBack() {
597         runShellCommandOrThrow("input keyevent KEYCODE_BACK")
598     }
599 
600     private fun killDummyApp(pkg: String = supportedAppPackageName) {
601         if (!SdkLevel.isAtLeastS()) {
602             // Work around a race condition on R that killing the app process too fast after
603             // activity launch would result in a stale process record in LRU process list that
604             // sticks until next reboot.
605             Thread.sleep(5000)
606         }
607         assertThat(
608                 runShellCommandOrThrow("am force-stop " + pkg),
609                 equalTo(""))
610         awaitAppState(pkg, greaterThan(IMPORTANCE_TOP_SLEEPING))
611     }
612 
613     private fun clickUninstallIcon() {
614         val rowSelector = By.text(supportedAppPackageName)
615 
616         val rowItem = if (isAutomotiveDevice()) {
617             val rowItemSelector = By
618                     .res(Pattern.compile(".*id/car_ui_first_action_container"))
619                     .hasDescendant(rowSelector)
620             waitFindObject(rowItemSelector).parent
621         } else if (hasFeatureWatch()) {
622             waitFindObject(rowSelector)
623         } else {
624             waitFindObject(rowSelector).parent.parent
625         }
626 
627         if (!hasFeatureWatch()) {
628             val uninstallSelector = if (isAutomotiveDevice()) {
629                 By.res(Pattern.compile(".*id/car_ui_secondary_action"))
630             } else {
631                 By.desc("Uninstall or disable")
632             }
633 
634             rowItem.findObject(uninstallSelector).click()
635         } else {
636             rowItem.click()
637             waitFindObject(By.text("Uninstall")).click()
638         }
639     }
640 
641     private fun clickUninstallOk() {
642         val uninstallSelector = if (hasFeatureWatch()) {
643                 By.desc("OK")
644             } else {
645                 By.text("OK")
646             }
647 
648         waitFindObject(uninstallSelector).click()
649     }
650 
651     private inline fun withDummyApp(
652         apk: String = supportedApkPath,
653         packageName: String = supportedAppPackageName,
654         action: () -> Unit
655     ) {
656         withApp(apk, packageName, action)
657     }
658 
659     private inline fun withDummyAppNoUninstallAssertion(
660         apk: String = supportedApkPath,
661         packageName: String = supportedAppPackageName,
662         action: () -> Unit
663     ) {
664         withAppNoUninstallAssertion(apk, packageName, action)
665     }
666 
667     private fun grantPermission(
668         packageName: String = supportedAppPackageName,
669         permission: String = READ_CALENDAR
670     ) {
671         instrumentation.uiAutomation.grantRuntimePermission(packageName, permission)
672         runWithShellPermissionIdentity {
673             context.packageManager.updatePermissionFlags(permission, packageName,
674                     PERMISSION_USER_SENSITIVE_FLAG, PERMISSION_USER_SENSITIVE_FLAG,
675                     Process.myUserHandle())
676         }
677     }
678 
679     private fun assertPermission(
680         state: Int,
681         packageName: String = supportedAppPackageName,
682         permission: String = READ_CALENDAR
683     ) {
684         assertPermission(packageName, permission, state)
685     }
686 
687     private fun goToPermissions(packageName: String = supportedAppPackageName) {
688         context.startActivity(Intent(ACTION_AUTO_REVOKE_PERMISSIONS)
689                 .setData(Uri.fromParts("package", packageName, null))
690                 .addFlags(FLAG_ACTIVITY_NEW_TASK)
691                 .addFlags(FLAG_ACTIVITY_CLEAR_TASK))
692 
693         waitForIdle()
694 
695         click("Permissions")
696     }
697 
698     private fun click(label: String) {
699         try {
700             waitFindObject(byTextIgnoreCase(label)).click()
701         } catch (e: UiObjectNotFoundException) {
702             // waitFindObject sometimes fails to find UI that is present in the view hierarchy
703             // Increasing sleep to 2000 in waitForIdle() might be passed but no guarantee that the
704             // UI is fully displayed So Adding one more check without using the UiAutomator helps
705             // reduce false positives
706             waitFindNode(hasTextThat(containsStringIgnoringCase(label))).click()
707         }
708         waitForIdle()
709     }
710 
711     private fun assertAllowlistState(state: Boolean) {
712         assertThat(
713             waitFindObject(By.textStartsWith("Auto-revoke allowlisted: ")).text,
714             containsString(state.toString()))
715     }
716 
717     private fun getAllowlistToggle(): UiObject2 {
718         waitForIdle()
719         // Wear: per b/253990371, unused_apps_summary string is not available,
720         // so look for unused_apps_label_v2 string instead.
721         val autoRevokeText = if (hasFeatureWatch()) {
722             "Pause app"
723         } else {
724             "Remove permissions"
725         }
726 
727         if (hasFeatureWatch()) {
728             return waitFindObject(
729                     By.checkable(true).hasDescendant(By.textStartsWith(autoRevokeText)))
730         }
731 
732         val parent = waitFindObject(
733             By.clickable(true)
734                 .hasDescendant(By.textStartsWith(autoRevokeText))
735                 .hasDescendant(By.checkable(true))
736         )
737         return parent.findObject(By.checkable(true))
738     }
739 
740     private fun waitForIdle() {
741         instrumentation.uiAutomation.waitForIdle(1000, 10000)
742         Thread.sleep(500)
743         instrumentation.uiAutomation.waitForIdle(1000, 10000)
744     }
745 
746     private inline fun <T> eventually(crossinline action: () -> T): T {
747         val res = AtomicReference<T>()
748         SystemUtil.eventually {
749             res.set(action())
750         }
751         return res.get()
752     }
753 
754     private fun waitFindObject(selector: BySelector): UiObject2 {
755         return waitFindObject(instrumentation.uiAutomation, selector)
756     }
757 }
758 
permissionStateToStringnull759 private fun permissionStateToString(state: Int): String {
760     return constToString<PackageManager>("PERMISSION_", state)
761 }
762 
763 /**
764  * For some reason waitFindObject sometimes fails to find UI that is present in the view hierarchy
765  */
waitFindNodenull766 fun waitFindNode(
767     matcher: Matcher<AccessibilityNodeInfo>,
768     failMsg: String? = null,
769     timeoutMs: Long = 10_000
770 ): AccessibilityNodeInfo {
771     return getEventually({
772         val ui = UI_ROOT
773         ui.depthFirstSearch { node ->
774             matcher.matches(node)
775         }.assertNotNull {
776             buildString {
777                 if (failMsg != null) {
778                     appendLine(failMsg)
779                 }
780                 appendLine("No view found matching $matcher:\n\n${uiDump(ui)}")
781             }
782         }
783     }, timeoutMs)
784 }
785 
byTextIgnoreCasenull786 fun byTextIgnoreCase(txt: String): BySelector {
787     return By.text(Pattern.compile(txt, Pattern.CASE_INSENSITIVE))
788 }
789 
waitForIdlenull790 fun waitForIdle() {
791     InstrumentationRegistry.getInstrumentation().uiAutomation.waitForIdle(1000, 10000)
792 }
793 
uninstallAppnull794 fun uninstallApp(packageName: String) {
795     assertThat(runShellCommandOrThrow("pm uninstall $packageName"), containsString("Success"))
796 }
797 
uninstallAppWithoutAssertionnull798 fun uninstallAppWithoutAssertion(packageName: String) {
799     runShellCommandOrThrow("pm uninstall $packageName")
800 }
801 
installApknull802 fun installApk(apk: String) {
803     assertThat(runShellCommandOrThrow("pm install -r $apk"), containsString("Success"))
804 }
805 
assertPermissionnull806 fun assertPermission(packageName: String, permissionName: String, state: Int) {
807     assertThat(permissionName, containsString("permission."))
808     eventually {
809         runWithShellPermissionIdentity {
810             assertEquals(
811                     permissionStateToString(state),
812                     permissionStateToString(
813                             InstrumentationRegistry.getTargetContext()
814                                     .packageManager
815                                     .checkPermission(permissionName, packageName)))
816         }
817     }
818 }
819 
constToStringnull820 inline fun <reified T> constToString(prefix: String, value: Int): String {
821     return T::class.java.declaredFields.filter {
822         Modifier.isStatic(it.modifiers) && it.name.startsWith(prefix)
823     }.map {
824         it.isAccessible = true
825         it.name to it.get(null)
826     }.find { (k, v) ->
827         v == value
828     }.assertNotNull {
829         "None of ${T::class.java.simpleName}.$prefix* == $value"
830     }.first
831 }
832 
assertNotNullnull833 inline fun <T> T?.assertNotNull(errorMsg: () -> String): T {
834     return if (this == null) throw AssertionError(errorMsg()) else this
835 }
836