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