• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download

<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 }