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