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