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