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.os.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.Instrumentation
23 import android.app.UiAutomation
24 import android.content.BroadcastReceiver
25 import android.content.Context
26 import android.content.Intent
27 import android.content.pm.PackageManager
28 import android.os.Handler
29 import android.os.Looper
30 import android.os.ParcelFileDescriptor
31 import android.os.Process
32 import android.provider.DeviceConfig
33 import android.support.test.uiautomator.By
34 import android.support.test.uiautomator.BySelector
35 import android.support.test.uiautomator.UiDevice
36 import android.support.test.uiautomator.UiObject2
37 import android.support.test.uiautomator.UiScrollable
38 import android.support.test.uiautomator.UiSelector
39 import android.support.test.uiautomator.Until
40 import android.util.Log
41 import androidx.test.InstrumentationRegistry
42 import com.android.compatibility.common.util.ExceptionUtils.wrappingExceptions
43 import com.android.compatibility.common.util.FeatureUtil
44 import com.android.compatibility.common.util.LogcatInspector
45 import com.android.compatibility.common.util.SystemUtil.eventually
46 import com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow
47 import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
48 import com.android.compatibility.common.util.ThrowingSupplier
49 import com.android.compatibility.common.util.UiAutomatorUtils
50 import com.android.compatibility.common.util.UiDumpUtils
51 import com.android.compatibility.common.util.click
52 import com.android.compatibility.common.util.depthFirstSearch
53 import com.android.compatibility.common.util.textAsString
54 import org.hamcrest.Matcher
55 import org.hamcrest.Matchers
56 import org.junit.Assert
57 import org.junit.Assert.assertThat
58 import org.junit.Assert.assertTrue
59 import java.io.InputStream
60 import java.util.concurrent.CountDownLatch
61 import java.util.concurrent.TimeUnit
62
63 private const val BROADCAST_TIMEOUT_MS = 60000L
64
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 = "com.android.systemui:id/notification_stack_scroller"
72 const val NOTIF_LIST_ID_AUTOMOTIVE = "com.android.systemui:id/notifications"
73 const val CLEAR_ALL_BUTTON_ID = "dismiss_text"
74 // Time to find a notification. Unlikely, but in cases with a lot of notifications, it may take
75 // time to find the notification we're looking for
76 const val NOTIF_FIND_TIMEOUT = 20000L
77 const val VIEW_WAIT_TIMEOUT = 3000L
78
79 const val CMD_EXPAND_NOTIFICATIONS = "cmd statusbar expand-notifications"
80 const val CMD_COLLAPSE = "cmd statusbar collapse"
81
82 const val APK_PATH_S_APP = "/data/local/tmp/cts/os/CtsAutoRevokeSApp.apk"
83 const val APK_PACKAGE_NAME_S_APP = "android.os.cts.autorevokesapp"
84 const val APK_PATH_R_APP = "/data/local/tmp/cts/os/CtsAutoRevokeRApp.apk"
85 const val APK_PACKAGE_NAME_R_APP = "android.os.cts.autorevokerapp"
86 const val APK_PATH_Q_APP = "/data/local/tmp/cts/os/CtsAutoRevokeQApp.apk"
87 const val APK_PACKAGE_NAME_Q_APP = "android.os.cts.autorevokeqapp"
88
89 fun runBootCompleteReceiver(context: Context, testTag: String) {
90 val pkgManager = context.packageManager
91 val permissionControllerPkg = pkgManager.permissionControllerPackageName
92 var permissionControllerSetupIntent = Intent(ACTION_SET_UP_HIBERNATION).apply {
93 setPackage(permissionControllerPkg)
94 setFlags(Intent.FLAG_RECEIVER_FOREGROUND)
95 }
96 val receivers = pkgManager.queryBroadcastReceivers(
97 permissionControllerSetupIntent, /* flags= */ 0)
98 if (receivers.size == 0) {
99 // May be on an older, pre-built PermissionController. In this case, try sending directly.
100 permissionControllerSetupIntent = Intent().apply {
101 setPackage(permissionControllerPkg)
102 setClassName(permissionControllerPkg, HIBERNATION_BOOT_RECEIVER_CLASS_NAME)
103 setFlags(Intent.FLAG_RECEIVER_FOREGROUND)
104 }
105 }
106 val countdownLatch = CountDownLatch(1)
107 Log.d(testTag, "Sending boot complete broadcast directly to $permissionControllerPkg")
108 context.sendOrderedBroadcast(
109 permissionControllerSetupIntent,
110 /* receiverPermission= */ null,
111 object : BroadcastReceiver() {
112 override fun onReceive(context: Context?, intent: Intent?) {
113 countdownLatch.countDown()
114 Log.d(testTag, "Broadcast received by $permissionControllerPkg")
115 }
116 },
117 Handler.createAsync(Looper.getMainLooper()),
118 Activity.RESULT_OK,
119 /* initialData= */ null,
120 /* initialExtras= */ null)
121 assertTrue("Timed out while waiting for boot receiver broadcast to be received",
122 countdownLatch.await(BROADCAST_TIMEOUT_MS, TimeUnit.MILLISECONDS))
123 }
124
runAppHibernationJobnull125 fun runAppHibernationJob(context: Context, tag: String) {
126 val logcat = Logcat()
127
128 // Sometimes first run observes stale package data
129 // so run twice to prevent that
130 repeat(2) {
131 val mark = logcat.mark(tag)
132 eventually {
133 runShellCommandOrThrow("cmd jobscheduler run -u " +
134 "${Process.myUserHandle().identifier} -f " +
135 "${context.packageManager.permissionControllerPackageName} 2")
136 }
137 logcat.assertLogcatContainsInOrder("*:*", 30_000,
138 mark,
139 "onStartJob",
140 "Done auto-revoke for user")
141 }
142 }
143
withAppnull144 inline fun withApp(
145 apk: String,
146 packageName: String,
147 action: () -> Unit
148 ) {
149 installApk(apk)
150 try {
151 // Try to reduce flakiness caused by new package update not propagating in time
152 Thread.sleep(1000)
153 action()
154 } finally {
155 uninstallApp(packageName)
156 }
157 }
158
withAppNoUninstallAssertionnull159 inline fun withAppNoUninstallAssertion(
160 apk: String,
161 packageName: String,
162 action: () -> Unit
163 ) {
164 installApk(apk)
165 try {
166 // Try to reduce flakiness caused by new package update not propagating in time
167 Thread.sleep(1000)
168 action()
169 } finally {
170 uninstallAppWithoutAssertion(packageName)
171 }
172 }
173
withDeviceConfignull174 inline fun <T> withDeviceConfig(
175 namespace: String,
176 name: String,
177 value: String,
178 action: () -> T
179 ): T {
180 val oldValue = runWithShellPermissionIdentity(ThrowingSupplier {
181 DeviceConfig.getProperty(namespace, name)
182 })
183 try {
184 runWithShellPermissionIdentity {
185 DeviceConfig.setProperty(namespace, name, value, false /* makeDefault */)
186 }
187 return action()
188 } finally {
189 runWithShellPermissionIdentity {
190 DeviceConfig.setProperty(namespace, name, oldValue, false /* makeDefault */)
191 }
192 }
193 }
194
withUnusedThresholdMsnull195 inline fun <T> withUnusedThresholdMs(threshold: Long, action: () -> T): T {
196 return withDeviceConfig(
197 DeviceConfig.NAMESPACE_PERMISSIONS, "auto_revoke_unused_threshold_millis2",
198 threshold.toString(), action)
199 }
200
awaitAppStatenull201 fun awaitAppState(pkg: String, stateMatcher: Matcher<Int>) {
202 val context: Context = InstrumentationRegistry.getTargetContext()
203 eventually {
204 runWithShellPermissionIdentity {
205 val packageImportance = context
206 .getSystemService(ActivityManager::class.java)!!
207 .getPackageImportance(pkg)
208 assertThat(packageImportance, stateMatcher)
209 }
210 }
211 }
212
startAppnull213 fun startApp(packageName: String) {
214 val context = InstrumentationRegistry.getTargetContext()
215 val intent = context.packageManager.getLaunchIntentForPackage(packageName)
216 context.startActivity(intent)
217 awaitAppState(packageName, Matchers.lessThanOrEqualTo(IMPORTANCE_TOP_SLEEPING))
218 waitForIdle()
219 }
220
goHomenull221 fun goHome() {
222 runShellCommandOrThrow("input keyevent KEYCODE_HOME")
223 }
224
225 /**
226 * Open the "unused apps" notification which is sent after the hibernation job.
227 */
openUnusedAppsNotificationnull228 fun openUnusedAppsNotification() {
229 val notifSelector = By.textContains("unused app")
230 if (hasFeatureWatch()) {
231 val uiAutomation = InstrumentationRegistry.getInstrumentation().uiAutomation
232 expandNotificationsWatch(UiAutomatorUtils.getUiDevice())
233 waitFindObject(uiAutomation, notifSelector).click()
234 // In wear os, notification has one additional button to open it
235 waitFindObject(uiAutomation, By.text("Open")).click()
236 } else {
237 runShellCommandOrThrow(CMD_EXPAND_NOTIFICATIONS)
238 waitFindNotification(notifSelector, NOTIF_FIND_TIMEOUT).click()
239 }
240 }
241
hasFeatureWatchnull242 fun hasFeatureWatch(): Boolean {
243 return InstrumentationRegistry.getTargetContext().packageManager.hasSystemFeature(
244 PackageManager.FEATURE_WATCH)
245 }
246
hasFeatureTVnull247 fun hasFeatureTV(): Boolean {
248 return InstrumentationRegistry.getTargetContext().packageManager.hasSystemFeature(
249 PackageManager.FEATURE_LEANBACK) ||
250 InstrumentationRegistry.getTargetContext().packageManager.hasSystemFeature(
251 PackageManager.FEATURE_TELEVISION)
252 }
253
expandNotificationsWatchnull254 private fun expandNotificationsWatch(uiDevice: UiDevice) {
255 with(uiDevice) {
256 wakeUp()
257 // Swipe up from bottom to reveal notifications
258 val x = displayWidth / 2
259 swipe(x, displayHeight, x, 0, 1)
260 }
261 }
262
263 /**
264 * Reset to the top of the notifications list.
265 */
resetNotificationsnull266 private fun resetNotifications(notificationList: UiScrollable) {
267 runShellCommandOrThrow(CMD_COLLAPSE)
268 notificationList.waitUntilGone(VIEW_WAIT_TIMEOUT)
269 runShellCommandOrThrow(CMD_EXPAND_NOTIFICATIONS)
270 }
271
waitFindNotificationnull272 private fun waitFindNotification(selector: BySelector, timeoutMs: Long):
273 UiObject2 {
274 var view: UiObject2? = null
275 val start = System.currentTimeMillis()
276 val uiDevice = UiAutomatorUtils.getUiDevice()
277
278 var isAtEnd = false
279 var wasScrolledUpAlready = false
280 while (view == null && start + timeoutMs > System.currentTimeMillis()) {
281 view = uiDevice.wait(Until.findObject(selector), VIEW_WAIT_TIMEOUT)
282 if (view == null) {
283 val notificationListId = if (FeatureUtil.isAutomotive()) {
284 NOTIF_LIST_ID_AUTOMOTIVE
285 } else {
286 NOTIF_LIST_ID
287 }
288 val notificationList = UiScrollable(UiSelector().resourceId(notificationListId))
289 wrappingExceptions({ cause: Throwable? -> UiDumpUtils.wrapWithUiDump(cause) }) {
290 Assert.assertTrue("Notification list view not found",
291 notificationList.waitForExists(VIEW_WAIT_TIMEOUT))
292 }
293 if (isAtEnd) {
294 if (wasScrolledUpAlready) {
295 break
296 }
297 resetNotifications(notificationList)
298 isAtEnd = false
299 wasScrolledUpAlready = true
300 } else {
301 notificationList.scrollForward()
302 isAtEnd = uiDevice.hasObject(By.res(SYSUI_PKG_NAME, CLEAR_ALL_BUTTON_ID))
303 }
304 }
305 }
306 wrappingExceptions({ cause: Throwable? -> UiDumpUtils.wrapWithUiDump(cause) }) {
307 Assert.assertNotNull("View not found after waiting for " + timeoutMs + "ms: " + selector,
308 view)
309 }
310 return view!!
311 }
312
waitFindObjectnull313 fun waitFindObject(uiAutomation: UiAutomation, selector: BySelector): UiObject2 {
314 try {
315 return UiAutomatorUtils.waitFindObject(selector)
316 } catch (e: RuntimeException) {
317 val ui = uiAutomation.rootInActiveWindow
318
319 val title = ui.depthFirstSearch { node ->
320 node.viewIdResourceName?.contains("alertTitle") == true
321 }
322 val okCloseButton = ui.depthFirstSearch { node ->
323 (node.textAsString?.equals("OK", ignoreCase = true) ?: false) ||
324 (node.textAsString?.equals("Close app", ignoreCase = true) ?: false)
325 }
326 val titleString = title?.text?.toString()
327 if (okCloseButton != null &&
328 titleString != null &&
329 (titleString == "Android System" ||
330 titleString.endsWith("keeps stopping"))) {
331 // Auto dismiss occasional system dialogs to prevent interfering with the test
332 android.util.Log.w(AutoRevokeTest.LOG_TAG, "Ignoring exception", e)
333 okCloseButton.click()
334 return UiAutomatorUtils.waitFindObject(selector)
335 } else {
336 throw e
337 }
338 }
339 }
340
341 class Logcat() : LogcatInspector() {
executeShellCommandnull342 override fun executeShellCommand(command: String?): InputStream {
343 val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
344 return ParcelFileDescriptor.AutoCloseInputStream(
345 instrumentation.uiAutomation.executeShellCommand(command))
346 }
347 }
348