• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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