• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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