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