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 // Second, wait until the permissionList dialog shows up and scroll to the bottom. 242 confirmationUi.scrollToBottom() 243 // Third, tap the `Allow` bottom. 244 callback.assertInvokedByActions { 245 confirmationUi.waitUntilPositiveButtonIsEnabledAndClick() 246 } 247 } 248 } else { 249 callback.assertInvokedByActions { 250 confirmationAction() 251 } 252 } 253 254 // Check callback invocations: there should have been exactly 1 invocation of the 255 // OnAssociationCreated() method. 256 assertEquals(1, callback.invocations.size) 257 val associationInvocation = callback.invocations.first() 258 assertIs<OnAssociationCreated>(associationInvocation) 259 val associationFromCallback = associationInvocation.associationInfo 260 261 // Wait until the Confirmation UI goes away. 262 confirmationUi.waitUntilGone() 263 264 // Check the result code and the data delivered via onActivityResult() 265 val (resultCode: Int, data: Intent?) = CompanionActivity.waitForActivityResult() 266 assertEquals(actual = resultCode, expected = Activity.RESULT_OK) 267 assertNotNull(data) 268 val associationFromActivityResult: AssociationInfo? = data.getParcelableExtra( 269 CompanionDeviceManager.EXTRA_ASSOCIATION, 270 AssociationInfo::class.java) 271 assertNotNull(associationFromActivityResult) 272 // Check that the association reported back via the callback same as the association 273 // delivered via onActivityResult(). 274 assertEquals(associationFromCallback, associationFromActivityResult) 275 276 // Make sure "device data" was included (for backwards compatibility) 277 val deviceFromActivityResult = associationFromActivityResult.associatedDevice 278 assertNotNull(deviceFromActivityResult) 279 280 // At least one of three types of devices is not null and MAC address from this data 281 // matches the MAC address from AssociationInfo 282 val deviceData: Parcelable = listOf( 283 deviceFromActivityResult.bluetoothDevice, 284 deviceFromActivityResult.bleDevice, 285 deviceFromActivityResult.wifiDevice 286 ).firstNotNullOf { it } 287 assertNotNull(deviceData) 288 val deviceMacAddress = BluetoothDeviceFilterUtils.getDeviceMacAddress(deviceData) 289 assertEquals(actual = MacAddress.fromString(deviceMacAddress), 290 expected = associationFromCallback.deviceMacAddress) 291 292 // Make sure getMyAssociations() returns the same association we received via the callback 293 // as well as in onActivityResult() 294 assertContentEquals(actual = cdm.myAssociations, expected = listOf(associationFromCallback)) 295 296 // Make sure that the role (for the current CDM device profile) was granted. 297 assertIsProfileRoleHolder() 298 } 299 300 protected fun sendRequestAndLaunchConfirmation( 301 singleDevice: Boolean = false, 302 selfManaged: Boolean = false, 303 displayName: String? = null, 304 deviceFilter: DeviceFilter<*>? = null 305 ) { 306 val request = AssociationRequest.Builder() 307 .apply { 308 // Set the single-device flag. 309 setSingleDevice(singleDevice) 310 311 // Set the self-managed flag. 312 setSelfManaged(selfManaged) 313 314 // Set profile if not null. 315 profile?.let { setDeviceProfile(it) } 316 317 // Set display name if not null. 318 displayName?.let { setDisplayName(it) } 319 320 // Add device filter if not null. 321 deviceFilter?.let { addDeviceFilter(it) } 322 } 323 .build() 324 callback.clearRecordedInvocations() 325 326 callback.assertInvokedByActions { 327 // If the REQUEST_COMPANION_SELF_MANAGED and/or the profile permission is required: 328 // run with these permissions as the Shell; 329 // otherwise: just call associate(). 330 with(getRequiredPermissions(selfManaged)) { 331 if (isNotEmpty()) { 332 withShellPermissionIdentity(*toTypedArray()) { 333 cdm.associate(request, SIMPLE_EXECUTOR, callback) 334 } 335 } else { 336 cdm.associate(request, SIMPLE_EXECUTOR, callback) 337 } 338 } 339 } 340 // Check callback invocations: there should have been exactly 1 invocation of the 341 // onAssociationPending() method. 342 343 assertEquals(1, callback.invocations.size) 344 val associationInvocation = callback.invocations.first() 345 assertIs<OnAssociationPending>(associationInvocation) 346 347 // Get intent sender and clear callback invocations. 348 val pendingConfirmation = associationInvocation.intentSender 349 callback.clearRecordedInvocations() 350 351 // Launch CompanionActivity, and then launch confirmation UI from it. 352 CompanionActivity.launchAndWait(context) 353 CompanionActivity.startIntentSender(pendingConfirmation) 354 355 confirmationUi.waitUntilVisible() 356 } 357 358 /** 359 * If the current CDM Device [profile] is not null, check that the application was "granted" 360 * the corresponding role (all CDM device profiles are "backed up" by roles). 361 */ 362 protected fun assertIsProfileRoleHolder() = profile?.let { roleName -> 363 val roleHolders = withShellPermissionIdentity(Manifest.permission.MANAGE_ROLE_HOLDERS) { 364 roleManager.getRoleHolders(roleName) 365 } 366 assertContains(roleHolders, targetPackageName, "Not a holder of $roleName") 367 } 368 369 protected fun getRequiredPermissions(selfManaged: Boolean): List<String> = 370 mutableListOf<String>().also { 371 if (selfManaged) it += Manifest.permission.REQUEST_COMPANION_SELF_MANAGED 372 if (profilePermission != null) it += profilePermission 373 } 374 375 private fun restoreDiscoveryTimeout() = setSystemPropertyDuration( 376 ZERO, SYS_PROP_DEBUG_DISCOVERY_TIMEOUT 377 ) 378 379 fun enableBluetoothIfNeeded() { 380 bluetoothWasEnabled = bluetoothAdapter.isEnabled 381 if (!bluetoothWasEnabled) { 382 runShellCommand("svc bluetooth enable") 383 val result = waitFor(timeout = 5.seconds, interval = 100.milliseconds) { 384 bluetoothAdapter.isEnabled 385 } 386 assumeFalse("Not able to enable the bluetooth", !result) 387 } 388 } 389 390 fun disableBluetoothIfNeeded() { 391 if (!bluetoothWasEnabled) { 392 runShellCommand("svc bluetooth disable") 393 val result = waitFor(timeout = 5.seconds, interval = 100.milliseconds) { 394 !bluetoothAdapter.isEnabled 395 } 396 assumeFalse("Not able to disable the bluetooth", !result) 397 } 398 } 399 400 companion object { 401 /** 402 * List of (profile, permission, name) tuples that represent all supported profiles and 403 * null. 404 */ 405 @JvmStatic 406 protected fun supportedProfilesAndNull() = mutableListOf<Array<String?>>().apply { 407 add(arrayOf(null, null, "null")) 408 addAll(supportedProfiles()) 409 } 410 411 /** List of (profile, permission, name) tuples that represent all supported profiles. */ 412 private fun supportedProfiles(): Collection<Array<String?>> = DEVICE_PROFILES.map { 413 profile -> 414 arrayOf(profile, 415 DEVICE_PROFILE_TO_PERMISSION[profile]!!, 416 DEVICE_PROFILE_TO_NAME[profile]!!) 417 } 418 419 private val UNMATCHABLE_BT_FILTER = BluetoothDeviceFilter.Builder() 420 .setAddress("FF:FF:FF:FF:FF:FF") 421 .setNamePattern(Pattern.compile("This Device Does Not Exist")) 422 .build() 423 424 private const val SYS_PROP_DEBUG_DISCOVERY_TIMEOUT = "debug.cdm.discovery_timeout" 425 426 @JvmStatic 427 @BeforeClass 428 fun setupBeforeClass() { 429 // Enable bluetooth if it was disabled. 430 val uiAutomationTestBase = UiAutomationTestBase(null, null) 431 uiAutomationTestBase.enableBluetoothIfNeeded() 432 } 433 434 @JvmStatic 435 @AfterClass 436 fun tearDownAfterClass() { 437 // Disable bluetooth if it was disabled. 438 val uiAutomationTestBase = UiAutomationTestBase(null, null) 439 uiAutomationTestBase.disableBluetoothIfNeeded() 440 } 441 } 442 } 443