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