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.os.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_NEW_TASK
25 import android.content.pm.PackageManager
26 import android.content.pm.PackageManager.PERMISSION_DENIED
27 import android.content.pm.PackageManager.PERMISSION_GRANTED
28 import android.content.res.Resources
29 import android.provider.DeviceConfig
30 import android.net.Uri
31 import android.os.Build
32 import android.platform.test.annotations.AppModeFull
33 import android.support.test.uiautomator.By
34 import android.support.test.uiautomator.BySelector
35 import android.support.test.uiautomator.UiObject2
36 import android.support.test.uiautomator.UiObjectNotFoundException
37 import android.view.accessibility.AccessibilityNodeInfo
38 import android.widget.Switch
39 import androidx.test.InstrumentationRegistry
40 import androidx.test.filters.SdkSuppress
41 import androidx.test.runner.AndroidJUnit4
42 import com.android.compatibility.common.util.DisableAnimationRule
43 import com.android.compatibility.common.util.FreezeRotationRule
44 import com.android.compatibility.common.util.MatcherUtils.hasTextThat
45 import com.android.compatibility.common.util.SystemUtil
46 import com.android.compatibility.common.util.SystemUtil.callWithShellPermissionIdentity
47 import com.android.compatibility.common.util.SystemUtil.eventually
48 import com.android.compatibility.common.util.SystemUtil.getEventually
49 import com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow
50 import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
51 import com.android.compatibility.common.util.ThrowingSupplier
52 import com.android.compatibility.common.util.UI_ROOT
53 import com.android.compatibility.common.util.click
54 import com.android.compatibility.common.util.depthFirstSearch
55 import com.android.compatibility.common.util.uiDump
56 import com.android.modules.utils.build.SdkLevel
57 import org.hamcrest.CoreMatchers.containsString
58 import org.hamcrest.CoreMatchers.containsStringIgnoringCase
59 import org.hamcrest.CoreMatchers.equalTo
60 import org.hamcrest.Matcher
61 import org.hamcrest.Matchers.greaterThan
62 import org.junit.After
63 import org.junit.Assert.assertEquals
64 import org.junit.Assert.assertFalse
65 import org.junit.Assert.assertThat
66 import org.junit.Assert.assertTrue
67 import org.junit.Assume.assumeFalse
68 import org.junit.Before
69 import org.junit.BeforeClass
70 import org.junit.Ignore
71 import org.junit.Rule
72 import org.junit.Test
73 import org.junit.runner.RunWith
74 import java.lang.reflect.Modifier
75 import java.util.concurrent.TimeUnit
76 import java.util.concurrent.atomic.AtomicReference
77 import java.util.regex.Pattern
78
79 private const val READ_CALENDAR = "android.permission.READ_CALENDAR"
80 private const val BLUETOOTH_CONNECT = "android.permission.BLUETOOTH_CONNECT"
81
82 /**
83 * Test for auto revoke
84 */
85 @RunWith(AndroidJUnit4::class)
86 class AutoRevokeTest {
87
88 private val context: Context = InstrumentationRegistry.getTargetContext()
89 private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
90
91 private val mPermissionControllerResources: Resources = context.createPackageContext(
92 context.packageManager.permissionControllerPackageName, 0).resources
93
94 private lateinit var supportedApkPath: String
95 private lateinit var supportedAppPackageName: String
96 private lateinit var preMinVersionApkPath: String
97 private lateinit var preMinVersionAppPackageName: String
98
99 companion object {
100 const val LOG_TAG = "AutoRevokeTest"
101
102 @JvmStatic
103 @BeforeClass
104 fun beforeAllTests() {
105 runBootCompleteReceiver(InstrumentationRegistry.getTargetContext(), LOG_TAG)
106 }
107 }
108
109 @get:Rule
110 val disableAnimationRule = DisableAnimationRule()
111
112 @get:Rule
113 val freezeRotationRule = FreezeRotationRule()
114
115 @Before
116 fun setup() {
117 // Collapse notifications
118 assertThat(
119 runShellCommandOrThrow("cmd statusbar collapse"),
120 equalTo(""))
121
122 // Wake up the device
123 runShellCommandOrThrow("input keyevent KEYCODE_WAKEUP")
124 if ("false".equals(runShellCommandOrThrow("cmd lock_settings get-disabled"))) {
125 // Unlock screen only when it's lock settings enabled to prevent showing "wallpaper
126 // picker" which may cover another UI elements on freeform window configuration.
127 runShellCommandOrThrow("input keyevent 82")
128 }
129
130 if (isAutomotiveDevice()) {
131 supportedApkPath = APK_PATH_S_APP
132 supportedAppPackageName = APK_PACKAGE_NAME_S_APP
133 preMinVersionApkPath = APK_PATH_R_APP
134 preMinVersionAppPackageName = APK_PACKAGE_NAME_R_APP
135 } else {
136 supportedApkPath = APK_PATH_R_APP
137 supportedAppPackageName = APK_PACKAGE_NAME_R_APP
138 preMinVersionApkPath = APK_PATH_Q_APP
139 preMinVersionAppPackageName = APK_PACKAGE_NAME_Q_APP
140 }
141 }
142
143 @After
144 fun cleanUp() {
145 goHome()
146 }
147
148 @AppModeFull(reason = "Uses separate apps for testing")
149 @Test
150 @Ignore("b/201545116")
151 fun testUnusedApp_getsPermissionRevoked() {
152 assumeFalse(
153 "Watch doesn't provide a unified way to check notifications. it depends on UX",
154 hasFeatureWatch())
155 withUnusedThresholdMs(3L) {
156 withDummyApp {
157 // Setup
158 startAppAndAcceptPermission()
159 killDummyApp()
160 Thread.sleep(5) // wait longer than the unused threshold
161
162 // Run
163 runAppHibernationJob(context, LOG_TAG)
164
165 // Verify
166 assertPermission(PERMISSION_DENIED)
167
168 if (hasFeatureTV()) {
169 // Skip checking unused apps screen because it may be unavailable on TV
170 return
171 }
172 openUnusedAppsNotification()
173
174 waitFindObject(By.text(supportedAppPackageName))
175 waitFindObject(By.text("Calendar permission removed"))
176 goBack()
177 }
178 }
179 }
180
181 @AppModeFull(reason = "Uses separate apps for testing")
182 @Test
183 @Ignore("b/201545116")
184 fun testUnusedApp_uninstallApp() {
185 assumeFalse(
186 "Unused apps screen may be unavailable on TV",
187 hasFeatureTV())
188 withUnusedThresholdMs(3L) {
189 withDummyAppNoUninstallAssertion {
190 // Setup
191 startAppAndAcceptPermission()
192 killDummyApp()
193 Thread.sleep(5) // wait longer than the unused threshold
194
195 // Run
196 runAppHibernationJob(context, LOG_TAG)
197
198 // Verify
199 openUnusedAppsNotification()
200 waitFindObject(By.text(supportedAppPackageName))
201
202 assertTrue(isPackageInstalled(supportedAppPackageName))
203 clickUninstallIcon()
204 clickUninstallOk()
205
206 eventually {
207 assertFalse(isPackageInstalled(supportedAppPackageName))
208 }
209
210 goBack()
211 }
212 }
213 }
214
215 @AppModeFull(reason = "Uses separate apps for testing")
216 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
217 @Test
218 fun testUnusedApp_doesntGetSplitPermissionRevoked() {
219 assumeFalse(
220 "Auto doesn't support hibernation for pre-S apps",
221 isAutomotiveDevice())
222 withUnusedThresholdMs(3L) {
223 withDummyApp(APK_PATH_R_APP, APK_PACKAGE_NAME_R_APP) {
224 // Setup
225 startApp(APK_PACKAGE_NAME_R_APP)
226 assertPermission(PERMISSION_GRANTED, APK_PACKAGE_NAME_R_APP, BLUETOOTH_CONNECT)
227 killDummyApp(APK_PACKAGE_NAME_R_APP)
228 Thread.sleep(500)
229
230 // Run
231 runAppHibernationJob(context, LOG_TAG)
232
233 // Verify
234 assertPermission(PERMISSION_GRANTED, APK_PACKAGE_NAME_R_APP, BLUETOOTH_CONNECT)
235 }
236 }
237 }
238
239 @AppModeFull(reason = "Uses separate apps for testing")
240 @Test
241 fun testUsedApp_doesntGetPermissionRevoked() {
242 withUnusedThresholdMs(100_000L) {
243 withDummyApp {
244 // Setup
245 startApp()
246 clickPermissionAllow()
247 assertPermission(PERMISSION_GRANTED)
248 killDummyApp()
249 Thread.sleep(5)
250
251 // Run
252 runAppHibernationJob(context, LOG_TAG)
253 Thread.sleep(1000)
254
255 // Verify
256 assertPermission(PERMISSION_GRANTED)
257 }
258 }
259 }
260
261 @AppModeFull(reason = "Uses separate apps for testing")
262 @Test
263 fun testPreMinAutoRevokeVersionUnusedApp_doesntGetPermissionRevoked() {
264 assumeFalse(isHibernationEnabledForPreSApps())
265 withUnusedThresholdMs(3L) {
266 withDummyApp(preMinVersionApkPath, preMinVersionAppPackageName) {
267 withDummyApp {
268 startApp(preMinVersionAppPackageName)
269 clickPermissionAllow()
270 assertPermission(PERMISSION_GRANTED, preMinVersionAppPackageName)
271
272 killDummyApp(preMinVersionAppPackageName)
273
274 startApp()
275 clickPermissionAllow()
276 assertPermission(PERMISSION_GRANTED)
277
278 killDummyApp()
279 Thread.sleep(20)
280
281 // Run
282 runAppHibernationJob(context, LOG_TAG)
283 Thread.sleep(500)
284
285 // Verify
286 assertPermission(PERMISSION_DENIED)
287 assertPermission(PERMISSION_GRANTED, preMinVersionAppPackageName)
288 }
289 }
290 }
291 }
292
293 private fun isHibernationEnabledForPreSApps(): Boolean {
294 return runWithShellPermissionIdentity(
295 ThrowingSupplier {
296 DeviceConfig.getBoolean(
297 DeviceConfig.NAMESPACE_APP_HIBERNATION,
298 "app_hibernation_targets_pre_s_apps",
299 false
300 )
301 }
302 )
303 }
304
305 @AppModeFull(reason = "Uses separate apps for testing")
306 @Test
307 fun testAutoRevoke_userAllowlisting() {
308 assumeFalse(context.packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE))
309 withUnusedThresholdMs(4L) {
310 withDummyApp {
311 // Setup
312 startApp()
313 clickPermissionAllow()
314 assertAllowlistState(false)
315
316 // Verify
317 waitFindObject(byTextIgnoreCase("Request allowlist")).click()
318 waitFindObject(byTextIgnoreCase("Permissions")).click()
319 val autoRevokeEnabledToggle = getAllowlistToggle()
320 assertTrue(autoRevokeEnabledToggle.isChecked())
321
322 // Grant allowlist
323 autoRevokeEnabledToggle.click()
324 eventually {
325 assertFalse(getAllowlistToggle().isChecked())
326 }
327
328 // Run
329 goBack()
330 goBack()
331 goBack()
332 runAppHibernationJob(context, LOG_TAG)
333 Thread.sleep(500L)
334
335 // Verify
336 startApp()
337 assertAllowlistState(true)
338 assertPermission(PERMISSION_GRANTED)
339 }
340 }
341 }
342
343 @AppModeFull(reason = "Uses separate apps for testing")
344 @Test
345 fun testInstallGrants_notRevokedImmediately() {
346 withUnusedThresholdMs(TimeUnit.DAYS.toMillis(30)) {
347 withDummyApp {
348 // Setup
349 goToPermissions()
350 click("Calendar")
351 click("Allow")
352 goBack()
353 goBack()
354 goBack()
355
356 // Run
357 runAppHibernationJob(context, LOG_TAG)
358 Thread.sleep(500)
359
360 // Verify
361 assertPermission(PERMISSION_GRANTED)
362 }
363 }
364 }
365
366 @AppModeFull(reason = "Uses separate apps for testing")
367 @Test
368 fun testAutoRevoke_allowlistingApis() {
369 withDummyApp {
370 val pm = context.packageManager
371 runWithShellPermissionIdentity {
372 assertFalse(pm.isAutoRevokeWhitelisted(supportedAppPackageName))
373 }
374
375 runWithShellPermissionIdentity {
376 assertTrue(pm.setAutoRevokeWhitelisted(supportedAppPackageName, true))
377 }
378 eventually {
379 runWithShellPermissionIdentity {
380 assertTrue(pm.isAutoRevokeWhitelisted(supportedAppPackageName))
381 }
382 }
383
384 runWithShellPermissionIdentity {
385 assertTrue(pm.setAutoRevokeWhitelisted(supportedAppPackageName, false))
386 }
387 eventually {
388 runWithShellPermissionIdentity {
389 assertFalse(pm.isAutoRevokeWhitelisted(supportedAppPackageName))
390 }
391 }
392 }
393 }
394
395 private fun isAutomotiveDevice(): Boolean {
396 return context.packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)
397 }
398
399 private fun installApp() {
400 installApk(supportedApkPath)
401 }
402
403 private fun isPackageInstalled(packageName: String): Boolean {
404 val pm = context.packageManager
405
406 return callWithShellPermissionIdentity {
407 try {
408 pm.getPackageInfo(packageName, 0)
409 true
410 } catch (e: PackageManager.NameNotFoundException) {
411 false
412 }
413 }
414 }
415
416 private fun uninstallApp() {
417 uninstallApp(supportedAppPackageName)
418 }
419
420 private fun startApp() {
421 startApp(supportedAppPackageName)
422 }
423
424 private fun startAppAndAcceptPermission() {
425 startApp()
426 clickPermissionAllow()
427 assertPermission(PERMISSION_GRANTED)
428 }
429
430 private fun goBack() {
431 runShellCommandOrThrow("input keyevent KEYCODE_BACK")
432 }
433
434 private fun killDummyApp(pkg: String = supportedAppPackageName) {
435 if (!SdkLevel.isAtLeastS()) {
436 // Work around a race condition on R that killing the app process too fast after
437 // activity launch would result in a stale process record in LRU process list that
438 // sticks until next reboot.
439 Thread.sleep(5000)
440 }
441 assertThat(
442 runShellCommandOrThrow("am force-stop " + pkg),
443 equalTo(""))
444 awaitAppState(pkg, greaterThan(IMPORTANCE_TOP_SLEEPING))
445 }
446
447 private fun clickPermissionAllow() {
448 if (isAutomotiveDevice()) {
449 waitFindObject(By.text(Pattern.compile(
450 Pattern.quote(mPermissionControllerResources.getString(
451 mPermissionControllerResources.getIdentifier(
452 "grant_dialog_button_allow", "string",
453 "com.android.permissioncontroller"))),
454 Pattern.CASE_INSENSITIVE or Pattern.UNICODE_CASE))).click()
455 } else {
456 waitFindObject(By.res("com.android.permissioncontroller:id/permission_allow_button"))
457 .click()
458 }
459 }
460
461 private fun clickUninstallIcon() {
462 val rowSelector = By.text(supportedAppPackageName)
463 val rowItem = waitFindObject(rowSelector).parent.parent
464
465 val uninstallSelector = if (isAutomotiveDevice()) {
466 By.res("com.android.permissioncontroller:id/car_ui_secondary_action")
467 } else {
468 By.desc("Uninstall or disable")
469 }
470
471 rowItem.findObject(uninstallSelector).click()
472 }
473
474 private fun clickUninstallOk() {
475 waitFindObject(By.text("OK")).click()
476 }
477
478 private inline fun withDummyApp(
479 apk: String = supportedApkPath,
480 packageName: String = supportedAppPackageName,
481 action: () -> Unit
482 ) {
483 withApp(apk, packageName, action)
484 }
485
486 private inline fun withDummyAppNoUninstallAssertion(
487 apk: String = supportedApkPath,
488 packageName: String = supportedAppPackageName,
489 action: () -> Unit
490 ) {
491 withAppNoUninstallAssertion(apk, packageName, action)
492 }
493
494 private fun assertPermission(
495 state: Int,
496 packageName: String = supportedAppPackageName,
497 permission: String = READ_CALENDAR
498 ) {
499 assertPermission(packageName, permission, state)
500 }
501
502 private fun goToPermissions(packageName: String = supportedAppPackageName) {
503 context.startActivity(Intent(ACTION_AUTO_REVOKE_PERMISSIONS)
504 .setData(Uri.fromParts("package", packageName, null))
505 .addFlags(FLAG_ACTIVITY_NEW_TASK))
506
507 waitForIdle()
508
509 click("Permissions")
510 }
511
512 private fun click(label: String) {
513 try {
514 waitFindObject(byTextIgnoreCase(label)).click()
515 } catch (e: UiObjectNotFoundException) {
516 // waitFindObject sometimes fails to find UI that is present in the view hierarchy
517 // Increasing sleep to 2000 in waitForIdle() might be passed but no guarantee that the
518 // UI is fully displayed So Adding one more check without using the UiAutomator helps
519 // reduce false positives
520 waitFindNode(hasTextThat(containsStringIgnoringCase(label))).click()
521 }
522 waitForIdle()
523 }
524
525 private fun assertAllowlistState(state: Boolean) {
526 assertThat(
527 waitFindObject(By.textStartsWith("Auto-revoke allowlisted: ")).text,
528 containsString(state.toString()))
529 }
530
531 private fun getAllowlistToggle(): UiObject2 {
532 waitForIdle()
533 // Wear: per b/253990371, unused_apps_summary string is not available,
534 // so look for unused_apps_label_v2 string instead.
535 val autoRevokeText = if (hasFeatureWatch()) {
536 "Pause app"
537 } else {
538 "Remove permissions"
539 }
540 val parent = waitFindObject(
541 By.clickable(true)
542 .hasDescendant(By.textStartsWith(autoRevokeText))
543 .hasDescendant(By.checkable(true))
544 )
545 return parent.findObject(By.checkable(true))
546 }
547
548 private fun waitForIdle() {
549 instrumentation.uiAutomation.waitForIdle(1000, 10000)
550 Thread.sleep(500)
551 instrumentation.uiAutomation.waitForIdle(1000, 10000)
552 }
553
554 private inline fun <T> eventually(crossinline action: () -> T): T {
555 val res = AtomicReference<T>()
556 SystemUtil.eventually {
557 res.set(action())
558 }
559 return res.get()
560 }
561
562 private fun waitFindObject(selector: BySelector): UiObject2 {
563 return waitFindObject(instrumentation.uiAutomation, selector)
564 }
565 }
566
permissionStateToStringnull567 private fun permissionStateToString(state: Int): String {
568 return constToString<PackageManager>("PERMISSION_", state)
569 }
570
571 /**
572 * For some reason waitFindObject sometimes fails to find UI that is present in the view hierarchy
573 */
waitFindNodenull574 fun waitFindNode(
575 matcher: Matcher<AccessibilityNodeInfo>,
576 failMsg: String? = null,
577 timeoutMs: Long = 10_000
578 ): AccessibilityNodeInfo {
579 return getEventually({
580 val ui = UI_ROOT
581 ui.depthFirstSearch { node ->
582 matcher.matches(node)
583 }.assertNotNull {
584 buildString {
585 if (failMsg != null) {
586 appendLine(failMsg)
587 }
588 appendLine("No view found matching $matcher:\n\n${uiDump(ui)}")
589 }
590 }
591 }, timeoutMs)
592 }
593
byTextIgnoreCasenull594 fun byTextIgnoreCase(txt: String): BySelector {
595 return By.text(Pattern.compile(txt, Pattern.CASE_INSENSITIVE))
596 }
597
waitForIdlenull598 fun waitForIdle() {
599 InstrumentationRegistry.getInstrumentation().uiAutomation.waitForIdle(1000, 10000)
600 }
601
uninstallAppnull602 fun uninstallApp(packageName: String) {
603 assertThat(runShellCommandOrThrow("pm uninstall $packageName"), containsString("Success"))
604 }
605
uninstallAppWithoutAssertionnull606 fun uninstallAppWithoutAssertion(packageName: String) {
607 runShellCommandOrThrow("pm uninstall $packageName")
608 }
609
installApknull610 fun installApk(apk: String) {
611 assertThat(runShellCommandOrThrow("pm install -r $apk"), containsString("Success"))
612 }
613
assertPermissionnull614 fun assertPermission(packageName: String, permissionName: String, state: Int) {
615 assertThat(permissionName, containsString("permission."))
616 eventually {
617 runWithShellPermissionIdentity {
618 assertEquals(
619 permissionStateToString(state),
620 permissionStateToString(
621 InstrumentationRegistry.getTargetContext()
622 .packageManager
623 .checkPermission(permissionName, packageName)))
624 }
625 }
626 }
627
constToStringnull628 inline fun <reified T> constToString(prefix: String, value: Int): String {
629 return T::class.java.declaredFields.filter {
630 Modifier.isStatic(it.modifiers) && it.name.startsWith(prefix)
631 }.map {
632 it.isAccessible = true
633 it.name to it.get(null)
634 }.find { (k, v) ->
635 v == value
636 }.assertNotNull {
637 "None of ${T::class.java.simpleName}.$prefix* == $value"
638 }.first
639 }
640
assertNotNullnull641 inline fun <T> T?.assertNotNull(errorMsg: () -> String): T {
642 return if (this == null) throw AssertionError(errorMsg()) else this
643 }
644