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.permission.MANAGE_COMPANION_DEVICES
20 import android.annotation.CallSuper
21 import android.app.Activity.RESULT_CANCELED
22 import android.app.Activity.RESULT_OK
23 import android.companion.AssociationInfo
24 import android.companion.CompanionDeviceManager
25 import android.companion.CompanionException
26 import android.companion.Flags
27 import android.companion.cts.common.CompanionActivity
28 import android.companion.cts.uicommon.CompanionDeviceManagerUi.Companion.SYSTEM_DATA_TRANSFER_CONFIRMATION_UI
29 import android.content.Intent
30 import android.os.OutcomeReceiver
31 import android.platform.test.annotations.AppModeFull
32 import androidx.test.ext.junit.runners.AndroidJUnit4
33 import com.android.compatibility.common.util.FeatureUtil
34 import java.io.ByteArrayInputStream
35 import java.io.ByteArrayOutputStream
36 import java.io.PipedInputStream
37 import java.io.PipedOutputStream
38 import java.lang.IllegalStateException
39 import java.nio.ByteBuffer
40 import java.nio.charset.StandardCharsets
41 import java.util.concurrent.CountDownLatch
42 import java.util.concurrent.TimeUnit
43 import java.util.concurrent.TimeoutException
44 import java.util.concurrent.atomic.AtomicInteger
45 import java.util.concurrent.atomic.AtomicReference
46 import kotlin.test.assertEquals
47 import kotlin.test.assertFalse
48 import kotlin.test.assertNotNull
49 import kotlin.test.assertTrue
50 import libcore.util.EmptyArray
51 import org.junit.Assume.assumeFalse
52 import org.junit.Assume.assumeTrue
53 import org.junit.Ignore
54 import org.junit.Test
55 import org.junit.runner.RunWith
56
57 /**
58 * Tests the system data transfer.
59 *
60 * Build/Install/Run: atest CtsCompanionDeviceManagerUiAutomationTestCases:SystemDataTransferTest
61 */
62 @AppModeFull(reason = "CompanionDeviceManager APIs are not available to the instant apps.")
63 @RunWith(AndroidJUnit4::class)
64 class SystemDataTransferTest : UiAutomationTestBase(null, null) {
65 companion object {
66 private const val SYSTEM_DATA_TRANSFER_TIMEOUT = 10L // 10 seconds
67
68 private const val ACTION_CLICK_ALLOW = 1
69 private const val ACTION_CLICK_DISALLOW = 2
70 private const val ACTION_PRESS_BACK = 3
71 }
72
73 @CallSuper
74 override fun setUp() {
75 super.setUp()
76
77 assumeFalse(FeatureUtil.isWatch())
78
79 // Assume Permission Transfer is enabled, otherwise skip the test.
80 try {
81 val association = associate()
82 cdm.buildPermissionTransferUserConsentIntent(association.id)
83 true
84 } catch (e: UnsupportedOperationException) {
85 false
86 }.apply { assumeTrue("This test requires Permission Transfer to be enabled.", this) }
87
88 withShellPermissionIdentity(MANAGE_COMPANION_DEVICES) {
89 cdm.enableSecureTransport(false)
90 }
91 }
92
93 @CallSuper
94 override fun tearDown() {
95 withShellPermissionIdentity(MANAGE_COMPANION_DEVICES) {
96 cdm.enableSecureTransport(true)
97 }
98 super.tearDown()
99 }
100
101 @Test
102 fun test_userConsent_allow() {
103 val association1 = associate()
104
105 if (Flags.permSyncUserConsent()) {
106 assertFalse(cdm.isPermissionTransferUserConsented(association1.id))
107 }
108
109 val resultCode = requestPermissionTransferUserConsent(association1.id, ACTION_CLICK_ALLOW)
110
111 assertEquals(expected = RESULT_OK, actual = resultCode)
112 if (Flags.permSyncUserConsent()) {
113 assertTrue(cdm.isPermissionTransferUserConsented(association1.id))
114 }
115 }
116
117 @Test
118 fun test_userConsent_disallow() {
119 val association = associate()
120
121 if (Flags.permSyncUserConsent()) {
122 assertFalse(cdm.isPermissionTransferUserConsented(association.id))
123 }
124
125 val resultCode = requestPermissionTransferUserConsent(
126 association.id,
127 ACTION_CLICK_DISALLOW
128 )
129
130 assertEquals(expected = RESULT_CANCELED, actual = resultCode)
131 if (Flags.permSyncUserConsent()) {
132 assertFalse(cdm.isPermissionTransferUserConsented(association.id))
133 }
134 }
135
136 @Test
137 fun test_userConsent_cancel() {
138 val association = associate()
139
140 if (Flags.permSyncUserConsent()) {
141 assertFalse(cdm.isPermissionTransferUserConsented(association.id))
142 }
143
144 requestPermissionTransferUserConsent(association.id, ACTION_PRESS_BACK)
145
146 if (Flags.permSyncUserConsent()) {
147 assertFalse(cdm.isPermissionTransferUserConsented(association.id))
148 }
149 }
150
151 @Test
152 fun test_userConsent_allowThenDisallow() {
153 val association = associate()
154
155 if (Flags.permSyncUserConsent()) {
156 assertFalse(cdm.isPermissionTransferUserConsented(association.id))
157 }
158
159 val resultCode = requestPermissionTransferUserConsent(association.id, ACTION_CLICK_ALLOW)
160
161 assertEquals(expected = RESULT_OK, actual = resultCode)
162 if (Flags.permSyncUserConsent()) {
163 assertTrue(cdm.isPermissionTransferUserConsented(association.id))
164 }
165
166 val resultCode2 = requestPermissionTransferUserConsent(
167 association.id,
168 ACTION_CLICK_DISALLOW
169 )
170 assertEquals(expected = RESULT_CANCELED, actual = resultCode2)
171 if (Flags.permSyncUserConsent()) {
172 assertFalse(cdm.isPermissionTransferUserConsented(association.id))
173 }
174 }
175
176 @Test
177 fun test_userConsent_disallowThenAllow() {
178 val association = associate()
179
180 if (Flags.permSyncUserConsent()) {
181 assertFalse(cdm.isPermissionTransferUserConsented(association.id))
182 }
183
184 val resultCode = requestPermissionTransferUserConsent(association.id, ACTION_CLICK_DISALLOW)
185
186 assertEquals(expected = RESULT_CANCELED, actual = resultCode)
187 if (Flags.permSyncUserConsent()) {
188 assertFalse(cdm.isPermissionTransferUserConsented(association.id))
189 }
190
191 val resultCode2 = requestPermissionTransferUserConsent(association.id, ACTION_CLICK_ALLOW)
192 assertEquals(expected = RESULT_OK, actual = resultCode2)
193 if (Flags.permSyncUserConsent()) {
194 assertTrue(cdm.isPermissionTransferUserConsented(association.id))
195 }
196 }
197
198 /**
199 * Test that calling system data transfer API without first having acquired user consent
200 * results in triggering error callback.
201 */
202 @Test(expected = CompanionException::class)
203 fun test_startSystemDataTransfer_requiresUserConsent() {
204 val association = associate()
205
206 // Generate data packet with successful response
207 val response = generatePacket(MESSAGE_RESPONSE_SUCCESS, "SUCCESS")
208
209 // This will fail due to lack of user consent
210 startSystemDataTransfer(association.id, response)
211 }
212
213 /**
214 * Test that system data transfer triggers success callback when CDM receives successful
215 * response from the device whose permissions are being restored.
216 */
217 @Test
218 @Ignore("b/324260135")
219 fun test_startSystemDataTransfer_success() {
220 val association = associate()
221 requestPermissionTransferUserConsent(association.id, ACTION_CLICK_ALLOW)
222
223 // Generate data packet with successful response
224 val response = generatePacket(MESSAGE_RESPONSE_SUCCESS, "SUCCESS")
225 startSystemDataTransfer(association.id, response)
226 }
227
228 /**
229 * Test that system data transfer triggers error callback when CDM receives failure response
230 * from the device whose permissions are being restored.
231 */
232 @Test(expected = CompanionException::class)
233 @Ignore("b/324260135")
234 fun test_startSystemDataTransfer_failure() {
235 val association = associate()
236 requestPermissionTransferUserConsent(association.id, ACTION_CLICK_ALLOW)
237
238 // Generate data packet with failure as response
239 val response = generatePacket(MESSAGE_RESPONSE_FAILURE, "FAILURE")
240 startSystemDataTransfer(association.id, response)
241 }
242
243 /**
244 * Test that CDM sends a response to incoming request to restore permissions.
245 *
246 * This test uses a mock request with an empty body, so just assert that CDM sends any response.
247 */
248 @Test
249 fun test_receivePermissionRestore() {
250 assumeTrue(FeatureUtil.isWatch())
251
252 val association = associate()
253
254 // Generate data packet with permission restore request
255 val bytes = generatePacket(MESSAGE_REQUEST_PERMISSION_RESTORE)
256 val input = ByteArrayInputStream(bytes)
257
258 // Monitor output response from CDM
259 val messageSent = CountDownLatch(1)
260 val sentMessage = AtomicInteger()
261 val output = MonitoredOutputStream { message ->
262 sentMessage.set(message)
263 messageSent.countDown()
264 }
265
266 // "Receive" permission restore request
267 cdm.attachSystemDataTransport(association.id, input, output)
268
269 // Assert CDM sent a message
270 assertTrue(messageSent.await(SYSTEM_DATA_TRANSFER_TIMEOUT, TimeUnit.SECONDS))
271
272 // Assert that sent message was in response format (can be success or failure)
273 assertTrue(isResponse(sentMessage.get()))
274 }
275
276 /**
277 * Associate without checking the association data.
278 */
279 private fun associate(): AssociationInfo {
280 sendRequestAndLaunchConfirmation(singleDevice = true)
281 confirmationUi.scrollToBottom()
282 callback.assertInvokedByActions {
283 // User "approves" the request.
284 confirmationUi.clickPositiveButton()
285 }
286 // Wait until the Confirmation UI goes away.
287 confirmationUi.waitUntilGone()
288 // Check the result code and the data delivered via onActivityResult()
289 val (_: Int, associationData: Intent?) = CompanionActivity.waitForActivityResult()
290 assertNotNull(associationData)
291 val association: AssociationInfo? = associationData.getParcelableExtra(
292 CompanionDeviceManager.EXTRA_ASSOCIATION,
293 AssociationInfo::class.java
294 )
295 assertNotNull(association)
296
297 return association
298 }
299
300 /**
301 * Execute UI flow to request user consent for permission transfer for a given association
302 * and grant permission.
303 */
304 private fun requestPermissionTransferUserConsent(associationId: Int, action: Int): Int {
305 val pendingUserConsent = cdm.buildPermissionTransferUserConsentIntent(associationId)
306 CompanionActivity.startIntentSender(pendingUserConsent!!)
307 confirmationUi.waitUntilSystemDataTransferConfirmationVisible()
308 when (action) {
309 ACTION_CLICK_ALLOW -> {
310 confirmationUi.scrollToBottom(SYSTEM_DATA_TRANSFER_CONFIRMATION_UI)
311 confirmationUi.clickPositiveButton()
312 }
313 ACTION_CLICK_DISALLOW -> {
314 confirmationUi.scrollToBottom(SYSTEM_DATA_TRANSFER_CONFIRMATION_UI)
315 confirmationUi.clickNegativeButton()
316 }
317 ACTION_PRESS_BACK -> {
318 uiDevice.pressBack()
319 return -100 // an invalid result code which shouldn't be checked against
320 }
321 else -> throw IllegalStateException("Unknown action.")
322 }
323 val (resultCode: Int, _: Intent?) = CompanionActivity.waitForActivityResult()
324 return resultCode
325 }
326
327 /**
328 * Start system data transfer synchronously.
329 */
330 private fun startSystemDataTransfer(
331 associationId: Int,
332 simulatedResponse: ByteArray
333 ) {
334 // Piped input stream to simulate any response for CDM to receive
335 val inputSource = PipedOutputStream()
336 val pipedInput = PipedInputStream(inputSource)
337
338 // Only receive simulated response after permission restore request is sent
339 val monitoredOutput = MonitoredOutputStream { message ->
340 if (message == MESSAGE_REQUEST_PERMISSION_RESTORE) {
341 inputSource.write(simulatedResponse)
342 inputSource.flush()
343 }
344 }
345 cdm.attachSystemDataTransport(associationId, pipedInput, monitoredOutput)
346
347 // Synchronously start system data transfer
348 val transferFinished = CountDownLatch(1)
349 val err = AtomicReference<CompanionException>()
350 val callback = object : OutcomeReceiver<Void?, CompanionException> {
351 override fun onResult(result: Void?) {
352 transferFinished.countDown()
353 }
354
355 override fun onError(error: CompanionException) {
356 err.set(error)
357 transferFinished.countDown()
358 }
359 }
360 cdm.startSystemDataTransfer(associationId, context.mainExecutor, callback)
361
362 // Don't let it hang for too long!
363 if (!transferFinished.await(SYSTEM_DATA_TRANSFER_TIMEOUT, TimeUnit.SECONDS)) {
364 throw TimeoutException("System data transfer timed out.")
365 }
366
367 // Catch transfer failure
368 if (err.get() != null) {
369 throw err.get()
370 }
371
372 // Detach data transport
373 cdm.detachSystemDataTransport(associationId)
374 }
375 }
376
377 /**
378 * Message codes defined in [com.android.server.companion.transport.CompanionTransportManager].
379 */
380 private const val MESSAGE_RESPONSE_SUCCESS = 0x33838567
381 private const val MESSAGE_RESPONSE_FAILURE = 0x33706573
382 private const val MESSAGE_REQUEST_PERMISSION_RESTORE = 0x63826983
383 private const val HEADER_LENGTH = 12
384
385 /** Generate byte array containing desired header and data */
generatePacketnull386 private fun generatePacket(message: Int, data: String? = null): ByteArray {
387 val bytes = data?.toByteArray(StandardCharsets.UTF_8) ?: EmptyArray.BYTE
388
389 // Construct data packet with header + data
390 return ByteBuffer.allocate(bytes.size + 12)
391 .putInt(message) // message type
392 .putInt(1) // message sequence
393 .putInt(bytes.size) // data size
394 .put(bytes) // actual data
395 .array()
396 }
397
398 /** Message is the first 4-bytes of the stream, so just wrap the whole packet in an Integer. */
messageOfnull399 private fun messageOf(packet: ByteArray) = ByteBuffer.wrap(packet).int
400
401 /**
402 * Message is a response if the first byte of the message is 0x33.
403 *
404 * See [com.android.server.companion.transport.CompanionTransportManager].
405 */
406 private fun isResponse(message: Int): Boolean {
407 return (message and 0xFF000000.toInt()) == 0x33000000
408 }
409
410 /**
411 * Monitors the output from transport manager to detect when a full header (12-bytes) is sent and
412 * trigger callback with the message sent (4-bytes).
413 */
414 private class MonitoredOutputStream(
415 private val onHeaderSent: (Int) -> Unit
416 ) : ByteArrayOutputStream() {
417 private var callbackInvoked = false
418
flushnull419 override fun flush() {
420 super.flush()
421 if (!callbackInvoked && size() >= HEADER_LENGTH) {
422 onHeaderSent.invoke(messageOf(toByteArray()))
423 callbackInvoked = true
424 }
425 }
426 }
427