• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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