• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2021 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.Activity
20 import android.app.ActivityManager
21 import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_TOP_SLEEPING
22 import android.app.UiAutomation
23 import android.content.BroadcastReceiver
24 import android.content.Context
25 import android.content.Intent
26 import android.content.pm.PackageManager
27 import android.graphics.Point
28 import android.os.Build
29 import android.os.Handler
30 import android.os.Looper
31 import android.os.Process
32 import android.provider.DeviceConfig
33 import android.util.Log
34 import androidx.test.InstrumentationRegistry
35 import androidx.test.uiautomator.By
36 import androidx.test.uiautomator.BySelector
37 import androidx.test.uiautomator.Condition
38 import androidx.test.uiautomator.Direction
39 import androidx.test.uiautomator.UiDevice
40 import androidx.test.uiautomator.UiObject2
41 import androidx.test.uiautomator.UiScrollable
42 import androidx.test.uiautomator.UiSelector
43 import androidx.test.uiautomator.Until
44 import com.android.compatibility.common.util.ExceptionUtils.wrappingExceptions
45 import com.android.compatibility.common.util.FeatureUtil
46 import com.android.compatibility.common.util.SystemUtil.eventually
47 import com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow
48 import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
49 import com.android.compatibility.common.util.ThrowingSupplier
50 import com.android.compatibility.common.util.UiAutomatorUtils2
51 import com.android.compatibility.common.util.UiDumpUtils
52 import com.android.compatibility.common.util.UserHelper
53 import com.android.compatibility.common.util.click
54 import com.android.compatibility.common.util.depthFirstSearch
55 import com.android.compatibility.common.util.textAsString
56 import java.util.concurrent.CountDownLatch
57 import java.util.concurrent.TimeUnit
58 import org.hamcrest.Matcher
59 import org.hamcrest.Matchers
60 import org.junit.Assert
61 import org.junit.Assert.assertThat
62 import org.junit.Assert.assertTrue
63 import org.junit.Assume.assumeFalse
64 
65 private const val BROADCAST_TIMEOUT_MS = 60000L
66 
67 const val PROPERTY_SAFETY_CENTER_ENABLED = "safety_center_is_enabled"
68 const val HIBERNATION_BOOT_RECEIVER_CLASS_NAME =
69     "com.android.permissioncontroller.hibernation.HibernationOnBootReceiver"
70 const val ACTION_SET_UP_HIBERNATION =
71     "com.android.permissioncontroller.action.SET_UP_HIBERNATION"
72 
73 const val SYSUI_PKG_NAME = "com.android.systemui"
74 const val NOTIF_LIST_ID = "notification_stack_scroller"
75 const val NOTIF_LIST_ID_AUTOMOTIVE = "notifications"
76 const val BOTTOM_BAR_WINDOW_ID_AUTOMOTIVE = "car_bottom_bar_window"
77 const val CLEAR_ALL_BUTTON_ID = "dismiss_text"
78 const val MANAGE_BUTTON_AUTOMOTIVE = "manage_button"
79 // Time to find a notification. Unlikely, but in cases with a lot of notifications, it may take
80 // time to find the notification we're looking for
81 const val NOTIF_FIND_TIMEOUT = 20000L
82 const val VIEW_WAIT_TIMEOUT = 3000L
83 const val JOB_RUN_TIMEOUT = 40000L
84 const val JOB_RUN_WAIT_TIME = 3000L
85 
86 const val CMD_EXPAND_NOTIFICATIONS = "cmd statusbar expand-notifications"
87 const val CMD_COLLAPSE = "cmd statusbar collapse"
88 const val CMD_CLEAR_NOTIFS = "service call notification 1"
89 
90 const val APK_PATH_S_APP = "/data/local/tmp/cts/hibernation/CtsAutoRevokeSApp.apk"
91 const val APK_PACKAGE_NAME_S_APP = "android.hibernation.cts.autorevokesapp"
92 const val APK_PATH_R_APP = "/data/local/tmp/cts/hibernation/CtsAutoRevokeRApp.apk"
93 const val APK_PACKAGE_NAME_R_APP = "android.hibernation.cts.autorevokerapp"
94 const val APK_PATH_Q_APP = "/data/local/tmp/cts/hibernation/CtsAutoRevokeQApp.apk"
95 const val APK_PACKAGE_NAME_Q_APP = "android.hibernation.cts.autorevokeqapp"
96 
97 fun runBootCompleteReceiver(context: Context, testTag: String) {
98     val pkgManager = context.packageManager
99     val permissionControllerPkg = pkgManager.permissionControllerPackageName
100     var permissionControllerSetupIntent = Intent(ACTION_SET_UP_HIBERNATION).apply {
101         setPackage(permissionControllerPkg)
102         setFlags(Intent.FLAG_RECEIVER_FOREGROUND)
103     }
104     val receivers = pkgManager.queryBroadcastReceivers(
105         permissionControllerSetupIntent, /* flags= */ 0)
106     if (receivers.size == 0) {
107         // May be on an older, pre-built PermissionController. In this case, try sending directly.
108         permissionControllerSetupIntent = Intent().apply {
109             setPackage(permissionControllerPkg)
110             setClassName(permissionControllerPkg, HIBERNATION_BOOT_RECEIVER_CLASS_NAME)
111             setFlags(Intent.FLAG_RECEIVER_FOREGROUND)
112         }
113     }
114     val countdownLatch = CountDownLatch(1)
115     Log.d(testTag, "Sending boot complete broadcast directly to $permissionControllerPkg")
116     context.sendOrderedBroadcast(
117         permissionControllerSetupIntent,
118         /* receiverPermission= */ null,
119         object : BroadcastReceiver() {
120             override fun onReceive(context: Context?, intent: Intent?) {
121                 countdownLatch.countDown()
122                 Log.d(testTag, "Broadcast received by $permissionControllerPkg")
123             }
124         },
125         Handler.createAsync(Looper.getMainLooper()),
126         Activity.RESULT_OK,
127         /* initialData= */ null,
128         /* initialExtras= */ null)
129     assertTrue("Timed out while waiting for boot receiver broadcast to be received",
130         countdownLatch.await(BROADCAST_TIMEOUT_MS, TimeUnit.MILLISECONDS))
131 }
132 
resetJobnull133 fun resetJob(context: Context) {
134     val userId = Process.myUserHandle().identifier
135     val permissionControllerPackageName =
136         context.packageManager.permissionControllerPackageName
137     runShellCommandOrThrow("cmd jobscheduler reset-execution-quota -u " +
138             "$userId $permissionControllerPackageName")
139     runShellCommandOrThrow("cmd jobscheduler reset-schedule-quota")
140 }
141 
runAppHibernationJobnull142 fun runAppHibernationJob(context: Context, tag: String) {
143     runAppHibernationJobInternal(context, tag)
144     if (Build.VERSION.SDK_INT == 31) {
145         // On S and S only, run the job twice as a workaround for a deadlock. See b/291147868
146         runAppHibernationJobInternal(context, tag)
147     }
148 }
149 
runAppHibernationJobInternalnull150 private fun runAppHibernationJobInternal(context: Context, tag: String) {
151     val userId = Process.myUserHandle().identifier
152     val permissionControllerPackageName =
153         context.packageManager.permissionControllerPackageName
154     runShellCommandOrThrow("cmd jobscheduler run -u " +
155             "$userId -f " +
156             "$permissionControllerPackageName 2")
157     eventually({
158         Thread.sleep(JOB_RUN_WAIT_TIME)
159         val jobState = runShellCommandOrThrow("cmd jobscheduler get-job-state -u " +
160             "$userId " +
161             "$permissionControllerPackageName 2")
162         Log.d(tag, "Job output: $jobState")
163         assertTrue("Job expected waiting but is $jobState", jobState.contains("waiting"))
164     }, JOB_RUN_TIMEOUT)
165 }
166 
runPermissionEventCleanupJobnull167 fun runPermissionEventCleanupJob(context: Context) {
168     eventually {
169         runShellCommandOrThrow("cmd jobscheduler run -u " +
170             "${Process.myUserHandle().identifier} -f " +
171             "${context.packageManager.permissionControllerPackageName} 3")
172     }
173 }
174 
withAppnull175 inline fun withApp(
176     apk: String,
177     packageName: String,
178     action: () -> Unit
179 ) {
180     installApk(apk)
181     try {
182         // Try to reduce flakiness caused by new package update not propagating in time
183         Thread.sleep(1000)
184         action()
185     } finally {
186         uninstallApp(packageName)
187     }
188 }
189 
withAppNoUninstallAssertionnull190 inline fun withAppNoUninstallAssertion(
191     apk: String,
192     packageName: String,
193     action: () -> Unit
194 ) {
195     installApk(apk)
196     try {
197         // Try to reduce flakiness caused by new package update not propagating in time
198         Thread.sleep(1000)
199         action()
200     } finally {
201         uninstallAppWithoutAssertion(packageName)
202     }
203 }
204 
withDeviceConfignull205 inline fun <T> withDeviceConfig(
206     namespace: String,
207     name: String,
208     value: String,
209     action: () -> T
210 ): T {
211     val oldValue = runWithShellPermissionIdentity(ThrowingSupplier {
212         DeviceConfig.getProperty(namespace, name)
213     })
214     try {
215         runWithShellPermissionIdentity {
216             DeviceConfig.setProperty(namespace, name, value, false /* makeDefault */)
217         }
218         return action()
219     } finally {
220         runWithShellPermissionIdentity {
221             DeviceConfig.setProperty(namespace, name, oldValue, false /* makeDefault */)
222         }
223     }
224 }
225 
withUnusedThresholdMsnull226 inline fun <T> withUnusedThresholdMs(threshold: Long, action: () -> T): T {
227     return withDeviceConfig(
228         DeviceConfig.NAMESPACE_PERMISSIONS, "auto_revoke_unused_threshold_millis2",
229         threshold.toString(), action)
230 }
231 
withSafetyCenterEnablednull232 inline fun <T> withSafetyCenterEnabled(action: () -> T): T {
233     assumeFalse("This test is only supported on phones",
234         hasFeatureWatch() || hasFeatureTV() || hasFeatureAutomotive()
235     )
236 
237     return withDeviceConfig(
238         DeviceConfig.NAMESPACE_PRIVACY, PROPERTY_SAFETY_CENTER_ENABLED,
239         true.toString(), action)
240 }
241 
awaitAppStatenull242 fun awaitAppState(pkg: String, stateMatcher: Matcher<Int>) {
243     val context: Context = InstrumentationRegistry.getTargetContext()
244     eventually {
245         runWithShellPermissionIdentity {
246             val packageImportance = context
247                 .getSystemService(ActivityManager::class.java)!!
248                 .getPackageImportance(pkg)
249             assertThat(packageImportance, stateMatcher)
250         }
251     }
252 }
253 
startAppnull254 fun startApp(packageName: String) {
255     val context = InstrumentationRegistry.getTargetContext()
256     val intent = context.packageManager.getLaunchIntentForPackage(packageName)
257     context.startActivity(intent)
258     awaitAppState(packageName, Matchers.lessThanOrEqualTo(IMPORTANCE_TOP_SLEEPING))
259     waitForIdle()
260 }
261 
goBacknull262 fun goBack() {
263     val context = InstrumentationRegistry.getTargetContext()
264     val userHelper = UserHelper(context)
265     val displayId = userHelper.getMainDisplayId()
266     runShellCommandOrThrow("input keyevent KEYCODE_BACK -d $displayId")
267     waitForIdle()
268 }
269 
270 /**
271  * Clear notifications from shade
272  */
clearNotificationsnull273 fun clearNotifications() {
274     runWithShellPermissionIdentity {
275         runShellCommandOrThrow(CMD_CLEAR_NOTIFS)
276     }
277 }
278 
279 /**
280  * Open the "unused apps" notification which is sent after the hibernation job.
281  */
openUnusedAppsNotificationnull282 fun openUnusedAppsNotification() {
283     val notifSelector = By.textContains("unused app")
284     if (hasFeatureWatch()) {
285         val uiAutomation = InstrumentationRegistry.getInstrumentation().uiAutomation
286         expandNotificationsWatch(UiAutomatorUtils2.getUiDevice())
287         waitFindObject(uiAutomation, notifSelector).click()
288         // In wear os, notification has one additional button to open it
289         waitFindObject(uiAutomation, By.textContains("Open")).click()
290     } else {
291         val context = InstrumentationRegistry.getTargetContext()
292         val userHelper = UserHelper(context)
293         val isVisibleBackgroundUser = userHelper.isVisibleBackgroundUser()
294         val permissionPkg: String = context.packageManager.permissionControllerPackageName
295         eventually({
296             // Eventually clause because clicking is sometimes inconsistent if the screen is
297             // scrolling
298             if (isVisibleBackgroundUser) {
299                 expandNotificationsForVisibleBackgroundUser(context, userHelper.getMainDisplayId())
300             } else {
301                 runShellCommandOrThrow(CMD_EXPAND_NOTIFICATIONS)
302             }
303             val notification = waitFindNotification(notifSelector, NOTIF_FIND_TIMEOUT)
304             if (hasFeatureAutomotive()) {
305                 notification.click(Point(0, 0))
306             } else {
307                 notification.click()
308             }
309             wrappingExceptions({ cause: Throwable? -> UiDumpUtils.wrapWithUiDump(cause) }) {
310                 assertTrue(
311                     "Unused apps page did not open after tapping notification.",
312                     UiAutomatorUtils2.getUiDevice().wait(
313                         Until.hasObject(By.pkg(permissionPkg).depth(0)), VIEW_WAIT_TIMEOUT
314                     )
315                 )
316             }
317         }, NOTIF_FIND_TIMEOUT)
318     }
319 }
320 
hasFeatureWatchnull321 fun hasFeatureWatch(): Boolean {
322     return InstrumentationRegistry.getTargetContext().packageManager.hasSystemFeature(
323         PackageManager.FEATURE_WATCH)
324 }
325 
hasFeatureTVnull326 fun hasFeatureTV(): Boolean {
327     return InstrumentationRegistry.getTargetContext().packageManager.hasSystemFeature(
328             PackageManager.FEATURE_LEANBACK) ||
329             InstrumentationRegistry.getTargetContext().packageManager.hasSystemFeature(
330                     PackageManager.FEATURE_TELEVISION)
331 }
332 
hasFeatureAutomotivenull333 fun hasFeatureAutomotive(): Boolean {
334     return InstrumentationRegistry.getTargetContext().packageManager.hasSystemFeature(
335         PackageManager.FEATURE_AUTOMOTIVE)
336 }
337 
expandNotificationsWatchnull338 private fun expandNotificationsWatch(uiDevice: UiDevice) {
339     with(uiDevice) {
340         wakeUp()
341         // Swipe up from bottom to reveal notifications
342         val x = displayWidth / 2
343         swipe(x, displayHeight, x, 0, 1)
344     }
345 }
346 
expandNotificationsForVisibleBackgroundUsernull347 private fun expandNotificationsForVisibleBackgroundUser(context: Context, displayId: Int) {
348     val uiDevice = UiAutomatorUtils2.getUiDevice()
349     val searchCondition = object : Condition<UiDevice, Boolean> {
350         override fun apply(device: UiDevice): Boolean {
351             return device.findObjects(
352                 By.res(SYSUI_PKG_NAME, BOTTOM_BAR_WINDOW_ID_AUTOMOTIVE)
353             ).size > 0
354         }
355     }
356     uiDevice.wait(searchCondition, JOB_RUN_WAIT_TIME)
357     val navigationBarFrame = uiDevice.findObject(
358                 By.pkg(SYSUI_PKG_NAME).res(SYSUI_PKG_NAME, BOTTOM_BAR_WINDOW_ID_AUTOMOTIVE)
359                     .displayId(displayId))
360     // swipe up the car bottom bar to expand the notification panel of visible background user
361     navigationBarFrame.swipe(Direction.UP, 1.0f)
362 }
363 
364 /**
365  * Reset to the top of the notifications list.
366  */
resetNotificationsnull367 private fun resetNotifications(notificationList: UiScrollable) {
368     runShellCommandOrThrow(CMD_COLLAPSE)
369     notificationList.waitUntilGone(VIEW_WAIT_TIMEOUT)
370     runShellCommandOrThrow(CMD_EXPAND_NOTIFICATIONS)
371 }
372 
waitFindNotificationnull373 private fun waitFindNotification(selector: BySelector, timeoutMs: Long):
374     UiObject2 {
375     var view: UiObject2? = null
376     val start = System.currentTimeMillis()
377     val uiDevice = UiAutomatorUtils2.getUiDevice()
378 
379     var isAtEnd = false
380     var wasScrolledUpAlready = false
381     val notificationListId = if (FeatureUtil.isAutomotive()) {
382         NOTIF_LIST_ID_AUTOMOTIVE
383     } else {
384         NOTIF_LIST_ID
385     }
386     val notificationEndViewId = if (FeatureUtil.isAutomotive()) {
387         MANAGE_BUTTON_AUTOMOTIVE
388     } else {
389         CLEAR_ALL_BUTTON_ID
390     }
391     while (view == null && start + timeoutMs > System.currentTimeMillis()) {
392         view = uiDevice.wait(Until.findObject(selector), VIEW_WAIT_TIMEOUT)
393         if (view == null) {
394             val notificationList = UiScrollable(UiSelector().resourceId(
395                 SYSUI_PKG_NAME + ":id/" + notificationListId))
396             wrappingExceptions({ cause: Throwable? -> UiDumpUtils.wrapWithUiDump(cause) }) {
397                 Assert.assertTrue("Notification list view not found",
398                     notificationList.waitForExists(VIEW_WAIT_TIMEOUT))
399             }
400             if (isAtEnd) {
401                 if (wasScrolledUpAlready) {
402                     break
403                 }
404                 resetNotifications(notificationList)
405                 isAtEnd = false
406                 wasScrolledUpAlready = true
407             } else {
408                 notificationList.scrollForward()
409                 isAtEnd = uiDevice.hasObject(By.res(SYSUI_PKG_NAME, notificationEndViewId))
410             }
411         }
412     }
413     wrappingExceptions({ cause: Throwable? -> UiDumpUtils.wrapWithUiDump(cause) }) {
414         Assert.assertNotNull("View not found after waiting for " + timeoutMs + "ms: " + selector,
415             view)
416     }
417     return view!!
418 }
419 
waitFindObjectnull420 fun waitFindObject(uiAutomation: UiAutomation, selector: BySelector): UiObject2 {
421     try {
422         return UiAutomatorUtils2.waitFindObject(selector)
423     } catch (e: RuntimeException) {
424         val ui = uiAutomation.rootInActiveWindow
425 
426         val title = ui.depthFirstSearch { node ->
427             node.viewIdResourceName?.contains("alertTitle") == true
428         }
429         val okCloseButton = ui.depthFirstSearch { node ->
430             (node.textAsString?.equals("OK", ignoreCase = true) ?: false) ||
431                 (node.textAsString?.equals("Close app", ignoreCase = true) ?: false)
432         }
433         val titleString = title?.text?.toString()
434         if (okCloseButton != null &&
435             titleString != null &&
436             (titleString == "Android System" ||
437                 titleString.endsWith("keeps stopping"))) {
438             // Auto dismiss occasional system dialogs to prevent interfering with the test
439             android.util.Log.w(AutoRevokeTest.LOG_TAG, "Ignoring exception", e)
440             okCloseButton.click()
441             return UiAutomatorUtils2.waitFindObject(selector)
442         } else {
443             throw e
444         }
445     }
446 }
447 
scrollToLabelnull448 fun scrollToLabel(label: String) {
449     val uiDevice = UiAutomatorUtils2.getUiDevice()
450 
451     if (!uiDevice.hasObject(By.text(label))) {
452         var scrollableObject = UiScrollable(UiSelector().scrollable(true))
453         scrollableObject.scrollTextIntoView(label)
454     }
455 }
456