<lambda>null1 package android.companion.cts.uiautomation
2
3 import android.Manifest
4 import android.annotation.CallSuper
5 import android.app.Activity
6 import android.app.Activity.RESULT_CANCELED
7 import android.app.role.RoleManager
8 import android.companion.AssociationInfo
9 import android.companion.AssociationRequest
10 import android.companion.BluetoothDeviceFilter
11 import android.companion.BluetoothDeviceFilterUtils
12 import android.companion.CompanionDeviceManager
13 import android.companion.CompanionDeviceManager.REASON_USER_REJECTED
14 import android.companion.CompanionDeviceManager.REASON_DISCOVERY_TIMEOUT
15 import android.companion.CompanionDeviceManager.REASON_CANCELED
16 import android.companion.CompanionDeviceManager.RESULT_USER_REJECTED
17 import android.companion.CompanionDeviceManager.RESULT_DISCOVERY_TIMEOUT
18 import android.companion.DeviceFilter
19 import android.companion.cts.common.CompanionActivity
20 import android.companion.cts.common.DEVICE_PROFILES
21 import android.companion.cts.common.DEVICE_PROFILE_TO_NAME
22 import android.companion.cts.common.DEVICE_PROFILE_TO_PERMISSION
23 import android.companion.cts.common.RecordingCallback
24 import android.companion.cts.common.RecordingCallback.OnAssociationCreated
25 import android.companion.cts.common.RecordingCallback.OnAssociationPending
26 import android.companion.cts.common.RecordingCallback.OnFailure
27 import android.companion.cts.common.SIMPLE_EXECUTOR
28 import android.companion.cts.common.TestBase
29 import android.companion.cts.common.assertEmpty
30 import android.companion.cts.common.setSystemProp
31 import android.content.Intent
32 import android.net.MacAddress
33 import android.os.Parcelable
34 import android.os.SystemClock.sleep
35 import androidx.test.uiautomator.UiDevice
36 import org.junit.Assume
37 import org.junit.Assume.assumeFalse
38 import java.util.regex.Pattern
39 import kotlin.test.assertContains
40 import kotlin.test.assertContentEquals
41 import kotlin.test.assertEquals
42 import kotlin.test.assertIs
43 import kotlin.test.assertNotNull
44 import kotlin.time.Duration
45 import kotlin.time.Duration.Companion.ZERO
46 import kotlin.time.Duration.Companion.seconds
47
48 open class UiAutomationTestBase(
49 protected val profile: String?,
50 private val profilePermission: String?
51 ) : TestBase() {
52 private val roleManager: RoleManager by lazy {
53 context.getSystemService(RoleManager::class.java)!!
54 }
55
56 private val uiDevice: UiDevice by lazy { UiDevice.getInstance(instrumentation) }
57 protected val confirmationUi by lazy { CompanionDeviceManagerUi(uiDevice) }
58 protected val callback by lazy { RecordingCallback() }
59
60 @CallSuper
61 override fun setUp() {
62 super.setUp()
63
64 assumeFalse(confirmationUi.isVisible)
65 Assume.assumeTrue(CompanionActivity.waitUntilGone())
66 uiDevice.waitForIdle()
67
68 callback.clearRecordedInvocations()
69
70 // Make RoleManager bypass role qualification, which would allow this self-instrumenting
71 // test package to hold "systemOnly"" CDM roles (e.g. COMPANION_DEVICE_APP_STREAMING and
72 // SYSTEM_AUTOMOTIVE_PROJECTION)
73 withShellPermissionIdentity { roleManager.isBypassingRoleQualification = true }
74 }
75
76 @CallSuper
77 override fun tearDown() {
78 // If the profile (role) is not null: remove the app from the role holders.
79 // Do it via Shell (using the targetApp) because RoleManager takes way too many arguments.
80 profile?.let { roleName -> targetApp.removeFromHoldersOfRole(roleName) }
81
82 // Restore disallowing role qualifications.
83 withShellPermissionIdentity { roleManager.isBypassingRoleQualification = false }
84
85 CompanionActivity.safeFinish()
86 confirmationUi.dismiss()
87
88 restoreDiscoveryTimeout()
89
90 super.tearDown()
91 }
92
93 protected fun test_userRejected(
94 singleDevice: Boolean = false,
95 selfManaged: Boolean = false,
96 displayName: String? = null
97 ) = test_cancelled(singleDevice, selfManaged, userRejected = true, displayName) {
98 // User "rejects" the request.
99 if (singleDevice || selfManaged) {
100 confirmationUi.clickNegativeButton()
101 } else {
102 confirmationUi.clickNegativeButtonMultipleDevices()
103 }
104 }
105
106 protected fun test_userDismissed(
107 singleDevice: Boolean = false,
108 selfManaged: Boolean = false,
109 displayName: String? = null
110 ) = test_cancelled(singleDevice, selfManaged, userRejected = false, displayName) {
111 // User "dismisses" the request.
112 uiDevice.pressBack()
113 }
114
115 private fun test_cancelled(
116 singleDevice: Boolean,
117 selfManaged: Boolean,
118 userRejected: Boolean,
119 displayName: String?,
120 cancelAction: () -> Unit
121 ) {
122 // Give the discovery service extra time to find the first match device before
123 // pressing the negative button for singleDevice && userRejected.
124 if (singleDevice) {
125 setDiscoveryTimeout(2.seconds)
126 }
127
128 sendRequestAndLaunchConfirmation(singleDevice, selfManaged, displayName)
129
130 if (singleDevice) {
131 // The discovery timeout is 2 sec, but let's wait for 3. So that we have enough
132 // time to wait until the dialog appeared.
133 sleep(3.seconds.inWholeMilliseconds)
134 }
135 // Test can stop here since there's no device found after discovery timeout.
136 assumeFalse(callback.invocations.contains(OnFailure(REASON_DISCOVERY_TIMEOUT)))
137 // Check callback invocations: There should be 0 invocation before any actions are made.
138 assertEmpty(callback.invocations)
139
140 callback.assertInvokedByActions {
141 cancelAction()
142 }
143 // Check callback invocations: there should have been exactly 1 invocation of the
144 // onFailure() method.
145 val expectedError = if (userRejected) REASON_USER_REJECTED else REASON_CANCELED
146 assertContentEquals(
147 actual = callback.invocations,
148 expected = listOf(OnFailure(expectedError))
149 )
150 // Wait until the Confirmation UI goes away.
151 confirmationUi.waitUntilGone()
152
153 // Check the result code delivered via onActivityResult()
154 val (resultCode: Int, _) = CompanionActivity.waitForActivityResult()
155 val expectedResultCode = if (userRejected) RESULT_USER_REJECTED else RESULT_CANCELED
156 assertEquals(actual = resultCode, expected = expectedResultCode)
157 // Make sure no Associations were created.
158 assertEmpty(cdm.myAssociations)
159 }
160
161 protected fun test_timeout(singleDevice: Boolean = false) {
162 // Set discovery timeout to 2 seconds to avoid flaky that
163 // there's a chance CDM UI is disappeared before waitUntilVisible
164 // is called.
165 setDiscoveryTimeout(2.seconds)
166
167 callback.assertInvokedByActions(2.seconds) {
168 // Make sure no device will match the request
169 sendRequestAndLaunchConfirmation(
170 singleDevice = singleDevice,
171 deviceFilter = UNMATCHABLE_BT_FILTER
172 )
173 }
174
175 // Check callback invocations: there should have been exactly 1 invocation of the
176 // onFailure() method.
177 assertContentEquals(
178 actual = callback.invocations,
179 expected = listOf(OnFailure(REASON_DISCOVERY_TIMEOUT))
180 )
181
182 // Wait until the Confirmation UI goes away.
183 confirmationUi.waitUntilGone()
184
185 // Check the result code delivered via onActivityResult()
186 val (resultCode: Int, _) = CompanionActivity.waitForActivityResult()
187 assertEquals(actual = resultCode, expected = RESULT_DISCOVERY_TIMEOUT)
188
189 // Make sure no Associations were created.
190 assertEmpty(cdm.myAssociations)
191 }
192
193 protected fun test_userConfirmed_foundDevice(
194 singleDevice: Boolean,
195 confirmationAction: () -> Unit
196 ) {
197 sendRequestAndLaunchConfirmation(singleDevice = singleDevice)
198
199 callback.assertInvokedByActions {
200 confirmationAction()
201 }
202 // Check callback invocations: there should have been exactly 1 invocation of the
203 // OnAssociationCreated() method.
204 assertEquals(1, callback.invocations.size)
205 val associationInvocation = callback.invocations.first()
206 assertIs<OnAssociationCreated>(associationInvocation)
207 val associationFromCallback = associationInvocation.associationInfo
208
209 // Wait until the Confirmation UI goes away.
210 confirmationUi.waitUntilGone()
211
212 // Check the result code and the data delivered via onActivityResult()
213 val (resultCode: Int, data: Intent?) = CompanionActivity.waitForActivityResult()
214 assertEquals(actual = resultCode, expected = Activity.RESULT_OK)
215 assertNotNull(data)
216 val associationFromActivityResult: AssociationInfo? =
217 data.getParcelableExtra(CompanionDeviceManager.EXTRA_ASSOCIATION)
218 assertNotNull(associationFromActivityResult)
219 // Check that the association reported back via the callback same as the association
220 // delivered via onActivityResult().
221 assertEquals(associationFromCallback, associationFromActivityResult)
222
223 // Make sure "device data" was included (for backwards compatibility), and that the
224 // MAC address extracted from this data matches the MAC address from AssociationInfo.
225 val deviceFromActivityResult: Parcelable? =
226 data.getParcelableExtra(CompanionDeviceManager.EXTRA_DEVICE)
227 assertNotNull(deviceFromActivityResult)
228
229 val deviceMacAddress =
230 BluetoothDeviceFilterUtils.getDeviceMacAddress(deviceFromActivityResult)
231 assertEquals(actual = MacAddress.fromString(deviceMacAddress),
232 expected = associationFromCallback.deviceMacAddress)
233
234 // Make sure getMyAssociations() returns the same association we received via the callback
235 // as well as in onActivityResult()
236 assertContentEquals(actual = cdm.myAssociations, expected = listOf(associationFromCallback))
237
238 // Make sure that the role (for the current CDM device profile) was granted.
239 assertIsProfileRoleHolder()
240 }
241
242 protected fun sendRequestAndLaunchConfirmation(
243 singleDevice: Boolean = false,
244 selfManaged: Boolean = false,
245 displayName: String? = null,
246 deviceFilter: DeviceFilter<*>? = null
247 ) {
248 val request = AssociationRequest.Builder()
249 .apply {
250 // Set the single-device flag.
251 setSingleDevice(singleDevice)
252
253 // Set the self-managed flag.
254 setSelfManaged(selfManaged)
255
256 // Set profile if not null.
257 profile?.let { setDeviceProfile(it) }
258
259 // Set display name if not null.
260 displayName?.let { setDisplayName(it) }
261
262 // Add device filter if not null.
263 deviceFilter?.let { addDeviceFilter(it) }
264 }
265 .build()
266 callback.clearRecordedInvocations()
267
268 callback.assertInvokedByActions {
269 // If the REQUEST_COMPANION_SELF_MANAGED and/or the profile permission is required:
270 // run with these permissions as the Shell;
271 // otherwise: just call associate().
272 with(getRequiredPermissions(selfManaged)) {
273 if (isNotEmpty()) {
274 withShellPermissionIdentity(*toTypedArray()) {
275 cdm.associate(request, SIMPLE_EXECUTOR, callback)
276 }
277 } else {
278 cdm.associate(request, SIMPLE_EXECUTOR, callback)
279 }
280 }
281 }
282 // Check callback invocations: there should have been exactly 1 invocation of the
283 // onAssociationPending() method.
284
285 assertEquals(1, callback.invocations.size)
286 val associationInvocation = callback.invocations.first()
287 assertIs<OnAssociationPending>(associationInvocation)
288
289 // Get intent sender and clear callback invocations.
290 val pendingConfirmation = associationInvocation.intentSender
291 callback.clearRecordedInvocations()
292
293 // Launch CompanionActivity, and then launch confirmation UI from it.
294 CompanionActivity.launchAndWait(context)
295 CompanionActivity.startIntentSender(pendingConfirmation)
296
297 confirmationUi.waitUntilVisible()
298 }
299
300 /**
301 * If the current CDM Device [profile] is not null, check that the application was "granted"
302 * the corresponding role (all CDM device profiles are "backed up" by roles).
303 */
304 protected fun assertIsProfileRoleHolder() = profile?.let { roleName ->
305 val roleHolders = withShellPermissionIdentity(Manifest.permission.MANAGE_ROLE_HOLDERS) {
306 roleManager.getRoleHolders(roleName)
307 }
308 assertContains(roleHolders, targetPackageName, "Not a holder of $roleName")
309 }
310
311 private fun getRequiredPermissions(selfManaged: Boolean): List<String> =
312 mutableListOf<String>().also {
313 if (selfManaged) it += Manifest.permission.REQUEST_COMPANION_SELF_MANAGED
314 if (profilePermission != null) it += profilePermission
315 }
316
317 private fun setDiscoveryTimeout(timeout: Duration) =
318 instrumentation.setSystemProp(
319 SYS_PROP_DEBUG_TIMEOUT,
320 timeout.inWholeMilliseconds.toString()
321 )
322
323 private fun restoreDiscoveryTimeout() = setDiscoveryTimeout(ZERO)
324
325 companion object {
326 /**
327 * List of (profile, permission, name) tuples that represent all supported profiles and
328 * null.
329 */
330 @JvmStatic
331 protected fun supportedProfilesAndNull() = mutableListOf<Array<String?>>().apply {
332 add(arrayOf(null, null, "null"))
333 addAll(supportedProfiles())
334 }
335
336 /** List of (profile, permission, name) tuples that represent all supported profiles. */
337 private fun supportedProfiles(): Collection<Array<String?>> = DEVICE_PROFILES.map {
338 profile ->
339 arrayOf(profile,
340 DEVICE_PROFILE_TO_PERMISSION[profile]!!,
341 DEVICE_PROFILE_TO_NAME[profile]!!)
342 }
343
344 private val UNMATCHABLE_BT_FILTER = BluetoothDeviceFilter.Builder()
345 .setAddress("FF:FF:FF:FF:FF:FF")
346 .setNamePattern(Pattern.compile("This Device Does Not Exist"))
347 .build()
348
349 private const val SYS_PROP_DEBUG_TIMEOUT = "debug.cdm.discovery_timeout"
350 }
351 }