1 /* <lambda>null2 * Copyright (C) 2022 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.companion.cts.uiautomation 18 19 import android.Manifest 20 import android.annotation.CallSuper 21 import android.app.Activity 22 import android.app.Activity.RESULT_CANCELED 23 import android.app.role.RoleManager 24 import android.bluetooth.BluetoothAdapter 25 import android.bluetooth.BluetoothManager 26 import android.companion.AssociationInfo 27 import android.companion.AssociationRequest 28 import android.companion.BluetoothDeviceFilter 29 import android.companion.BluetoothDeviceFilterUtils 30 import android.companion.CompanionDeviceManager 31 import android.companion.CompanionDeviceManager.REASON_CANCELED 32 import android.companion.CompanionDeviceManager.REASON_USER_REJECTED 33 import android.companion.CompanionDeviceManager.RESULT_USER_REJECTED 34 import android.companion.DeviceFilter 35 import android.companion.Flags 36 import android.companion.cts.common.CompanionActivity 37 import android.companion.cts.common.DEVICE_PROFILES 38 import android.companion.cts.common.DEVICE_PROFILE_TO_NAME 39 import android.companion.cts.common.DEVICE_PROFILE_TO_PERMISSION 40 import android.companion.cts.common.RecordingCallback 41 import android.companion.cts.common.RecordingCallback.OnAssociationCreated 42 import android.companion.cts.common.RecordingCallback.OnAssociationPending 43 import android.companion.cts.common.RecordingCallback.OnFailure 44 import android.companion.cts.common.RecordingCallback.OnFailureCode 45 import android.companion.cts.common.SIMPLE_EXECUTOR 46 import android.companion.cts.common.TestBase 47 import android.companion.cts.common.assertEmpty 48 import android.companion.cts.common.waitFor 49 import android.companion.cts.uicommon.CompanionDeviceManagerUi 50 import android.content.Intent 51 import android.graphics.drawable.Icon 52 import android.net.MacAddress 53 import android.os.Parcelable 54 import android.os.SystemClock.sleep 55 import androidx.test.uiautomator.UiDevice 56 import java.util.regex.Pattern 57 import kotlin.test.assertContains 58 import kotlin.test.assertContentEquals 59 import kotlin.test.assertEquals 60 import kotlin.test.assertIs 61 import kotlin.test.assertNotNull 62 import kotlin.test.assertTrue 63 import kotlin.time.Duration.Companion.ZERO 64 import kotlin.time.Duration.Companion.milliseconds 65 import kotlin.time.Duration.Companion.seconds 66 import org.junit.AfterClass 67 import org.junit.Assume 68 import org.junit.Assume.assumeFalse 69 import org.junit.BeforeClass 70 71 open class UiAutomationTestBase( 72 protected val profile: String?, 73 private val profilePermission: String? 74 ) : TestBase() { 75 private val roleManager: RoleManager by lazy { 76 context.getSystemService(RoleManager::class.java)!! 77 } 78 79 val uiDevice: UiDevice = UiDevice.getInstance(instrumentation) 80 81 // CDM discovery requires bluetooth is enabled, enable the location if it was disabled. 82 var bluetoothWasEnabled: Boolean = false 83 protected val confirmationUi = CompanionDeviceManagerUi(uiDevice) 84 protected val callback by lazy { RecordingCallback() } 85 private val bluetoothManager = context.getSystemService(BluetoothManager::class.java)!! 86 private val bluetoothAdapter: BluetoothAdapter = bluetoothManager.adapter 87 88 @CallSuper 89 override fun setUp() { 90 super.setUp() 91 92 assumeFalse(confirmationUi.isVisible) 93 Assume.assumeTrue(CompanionActivity.waitUntilGone()) 94 95 uiDevice.waitForIdle() 96 97 callback.clearRecordedInvocations() 98 99 // Make RoleManager bypass role qualification, which would allow this self-instrumenting 100 // test package to hold "systemOnly"" CDM roles (e.g. COMPANION_DEVICE_APP_STREAMING and 101 // SYSTEM_AUTOMOTIVE_PROJECTION) 102 withShellPermissionIdentity { roleManager.isBypassingRoleQualification = true } 103 } 104 105 @CallSuper 106 override fun tearDown() { 107 // If the profile (role) is not null: remove the app from the role holders. 108 // Do it via Shell (using the targetApp) because RoleManager takes way too many arguments. 109 profile?.let { roleName -> targetApp.removeFromHoldersOfRole(roleName) } 110 111 // Restore disallowing role qualifications. 112 withShellPermissionIdentity { roleManager.isBypassingRoleQualification = false } 113 114 CompanionActivity.safeFinish() 115 CompanionActivity.waitUntilGone() 116 117 confirmationUi.dismiss() 118 confirmationUi.waitUntilGone() 119 120 restoreDiscoveryTimeout() 121 122 super.tearDown() 123 } 124 125 protected fun test_userRejected( 126 singleDevice: Boolean = false, 127 selfManaged: Boolean = false, 128 timeout: Boolean = false, 129 displayName: String? = null 130 ) = test_canceled(singleDevice, selfManaged, timeout, userRejected = true, displayName) 131 132 protected fun test_userDismissed( 133 singleDevice: Boolean = false, 134 selfManaged: Boolean = false, 135 timeout: Boolean = false, 136 displayName: String? = null 137 ) = test_canceled(singleDevice, selfManaged, timeout, userRejected = false, displayName) 138 139 private fun test_canceled( 140 singleDevice: Boolean, 141 selfManaged: Boolean, 142 timeout: Boolean, 143 userRejected: Boolean, 144 displayName: String? 145 ) { 146 if (!singleDevice && profile == null && userRejected && !timeout) { 147 // Multi-device association flow for null-profile does not have a dedicated user 148 // consent prompt after the device selection, so it cannot be rejected. 149 return 150 } 151 152 fun cancelAction() { 153 if (!userRejected) { 154 // User "dismisses" the request. 155 uiDevice.pressBack() 156 } else if (timeout && !selfManaged) { 157 // User "cancels" the device discovery (may or may not have timed out) 158 confirmationUi.clickNegativeButtonMultipleDevices() 159 } else { 160 // User "rejects" the association confirmation prompt 161 confirmationUi.scrollToBottom() 162 sleep(1.seconds.inWholeMilliseconds) 163 confirmationUi.clickNegativeButton() 164 } 165 } 166 167 // Give the discovery service extra time to find the first match device before 168 // pressing the negative button for singleDevice && userRejected. 169 if (singleDevice || timeout) { 170 setSystemPropertyDuration(2.seconds, SYS_PROP_DEBUG_DISCOVERY_TIMEOUT) 171 } 172 173 val deviceFilter = if (timeout) UNMATCHABLE_BT_FILTER else null 174 sendRequestAndLaunchConfirmation(singleDevice, selfManaged, displayName, deviceFilter) 175 176 if (!timeout) { 177 // Wait until dialog proceeds to association confirmation 178 if (singleDevice) { 179 // The discovery timeout is 2 sec, but let's wait for 3. So that we have enough 180 // time to wait until the dialog appeared. 181 sleep(3.seconds.inWholeMilliseconds) 182 } else if (!selfManaged && profile != null) { 183 // Click the first found device for multiple device dialog 184 confirmationUi.waitAndClickOnFirstFoundDevice() 185 } 186 187 // If device profile exists, permissions list must be scrolled to enable the buttons 188 if (profile != null) { 189 confirmationUi.scrollToBottom() 190 } 191 } 192 193 // Check callback invocations: There should be 0 invocation before any actions are made. 194 assertEmpty(callback.invocations) 195 196 val expectedError = if (userRejected) REASON_USER_REJECTED else REASON_CANCELED 197 val expectedResultCode = if (userRejected) RESULT_USER_REJECTED else RESULT_CANCELED 198 199 if (Flags.associationFailureCode()) { 200 // Check callback invocations: there should have been exactly 2 invocation of the 201 // onFailure(CharSequence) and onFailure(Int) method. 202 callback.assertInvokedByActions (minOccurrences = 2) { 203 cancelAction() 204 } 205 206 assertTrue { 207 callback.invocations.contains(OnFailure(expectedError)) && 208 callback.invocations.contains( 209 OnFailureCode(expectedResultCode, expectedError)) 210 } 211 } else { 212 // Check callback invocations: there should have been exactly 1 invocation of the 213 // onFailure() method. 214 callback.assertInvokedByActions { 215 cancelAction() 216 } 217 assertContentEquals( 218 actual = callback.invocations, 219 expected = listOf(OnFailure(expectedError)) 220 ) 221 } 222 223 // Wait until the Confirmation UI goes away. 224 confirmationUi.waitUntilGone() 225 226 // Check the result code delivered via onActivityResult() 227 val (resultCode: Int, _) = CompanionActivity.waitForActivityResult() 228 assertEquals(actual = resultCode, expected = expectedResultCode) 229 // Make sure no Associations were created. 230 assertEmpty(cdm.myAssociations) 231 } 232 233 protected fun test_timeout(singleDevice: Boolean = false) { 234 // Set discovery timeout to 2 seconds to avoid flaky that 235 // there's a chance CDM UI is disappeared before waitUntilVisible 236 // is called. 237 setSystemPropertyDuration(2.seconds, SYS_PROP_DEBUG_DISCOVERY_TIMEOUT) 238 239 // Make sure no device will match the request 240 sendRequestAndLaunchConfirmation( 241 singleDevice = singleDevice, 242 deviceFilter = UNMATCHABLE_BT_FILTER 243 ) 244 245 sleep(2.seconds.inWholeMilliseconds) // Wait for discovery to timeout 246 confirmationUi.waitUntilTimeoutMessageVisible() 247 248 // Make sure cancel button is available and click it 249 confirmationUi.clickNegativeButtonMultipleDevices() 250 assertEmpty(cdm.myAssociations) // No associations were created 251 } 252 253 protected fun test_userConfirmed_foundDevice( 254 singleDevice: Boolean, 255 confirmationAction: () -> Unit 256 ) { 257 sendRequestAndLaunchConfirmation(singleDevice = singleDevice) 258 259 if (profile != null) { 260 if (singleDevice) { 261 confirmationUi.scrollToBottom() 262 callback.assertInvokedByActions { 263 confirmationAction() 264 } 265 } else { 266 // First, select the device in the device chooser dialog. 267 confirmationUi.waitAndClickOnFirstFoundDevice() 268 // Second, wait until the permissionList dialog shows up and scroll to the bottom. 269 confirmationUi.scrollToBottom() 270 // Third, tap the `Allow` bottom. 271 callback.assertInvokedByActions { 272 confirmationUi.waitUntilPositiveButtonIsEnabledAndClick() 273 } 274 } 275 } else { 276 if (singleDevice) { 277 confirmationUi.scrollToBottom() 278 } 279 callback.assertInvokedByActions { 280 confirmationAction() 281 } 282 } 283 284 // Check callback invocations: there should have been exactly 1 invocation of the 285 // OnAssociationCreated() method. 286 assertEquals(1, callback.invocations.size) 287 val associationInvocation = callback.invocations.first() 288 assertIs<OnAssociationCreated>(associationInvocation) 289 val associationFromCallback = associationInvocation.associationInfo 290 291 // Wait until the Confirmation UI goes away. 292 confirmationUi.waitUntilGone() 293 294 // Check the result code and the data delivered via onActivityResult() 295 val (resultCode: Int, data: Intent?) = CompanionActivity.waitForActivityResult() 296 assertEquals(actual = resultCode, expected = Activity.RESULT_OK) 297 assertNotNull(data) 298 val associationFromActivityResult: AssociationInfo? = data.getParcelableExtra( 299 CompanionDeviceManager.EXTRA_ASSOCIATION, 300 AssociationInfo::class.java 301 ) 302 assertNotNull(associationFromActivityResult) 303 // Check that the association reported back via the callback same as the association 304 // delivered via onActivityResult(). 305 assertEquals(associationFromCallback, associationFromActivityResult) 306 307 // Make sure "device data" was included (for backwards compatibility) 308 val deviceFromActivityResult = associationFromActivityResult.associatedDevice 309 assertNotNull(deviceFromActivityResult) 310 311 // At least one of three types of devices is not null and MAC address from this data 312 // matches the MAC address from AssociationInfo 313 val deviceData: Parcelable = listOf( 314 deviceFromActivityResult.bluetoothDevice, 315 deviceFromActivityResult.bleDevice, 316 deviceFromActivityResult.wifiDevice 317 ).firstNotNullOf { it } 318 assertNotNull(deviceData) 319 val deviceMacAddress = BluetoothDeviceFilterUtils.getDeviceMacAddress(deviceData) 320 assertEquals( 321 actual = MacAddress.fromString(deviceMacAddress), 322 expected = associationFromCallback.deviceMacAddress 323 ) 324 325 // Make sure getMyAssociations() returns the same association we received via the callback 326 // as well as in onActivityResult() 327 assertContentEquals(actual = cdm.myAssociations, expected = listOf(associationFromCallback)) 328 329 // Make sure that the role (for the current CDM device profile) was granted. 330 assertIsProfileRoleHolder() 331 } 332 333 protected fun sendRequestAndLaunchConfirmation( 334 singleDevice: Boolean = false, 335 selfManaged: Boolean = false, 336 displayName: String? = null, 337 deviceFilter: DeviceFilter<*>? = null 338 ) { 339 val request = AssociationRequest.Builder() 340 .apply { 341 // Set the single-device flag. 342 setSingleDevice(singleDevice) 343 344 // Set the self-managed flag. 345 setSelfManaged(selfManaged) 346 347 // Set profile if not null. 348 profile?.let { setDeviceProfile(it) } 349 350 // Set display name if not null. 351 displayName?.let { setDisplayName(it) } 352 353 // Add device filter if not null. 354 deviceFilter?.let { addDeviceFilter(it) } 355 356 if (Flags.associationDeviceIcon() && selfManaged) { 357 setDeviceIcon( 358 Icon.createWithResource(context, R.drawable.ic_cts_device_icon) 359 ) 360 } 361 } 362 .build() 363 callback.clearRecordedInvocations() 364 365 callback.assertInvokedByActions { 366 // If the REQUEST_COMPANION_SELF_MANAGED and/or the profile permission is required: 367 // run with these permissions as the Shell; 368 // otherwise: just call associate(). 369 with(getRequiredPermissions(selfManaged)) { 370 if (isNotEmpty()) { 371 withShellPermissionIdentity(*toTypedArray()) { 372 cdm.associate(request, SIMPLE_EXECUTOR, callback) 373 } 374 } else { 375 cdm.associate(request, SIMPLE_EXECUTOR, callback) 376 } 377 } 378 } 379 // Check callback invocations: there should have been exactly 1 invocation of the 380 // onAssociationPending() method. 381 382 assertEquals(1, callback.invocations.size) 383 val associationInvocation = callback.invocations.first() 384 assertIs<OnAssociationPending>(associationInvocation) 385 386 // Get intent sender and clear callback invocations. 387 val pendingConfirmation = associationInvocation.intentSender 388 callback.clearRecordedInvocations() 389 390 // Launch CompanionActivity, and then launch confirmation UI from it. 391 CompanionActivity.launchAndWait(context) 392 CompanionActivity.startIntentSender(pendingConfirmation) 393 394 confirmationUi.waitUntilVisible() 395 } 396 397 /** 398 * If the current CDM Device [profile] is not null, check that the application was "granted" 399 * the corresponding role (all CDM device profiles are "backed up" by roles). 400 */ 401 protected fun assertIsProfileRoleHolder() = profile?.let { roleName -> 402 val roleHolders = withShellPermissionIdentity(Manifest.permission.MANAGE_ROLE_HOLDERS) { 403 roleManager.getRoleHolders(roleName) 404 } 405 assertContains(roleHolders, targetPackageName, "Not a holder of $roleName") 406 } 407 408 protected fun getRequiredPermissions(selfManaged: Boolean): List<String> = 409 mutableListOf<String>().also { 410 if (selfManaged) it += Manifest.permission.REQUEST_COMPANION_SELF_MANAGED 411 if (profilePermission != null) it += profilePermission 412 } 413 414 private fun restoreDiscoveryTimeout() = setSystemPropertyDuration( 415 ZERO, 416 SYS_PROP_DEBUG_DISCOVERY_TIMEOUT 417 ) 418 419 fun enableBluetoothIfNeeded() { 420 bluetoothWasEnabled = bluetoothAdapter.isEnabled 421 if (!bluetoothWasEnabled) { 422 runShellCommand("svc bluetooth enable") 423 val result = waitFor(timeout = 5.seconds, interval = 100.milliseconds) { 424 bluetoothAdapter.isEnabled 425 } 426 assumeFalse("Not able to enable the bluetooth", !result) 427 } 428 } 429 430 fun disableBluetoothIfNeeded() { 431 if (!bluetoothWasEnabled) { 432 runShellCommand("svc bluetooth disable") 433 val result = waitFor(timeout = 5.seconds, interval = 100.milliseconds) { 434 !bluetoothAdapter.isEnabled 435 } 436 assumeFalse("Not able to disable the bluetooth", !result) 437 } 438 } 439 440 companion object { 441 /** 442 * List of (profile, permission, name) tuples that represent all supported profiles and 443 * null. 444 */ 445 @JvmStatic 446 protected fun supportedProfilesAndNull() = mutableListOf<Array<String?>>().apply { 447 add(arrayOf(null, null, "null")) 448 addAll(supportedProfiles()) 449 } 450 451 /** List of (profile, permission, name) tuples that represent all supported profiles. */ 452 private fun supportedProfiles(): Collection<Array<String?>> = DEVICE_PROFILES.map { 453 profile -> 454 arrayOf( 455 profile, 456 DEVICE_PROFILE_TO_PERMISSION[profile]!!, 457 DEVICE_PROFILE_TO_NAME[profile]!! 458 ) 459 } 460 461 private val UNMATCHABLE_BT_FILTER = BluetoothDeviceFilter.Builder() 462 .setAddress("FF:FF:FF:FF:FF:FF") 463 .setNamePattern(Pattern.compile("This Device Does Not Exist")) 464 .build() 465 466 private const val SYS_PROP_DEBUG_DISCOVERY_TIMEOUT = "debug.cdm.discovery_timeout" 467 468 @JvmStatic 469 @BeforeClass 470 fun setupBeforeClass() { 471 // Enable bluetooth if it was disabled. 472 val uiAutomationTestBase = UiAutomationTestBase(null, null) 473 uiAutomationTestBase.enableBluetoothIfNeeded() 474 } 475 476 @JvmStatic 477 @AfterClass 478 fun tearDownAfterClass() { 479 // Disable bluetooth if it was disabled. 480 val uiAutomationTestBase = UiAutomationTestBase(null, null) 481 uiAutomationTestBase.disableBluetoothIfNeeded() 482 } 483 } 484 } 485