1 /*
<lambda>null2  * Copyright (C) 2016 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.permissionui.cts
18 
19 import android.app.Instrumentation
20 import android.app.PendingIntent
21 import android.app.PendingIntent.FLAG_MUTABLE
22 import android.app.PendingIntent.FLAG_UPDATE_CURRENT
23 import android.app.UiAutomation
24 import android.content.BroadcastReceiver
25 import android.content.ComponentName
26 import android.content.Context
27 import android.content.Context.RECEIVER_EXPORTED
28 import android.content.Intent
29 import android.content.IntentFilter
30 import android.content.pm.PackageInstaller
31 import android.content.pm.PackageInstaller.EXTRA_STATUS
32 import android.content.pm.PackageInstaller.EXTRA_STATUS_MESSAGE
33 import android.content.pm.PackageInstaller.STATUS_FAILURE_INVALID
34 import android.content.pm.PackageInstaller.STATUS_SUCCESS
35 import android.content.pm.PackageInstaller.SessionParams
36 import android.content.pm.PackageManager
37 import android.content.res.Resources
38 import android.os.PersistableBundle
39 import android.os.SystemClock
40 import android.platform.test.rule.ScreenRecordRule
41 import android.provider.DeviceConfig
42 import android.provider.Settings
43 import android.text.Html
44 import android.util.Log
45 import android.view.KeyEvent
46 import androidx.test.core.app.ActivityScenario
47 import androidx.test.platform.app.InstrumentationRegistry
48 import androidx.test.uiautomator.By
49 import androidx.test.uiautomator.BySelector
50 import androidx.test.uiautomator.Direction
51 import androidx.test.uiautomator.StaleObjectException
52 import androidx.test.uiautomator.UiDevice
53 import androidx.test.uiautomator.UiObject2
54 import androidx.test.uiautomator.UiScrollable
55 import androidx.test.uiautomator.UiSelector
56 import androidx.test.uiautomator.Until
57 import com.android.compatibility.common.util.DisableAnimationRule
58 import com.android.compatibility.common.util.FreezeRotationRule
59 import com.android.compatibility.common.util.SystemUtil.runShellCommand
60 import com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow
61 import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
62 import com.android.compatibility.common.util.UiAutomatorUtils2
63 import com.android.compatibility.common.util.UserHelper
64 import com.android.modules.utils.build.SdkLevel
65 import com.google.common.truth.Truth.assertThat
66 import java.io.File
67 import java.util.concurrent.CompletableFuture
68 import java.util.concurrent.LinkedBlockingQueue
69 import java.util.concurrent.TimeUnit
70 import java.util.regex.Pattern
71 import org.junit.After
72 import org.junit.Assert
73 import org.junit.Assert.assertEquals
74 import org.junit.Assert.assertNotEquals
75 import org.junit.Before
76 import org.junit.Rule
77 
78 @ScreenRecordRule.ScreenRecord
79 abstract class BasePermissionTest {
80     companion object {
81         private const val TAG = "BasePermissionTest"
82 
83         private const val INSTALL_ACTION_CALLBACK = "BasePermissionTest.install_callback"
84 
85         private const val MAX_SWIPES = 5
86 
87         const val APK_DIRECTORY = "/data/local/tmp/cts-permissionui"
88 
89         const val QUICK_CHECK_TIMEOUT_MILLIS = 100L
90         const val IDLE_TIMEOUT_MILLIS: Long = 1000
91         const val IDLE_LONG_TIMEOUT_MILLIS: Long = 5000
92         const val UNEXPECTED_TIMEOUT_MILLIS = 1000
93         const val TIMEOUT_MILLIS: Long = 20000
94         const val PACKAGE_INSTALLER_TIMEOUT = 60000L
95         const val NEW_WINDOW_TIMEOUT_MILLIS: Long = 20_000
96 
97         @JvmStatic
98         protected val instrumentation: Instrumentation =
99             InstrumentationRegistry.getInstrumentation()
100         @JvmStatic protected val context: Context = instrumentation.context
101         @JvmStatic protected val uiAutomation: UiAutomation = instrumentation.uiAutomation
102         @JvmStatic protected val uiDevice: UiDevice = UiDevice.getInstance(instrumentation)
103         @JvmStatic protected val packageManager: PackageManager = context.packageManager
104         private val packageInstaller = packageManager.packageInstaller
105         @JvmStatic
106         private val mPermissionControllerResources: Resources =
107             context
108                 .createPackageContext(context.packageManager.permissionControllerPackageName, 0)
109                 .resources
110 
111         @JvmStatic
112         protected val isTv = packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
113         @JvmStatic
114         protected val isWatch = packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)
115         @JvmStatic
116         protected val isAutomotive =
117             packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)
118         @JvmStatic
119         protected val isAutomotiveSplitscreen = isAutomotive &&
120             packageManager.hasSystemFeature(
121                     /* PackageManager.FEATURE_CAR_SPLITSCREEN_MULTITASKING */
122                     "android.software.car.splitscreen_multitasking")
123         @JvmStatic
124         private val isAutomotiveVisibleBackgroundUser = isAutomotive &&
125             UserHelper(context).isVisibleBackgroundUser()
126 
127         // TODO(b/382327037):find a way to avoid specifying the display ID for each UiSelector.
128         @JvmStatic
129         protected val displayId = UserHelper().mainDisplayId
130     }
131 
132     @get:Rule val screenRecordRule = ScreenRecordRule(false, false)
133 
134     @get:Rule val disableAnimationRule = DisableAnimationRule()
135 
136     @get:Rule val freezeRotationRule = FreezeRotationRule()
137 
138     var activityScenario: ActivityScenario<StartForFutureActivity>? = null
139 
140     data class SessionResult(val status: Int?)
141 
142     /** If a status was received the value of the status, otherwise null */
143     private var installSessionResult = LinkedBlockingQueue<SessionResult>()
144 
145     private val installSessionResultReceiver =
146         object : BroadcastReceiver() {
147             override fun onReceive(context: Context, intent: Intent) {
148                 val status = intent.getIntExtra(EXTRA_STATUS, STATUS_FAILURE_INVALID)
149                 val msg = intent.getStringExtra(EXTRA_STATUS_MESSAGE)
150                 Log.d(TAG, "status: $status, msg: $msg")
151 
152                 installSessionResult.offer(SessionResult(status))
153             }
154         }
155 
156     private var screenTimeoutBeforeTest: Long = 0L
157 
158     @Before
159     fun setUp() {
160         runWithShellPermissionIdentity {
161             screenTimeoutBeforeTest =
162                 Settings.System.getLong(context.contentResolver, Settings.System.SCREEN_OFF_TIMEOUT)
163             Settings.System.putLong(
164                 context.contentResolver,
165                 Settings.System.SCREEN_OFF_TIMEOUT,
166                 1800000L
167             )
168         }
169 
170         uiDevice.wakeUp()
171         runShellCommand(instrumentation, "wm dismiss-keyguard")
172 
173         uiDevice.findObject(By.text("Close").displayId(displayId))?.click()
174     }
175 
176     @Before
177     fun registerInstallSessionResultReceiver() {
178         context.registerReceiver(
179             installSessionResultReceiver,
180             IntentFilter(INSTALL_ACTION_CALLBACK),
181             RECEIVER_EXPORTED
182         )
183     }
184 
185     @After
186     fun unregisterInstallSessionResultReceiver() {
187         try {
188             context.unregisterReceiver(installSessionResultReceiver)
189         } catch (ignored: IllegalArgumentException) {}
190     }
191 
192     @After
193     fun tearDown() {
194         runWithShellPermissionIdentity {
195             Settings.System.putLong(
196                 context.contentResolver,
197                 Settings.System.SCREEN_OFF_TIMEOUT,
198                 screenTimeoutBeforeTest
199             )
200         }
201 
202         try {
203             activityScenario?.close()
204         } catch (e: NullPointerException) {
205             // ignore
206         }
207 
208         pressHome()
209     }
210 
211     protected fun setDeviceConfigPrivacyProperty(
212         propertyName: String,
213         value: String,
214     ) {
215         runWithShellPermissionIdentity(instrumentation.uiAutomation) {
216             val valueWasSet =
217                 DeviceConfig.setProperty(
218                     DeviceConfig.NAMESPACE_PRIVACY,
219                     /* name = */ propertyName,
220                     /* value = */ value,
221                     /* makeDefault = */ false
222                 )
223             check(valueWasSet) { "Could not set $propertyName to $value" }
224         }
225     }
226 
227     protected fun getPermissionControllerString(res: String, vararg formatArgs: Any): Pattern {
228         val textWithHtml =
229             mPermissionControllerResources.getString(
230                 mPermissionControllerResources.getIdentifier(
231                     res,
232                     "string",
233                     "com.android.permissioncontroller"
234                 ),
235                 *formatArgs
236             )
237         val textWithoutHtml = Html.fromHtml(textWithHtml, 0).toString()
238         return Pattern.compile(
239             Pattern.quote(textWithoutHtml),
240             Pattern.CASE_INSENSITIVE or Pattern.UNICODE_CASE
241         )
242     }
243 
244     protected fun getPermissionControllerResString(res: String): String? {
245         try {
246             return mPermissionControllerResources.getString(
247                 mPermissionControllerResources.getIdentifier(
248                     res,
249                     "string",
250                     "com.android.permissioncontroller"
251                 )
252             )
253         } catch (e: Resources.NotFoundException) {
254             return null
255         }
256     }
257 
258     protected fun byAnyText(vararg texts: String?): BySelector {
259         var regex = ""
260         for (text in texts) {
261             if (text != null) {
262                 regex = regex + Pattern.quote(text) + "|"
263             }
264         }
265         if (regex.endsWith("|")) {
266             regex = regex.dropLast(1)
267         }
268         return By.text(Pattern.compile(regex, Pattern.CASE_INSENSITIVE or Pattern.UNICODE_CASE))
269                 .displayId(displayId)
270     }
271 
272     protected open fun installPackage(
273         apkPath: String,
274         reinstall: Boolean = false,
275         grantRuntimePermissions: Boolean = false,
276         expectSuccess: Boolean = true,
277         installSource: String? = null
278     ) {
279         val output =
280             runShellCommandOrThrow(
281                     "pm install${if (SdkLevel.isAtLeastU()) " --bypass-low-target-sdk-block" else ""} " +
282                         "${if (reinstall) " -r" else ""}${if (grantRuntimePermissions) " -g"
283                 else ""}${if (installSource != null) " -i $installSource" else ""} $apkPath"
284                 )
285                 .trim()
286         if (expectSuccess) {
287             assertEquals("Success", output)
288         } else {
289             assertNotEquals("Success", output)
290         }
291     }
292 
293     protected fun installPackageViaSession(
294         apkName: String,
295         appMetadata: PersistableBundle? = null,
296         packageSource: Int? = null,
297         allowlistedRestrictedPermissions: Set<String>? = null
298     ) {
299         val (sessionId, session) = createPackageInstallerSession(
300             packageSource,
301             allowlistedRestrictedPermissions
302         )
303         runWithShellPermissionIdentity {
304             writePackageInstallerSession(session, apkName)
305             if (appMetadata != null) {
306                 setAppMetadata(session, appMetadata)
307             }
308             commitPackageInstallerSession(session)
309 
310             // No need to click installer UI here due to running in shell permission identity and
311             // not needing user interaciton to complete install. Install should have succeeded.
312             val result = getInstallSessionResult()
313             assertThat(result.status).isEqualTo(STATUS_SUCCESS)
314         }
315     }
316 
317     protected fun uninstallPackage(packageName: String, requireSuccess: Boolean = true) {
318         val output = runShellCommand("pm uninstall $packageName").trim()
319         if (requireSuccess) {
320             assertEquals("Success", output)
321         }
322     }
323 
324     protected fun waitFindObject(selector: BySelector): UiObject2 {
325         return findObjectWithRetry({ t -> UiAutomatorUtils2.waitFindObject(selector, t) })!!
326     }
327 
328     protected fun waitFindObject(selector: BySelector, timeoutMillis: Long): UiObject2 {
329         return findObjectWithRetry(
330             { t -> UiAutomatorUtils2.waitFindObject(selector, t) },
331             timeoutMillis
332         )!!
333     }
334 
335     protected fun waitFindObjectOrNull(selector: BySelector): UiObject2? {
336         return findObjectWithRetry({ t -> UiAutomatorUtils2.waitFindObjectOrNull(selector, t) })
337     }
338 
339     protected fun waitFindObjectOrNull(selector: BySelector, timeoutMillis: Long): UiObject2? {
340         return findObjectWithRetry(
341             { t -> UiAutomatorUtils2.waitFindObjectOrNull(selector, t) },
342             timeoutMillis
343         )
344     }
345 
346     private fun findObjectWithRetry(
347         automatorMethod: (timeoutMillis: Long) -> UiObject2?,
348         timeoutMillis: Long = 20_000L
349     ): UiObject2? {
350         val startTime = SystemClock.elapsedRealtime()
351         return try {
352             automatorMethod(timeoutMillis)
353         } catch (e: StaleObjectException) {
354             val remainingTime = timeoutMillis - (SystemClock.elapsedRealtime() - startTime)
355             if (remainingTime <= 0) {
356                 throw e
357             }
358             automatorMethod(remainingTime)
359         }
360     }
361 
362     protected fun click(selector: BySelector, timeoutMillis: Long = 20_000) {
363         waitFindObject(selector, timeoutMillis).click()
364     }
365 
366     protected fun clickAndWaitForWindowTransition(
367         selector: BySelector,
368         timeoutMillis: Long = 20_000
369     ) {
370         waitFindObject(selector, timeoutMillis)
371             .clickAndWait(Until.newWindow(), NEW_WINDOW_TIMEOUT_MILLIS)
372     }
373 
374     protected fun findView(selector: BySelector, timeoutMs: Long, expected: Boolean) {
375         val exception =
376             try {
377                 waitFindObject(selector, timeoutMs)
378                 null
379             } catch (e: Exception) {
380                 e
381             }
382         Assert.assertTrue("Expected to find view: $expected", (exception == null) == expected)
383     }
384 
385     protected fun findView(selector: BySelector, expected: Boolean) {
386         val timeoutMs =
387             if (expected) {
388                 // Small screens with larger font fail to find views within 10s while scrolling
389                 15000L
390             } else {
391                 1000L
392             }
393 
394         findView(selector, timeoutMs, expected)
395     }
396 
397     protected fun clickPermissionControllerUi(selector: BySelector, timeoutMillis: Long = 20_000) {
398         click(selector.pkg(context.packageManager.permissionControllerPackageName), timeoutMillis)
399     }
400 
401     /**
402      * Clicks Permission Controller UI with a swipe based timeout instead of a time based one
403      *
404      * Use this if finding some Permission Controller UI isn't time bound.
405      *
406      * @param text The text to search for
407      * @param maxSearchSwipes See {@link UiScrollable#setMaxSearchSwipes}
408      */
409     protected fun clickPermissionControllerUi(text: String, maxSearchSwipes: Int = 5) {
410         scrollToText(text, maxSearchSwipes).click()
411     }
412 
413     private fun scrollToText(text: String, maxSearchSwipes: Int = MAX_SWIPES): UiObject2 {
414         var foundObject: UiObject2?
415         if (isAutomotiveVisibleBackgroundUser) {
416             val scrollableObject = uiDevice.findObject(By.scrollable(true).displayId(displayId))
417             foundObject =
418                     scrollableObject.scrollUntil(Direction.DOWN, Until.findObject(By.text(text)))
419         } else {
420             val scrollable =
421                 UiScrollable(UiSelector().scrollable(true)).apply {
422                     this.maxSearchSwipes = maxSearchSwipes
423                 }
424             scrollable.scrollTextIntoView(text)
425             foundObject = uiDevice.findObject(
426                     By.text(text).pkg(context.packageManager.permissionControllerPackageName)
427             )
428         }
429         Assert.assertNotNull("View not found after scrolling", foundObject)
430         return foundObject
431     }
432 
433     protected fun pressBack() {
434         runShellCommandOrThrow("input -d $displayId keyevent ${KeyEvent.KEYCODE_BACK}")
435     }
436 
437     protected fun pressHome() {
438         runShellCommandOrThrow("input -d $displayId keyevent ${KeyEvent.KEYCODE_HOME}")
439     }
440 
441     protected fun pressDPadDown() {
442         runShellCommandOrThrow("input -d $displayId keyevent ${KeyEvent.KEYCODE_DPAD_DOWN}")
443         waitForIdle()
444     }
445 
446     protected fun waitForIdle() = uiAutomation.waitForIdle(IDLE_TIMEOUT_MILLIS, TIMEOUT_MILLIS)
447 
448     protected fun waitForIdleLong() =
449             uiAutomation.waitForIdle(IDLE_LONG_TIMEOUT_MILLIS, TIMEOUT_MILLIS)
450 
451     protected fun startActivityForFuture(
452         intent: Intent
453     ): CompletableFuture<Instrumentation.ActivityResult> =
454         CompletableFuture<Instrumentation.ActivityResult>().also {
455             activityScenario =
456                 ActivityScenario.launch(StartForFutureActivity::class.java).onActivity { activity ->
457                     activity.startActivityForFuture(intent, it)
458                 }
459         }
460 
461     open fun enableComponent(component: ComponentName) {
462         packageManager.setComponentEnabledSetting(
463             component,
464             PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
465             PackageManager.DONT_KILL_APP
466         )
467     }
468 
469     open fun disableComponent(component: ComponentName) {
470         packageManager.setComponentEnabledSetting(
471             component,
472             PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
473             PackageManager.DONT_KILL_APP
474         )
475     }
476 
477     private fun createPackageInstallerSession(
478         packageSource: Int? = null,
479         allowlistedRestrictedPermissions: Set<String>? = null
480     ): Pair<Int, PackageInstaller.Session> {
481         // Create session
482         val sessionParam = SessionParams(SessionParams.MODE_FULL_INSTALL)
483         allowlistedRestrictedPermissions?.let {
484             sessionParam.setWhitelistedRestrictedPermissions(it)
485         }
486 
487         if (packageSource != null) {
488             sessionParam.setPackageSource(packageSource)
489         }
490 
491         val sessionId = packageInstaller.createSession(sessionParam)
492         val session = packageInstaller.openSession(sessionId)!!
493 
494         return Pair(sessionId, session)
495     }
496 
497     private fun writePackageInstallerSession(session: PackageInstaller.Session, apkName: String) {
498         val apkFile = File(APK_DIRECTORY, apkName)
499         // Write data to session
500         apkFile.inputStream().use { fileOnDisk ->
501             session
502                 .openWrite(/* name= */ apkName, /* offsetBytes= */ 0, /* lengthBytes= */ -1)
503                 .use { sessionFile -> fileOnDisk.copyTo(sessionFile) }
504         }
505     }
506 
507     private fun commitPackageInstallerSession(session: PackageInstaller.Session) {
508         // PendingIntent that triggers a INSTALL_ACTION_CALLBACK broadcast that gets received by
509         // installSessionResultReceiver when install actions occur with this session
510         val installActionPendingIntent =
511             PendingIntent.getBroadcast(
512                 context,
513                 0,
514                 Intent(INSTALL_ACTION_CALLBACK).setPackage(context.packageName),
515                 FLAG_UPDATE_CURRENT or FLAG_MUTABLE
516             )
517         session.commit(installActionPendingIntent.intentSender)
518     }
519 
520     private fun setAppMetadata(session: PackageInstaller.Session, data: PersistableBundle) {
521         try {
522             session.setAppMetadata(data)
523         } catch (e: Exception) {
524             session.abandon()
525             throw e
526         }
527     }
528 
529     /** Wait for session's install result and return it */
530     private fun getInstallSessionResult(timeout: Long = PACKAGE_INSTALLER_TIMEOUT): SessionResult {
531         return installSessionResult.poll(timeout, TimeUnit.MILLISECONDS)
532             ?: SessionResult(null /* status */)
533     }
534 }
535