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