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