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