blob: 100ead49f50e24abd7666a99531a8a403989e0a0 [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.companion.cts.uiautomation
import android.Manifest.permission.READ_DEVICE_CONFIG
import android.annotation.CallSuper
import android.app.Activity.RESULT_CANCELED
import android.app.Activity.RESULT_OK
import android.companion.AssociationInfo
import android.companion.CompanionDeviceManager
import android.companion.CompanionException
import android.companion.cts.common.CompanionActivity
import android.companion.utils.FeatureUtils
import android.content.Intent
import android.os.OutcomeReceiver
import android.platform.test.annotations.AppModeFull
import androidx.test.ext.junit.runners.AndroidJUnit4
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.PipedInputStream
import java.io.PipedOutputStream
import java.nio.ByteBuffer
import java.nio.charset.StandardCharsets
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
import kotlin.time.Duration.Companion.seconds
import libcore.util.EmptyArray
import org.junit.Assume.assumeTrue
import org.junit.Test
import org.junit.runner.RunWith
/**
* Tests the system data transfer.
*
* Build/Install/Run: atest CtsCompanionDeviceManagerUiAutomationTestCases:SystemDataTransferTest
*/
@AppModeFull(reason = "CompanionDeviceManager APIs are not available to the instant apps.")
@RunWith(AndroidJUnit4::class)
class SystemDataTransferTest : UiAutomationTestBase(null, null) {
companion object {
private const val SYSTEM_DATA_TRANSFER_TIMEOUT = 10_000L // 10 seconds
}
@CallSuper
override fun setUp() {
super.setUp()
assumeTrue(withShellPermissionIdentity(READ_DEVICE_CONFIG) {
FeatureUtils.isPermSyncEnabled()
})
cdm.enableSecureTransport(false)
}
@CallSuper
override fun tearDown() {
cdm.enableSecureTransport(true)
super.tearDown()
}
@Test
fun test_userConsentDialogAllowed() {
val association1 = associate()
// First time request permission transfer should prompt a dialog
val pendingUserConsent = cdm.buildPermissionTransferUserConsentIntent(association1.id)
assertNotNull(pendingUserConsent)
CompanionActivity.startIntentSender(pendingUserConsent)
confirmationUi.waitUntilSystemDataTransferConfirmationVisible()
confirmationUi.clickPositiveButton()
val (resultCode: Int, _: Intent?) = CompanionActivity.waitForActivityResult()
assertEquals(expected = RESULT_OK, actual = resultCode)
// Second time request permission transfer should get non null IntentSender
val pendingUserConsent2 = cdm.buildPermissionTransferUserConsentIntent(association1.id)
assertNotNull(pendingUserConsent2)
// disassociate() should clean up the requests
cdm.disassociate(association1.id)
Thread.sleep(2_100)
val association2 = associate()
val pendingUserConsent3 = cdm.buildPermissionTransferUserConsentIntent(association2.id)
assertNotNull(pendingUserConsent3)
}
@Test
fun test_userConsentDialogDisallowed() {
val association1 = associate()
// First time request permission transfer should prompt a dialog
val pendingUserConsent = cdm.buildPermissionTransferUserConsentIntent(association1.id)
assertNotNull(pendingUserConsent)
CompanionActivity.startIntentSender(pendingUserConsent)
confirmationUi.waitUntilSystemDataTransferConfirmationVisible()
confirmationUi.clickNegativeButton()
val (resultCode: Int, _: Intent?) = CompanionActivity.waitForActivityResult()
assertEquals(expected = RESULT_CANCELED, actual = resultCode)
// Second time request permission transfer should get non null IntentSender
val pendingUserConsent2 = cdm.buildPermissionTransferUserConsentIntent(association1.id)
assertNotNull(pendingUserConsent2)
// disassociate() should clean up the requests
cdm.disassociate(association1.id)
Thread.sleep(2_100)
val association2 = associate()
val pendingUserConsent3 = cdm.buildPermissionTransferUserConsentIntent(association2.id)
assertNotNull(pendingUserConsent3)
}
@Test
fun test_userConsentDialogCanceled() {
val association1 = associate()
// First time request permission transfer should prompt a dialog
val pendingUserConsent = cdm.buildPermissionTransferUserConsentIntent(association1.id)
assertNotNull(pendingUserConsent)
CompanionActivity.startIntentSender(pendingUserConsent)
confirmationUi.waitUntilSystemDataTransferConfirmationVisible()
uiDevice.pressBack()
// Second time request permission transfer should prompt a dialog
val pendingUserConsent2 = cdm.buildPermissionTransferUserConsentIntent(association1.id)
assertNotNull(pendingUserConsent2)
}
@Test
fun test_userConsentDialogAllowedAndThenDisallowed() {
val association1 = associate()
// First time request permission transfer should prompt a dialog
val pendingUserConsent = cdm.buildPermissionTransferUserConsentIntent(association1.id)
assertNotNull(pendingUserConsent)
CompanionActivity.startIntentSender(pendingUserConsent)
confirmationUi.waitUntilSystemDataTransferConfirmationVisible()
confirmationUi.clickPositiveButton()
val (resultCode: Int, _: Intent?) = CompanionActivity.waitForActivityResult()
assertEquals(expected = RESULT_OK, actual = resultCode)
// Second time request permission transfer should prompt a dialog
val pendingUserConsent2 = cdm.buildPermissionTransferUserConsentIntent(association1.id)
assertNotNull(pendingUserConsent2)
CompanionActivity.startIntentSender(pendingUserConsent2)
confirmationUi.waitUntilSystemDataTransferConfirmationVisible()
confirmationUi.clickNegativeButton()
val (resultCode2: Int, _: Intent?) = CompanionActivity.waitForActivityResult()
assertEquals(expected = RESULT_CANCELED, actual = resultCode2)
}
/**
* Test that calling system data transfer API without first having acquired user consent
* results in triggering error callback.
*/
@Test(expected = CompanionException::class)
fun test_startSystemDataTransfer_requiresUserConsent() {
val association = associate()
// Generate data packet with successful response
val response = generatePacket(MESSAGE_RESPONSE_SUCCESS, "SUCCESS")
// This will fail due to lack of user consent
startSystemDataTransfer(association.id, response)
}
/**
* Test that system data transfer triggers success callback when CDM receives successful
* response from the device whose permissions are being restored.
*/
@Test
fun test_startSystemDataTransfer_success() {
val association = associate()
requestPermissionTransferUserConsent(association.id)
// Generate data packet with successful response
val response = generatePacket(MESSAGE_RESPONSE_SUCCESS, "SUCCESS")
startSystemDataTransfer(association.id, response)
}
/**
* Test that system data transfer triggers error callback when CDM receives failure response
* from the device whose permissions are being restored.
*/
@Test(expected = CompanionException::class)
fun test_startSystemDataTransfer_failure() {
val association = associate()
requestPermissionTransferUserConsent(association.id)
// Generate data packet with failure as response
val response = generatePacket(MESSAGE_RESPONSE_FAILURE, "FAILURE")
startSystemDataTransfer(association.id, response)
}
/**
* Test that CDM sends a response to incoming request to restore permissions.
*
* This test uses a mock request with an empty body, so just assert that CDM sends any response.
*/
@Test
fun test_receivePermissionRestore() {
val association = associate()
// Generate data packet with permission restore request
val bytes = generatePacket(MESSAGE_REQUEST_PERMISSION_RESTORE)
val input = ByteArrayInputStream(bytes)
// Monitor output response from CDM
val messageSent = CountDownLatch(1)
val sentMessage = AtomicInteger()
val output = MonitoredOutputStream { message ->
sentMessage.set(message)
messageSent.countDown()
}
// "Receive" permission restore request
cdm.attachSystemDataTransport(association.id, input, output)
// Assert CDM sent a message
assertTrue(messageSent.await(SYSTEM_DATA_TRANSFER_TIMEOUT, TimeUnit.SECONDS))
// Assert that sent message was in response format (can be success or failure)
assertTrue(isResponse(sentMessage.get()))
}
/**
* Associate without checking the association data.
*/
private fun associate(): AssociationInfo {
sendRequestAndLaunchConfirmation(singleDevice = true)
confirmationUi.scrollToBottom()
callback.assertInvokedByActions {
// User "approves" the request.
confirmationUi.clickPositiveButton()
}
// Wait until the Confirmation UI goes away.
confirmationUi.waitUntilGone()
// Check the result code and the data delivered via onActivityResult()
val (_: Int, associationData: Intent?) = CompanionActivity.waitForActivityResult()
assertNotNull(associationData)
val association: AssociationInfo? = associationData.getParcelableExtra(
CompanionDeviceManager.EXTRA_ASSOCIATION,
AssociationInfo::class.java)
assertNotNull(association)
return association
}
/**
* Execute UI flow to request user consent for permission transfer for a given association
* and grant permission.
*/
private fun requestPermissionTransferUserConsent(associationId: Int) {
val pendingUserConsent = cdm.buildPermissionTransferUserConsentIntent(associationId)
CompanionActivity.startIntentSender(pendingUserConsent!!)
confirmationUi.waitUntilSystemDataTransferConfirmationVisible()
confirmationUi.clickPositiveButton()
CompanionActivity.waitForActivityResult()
}
/**
* Start system data transfer synchronously.
*/
private fun startSystemDataTransfer(
associationId: Int,
simulatedResponse: ByteArray
) {
// Piped input stream to simulate any response for CDM to receive
val inputSource = PipedOutputStream()
val pipedInput = PipedInputStream(inputSource)
// Only receive simulated response after permission restore request is sent
val monitoredOutput = MonitoredOutputStream { message ->
if (message == MESSAGE_REQUEST_PERMISSION_RESTORE) {
inputSource.write(simulatedResponse)
inputSource.flush()
}
}
cdm.attachSystemDataTransport(associationId, pipedInput, monitoredOutput)
// Synchronously start system data transfer
val transferFinished = CountDownLatch(1)
val err = AtomicReference<CompanionException>()
val callback = object : OutcomeReceiver<Void?, CompanionException> {
override fun onResult(result: Void?) {
transferFinished.countDown()
}
override fun onError(error: CompanionException) {
err.set(error)
transferFinished.countDown()
}
}
cdm.startSystemDataTransfer(associationId, context.mainExecutor, callback)
// Don't let it hang for too long!
if (!transferFinished.await(SYSTEM_DATA_TRANSFER_TIMEOUT, TimeUnit.SECONDS)) {
throw TimeoutException("System data transfer timed out.")
}
// Catch transfer failure
if (err.get() != null) {
throw err.get()
}
// Detach data transport
cdm.detachSystemDataTransport(associationId)
}
}
/**
* Message codes defined in [com.android.server.companion.transport.CompanionTransportManager].
*/
private const val MESSAGE_RESPONSE_SUCCESS = 0x33838567
private const val MESSAGE_RESPONSE_FAILURE = 0x33706573
private const val MESSAGE_REQUEST_PERMISSION_RESTORE = 0x63826983
private const val HEADER_LENGTH = 12
/** Generate byte array containing desired header and data */
private fun generatePacket(message: Int, data: String? = null): ByteArray {
val bytes = data?.toByteArray(StandardCharsets.UTF_8) ?: EmptyArray.BYTE
// Construct data packet with header + data
return ByteBuffer.allocate(bytes.size + 12)
.putInt(message) // message type
.putInt(1) // message sequence
.putInt(bytes.size) // data size
.put(bytes) // actual data
.array()
}
/** Message is the first 4-bytes of the stream, so just wrap the whole packet in an Integer. */
private fun messageOf(packet: ByteArray) = ByteBuffer.wrap(packet).int
/**
* Message is a response if the first byte of the message is 0x33.
*
* See [com.android.server.companion.transport.CompanionTransportManager].
*/
private fun isResponse(message: Int): Boolean {
return (message and 0xFF000000.toInt()) == 0x33000000
}
/**
* Monitors the output from transport manager to detect when a full header (12-bytes) is sent and
* trigger callback with the message sent (4-bytes).
*/
private class MonitoredOutputStream(
private val onHeaderSent: (Int) -> Unit
) : ByteArrayOutputStream() {
private var callbackInvoked = false
override fun flush() {
super.flush()
if (!callbackInvoked && size() >= HEADER_LENGTH) {
onHeaderSent.invoke(messageOf(toByteArray()))
callbackInvoked = true
}
}
}