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