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