• 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_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