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