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