Merge "Add multi-device tests for CDM." into main

This commit is contained in:
Raphael Kim
2023-07-13 22:45:45 +00:00
committed by Android (Google) Code Review
15 changed files with 961 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
# Bug component: 708992
evanxinchen@google.com
guojing@google.com
jeremyns@google.com
raphk@google.com
yukl@google.com

View File

@@ -0,0 +1,17 @@
## CDM Multi-device Tests
### Device Setup
To test on physical devices, connect _two_ devices locally and enable USB debugging setting on both devices.
When running on a cloudtop or other remote setups, use pontis to connect the devices on remote set up by running `pontis start`.
Verify that pontis client is connected via `pontis status` and confirm that both devices are in "connected" state via `adb devices`.
See go/pontis for more details regarding this workflow.
To test on virtual devices, follow instructions to [set up netsim on cuttlefish](https://g3doc.corp.google.com/ambient/d2di/sim/g3doc/guide/cuttlefish.md?cl=head).
Launch _two_ instances of virtual devices by specifying `--num_instances=2` parameter.
### Running the Test
```
atest CompanionDeviceManagerMultiDeviceTestCases
```

View File

@@ -0,0 +1,50 @@
// 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 {
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "frameworks_base_license"
// to get the below license kinds:
// SPDX-license-identifier-Apache-2.0
default_applicable_licenses: ["frameworks_base_license"],
}
android_test {
name: "cdm_snippet",
srcs: ["src/**/*.kt"],
manifest: "AndroidManifest.xml",
platform_apis: true,
target_sdk_version: "current",
static_libs: [
"androidx.test.ext.junit",
"androidx.test.uiautomator_uiautomator",
"compatibility-device-util-axt",
"cts-companion-common",
"cts-companion-uicommon",
"kotlin-stdlib",
"mobly-snippet-lib",
],
libs: [
"android.test.base",
"android.test.runner",
],
optimize: {
proguard_compatibility: true,
proguard_flags_files: ["proguard.flags"],
},
}

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="android.companion.multidevices">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_COMPANION_SELF_MANAGED" />
<uses-permission android:name="android.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE" />
<uses-permission android:name="android.permission.REQUEST_COMPANION_PROFILE_WATCH" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.DELIVER_COMPANION_MESSAGES" />
<uses-feature android:name="android.hardware.bluetooth" android:required="true"/>
<uses-feature android:name="android.software.companion_device_setup" />
<application>
<!-- Add any classes that implement the Snippet interface as meta-data, whose
value is a comma-separated string, each section being the package path
of a snippet class -->
<meta-data
android:name="mobly-snippets"
android:value="android.companion.multidevices.CompanionDeviceManagerSnippet" />
</application>
<!-- Add an instrumentation tag so that the app can be launched through an
instrument command. The runner `com.google.android.mobly.snippet.SnippetRunner`
is derived from `AndroidJUnitRunner`, and is required to use the
Mobly Snippet Lib. -->
<instrumentation
android:name="com.google.android.mobly.snippet.SnippetRunner"
android:targetPackage="android.companion.multidevices" />
</manifest>

View File

@@ -0,0 +1,24 @@
# Keep all companion classes.
-keep class android.companion.** {
*;
}
# Do not touch Mobly.
-keep class com.google.android.mobly.** {
*;
}
# Keep names for easy debugging.
-dontobfuscate
# Necessary to allow debugging.
-keepattributes *
# By default, proguard leaves all classes in their original package, which
# needlessly repeats com.google.android.apps.etc.
-repackageclasses ""
# Allows proguard to make private and protected methods and fields public as
# part of optimization. This lets proguard inline trivial getter/setter
# methods.
-allowaccessmodification

View File

@@ -0,0 +1,107 @@
/*
* 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.multidevices
import android.companion.AssociationInfo
import android.companion.CompanionDeviceManager
import android.companion.CompanionException
import android.content.IntentSender
import android.os.OutcomeReceiver
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit.SECONDS
import java.util.concurrent.TimeoutException
/** Blocking callbacks for Wi-Fi Aware and Connectivity Manager. */
object CallbackUtils {
private const val TAG = "CDM_CallbackUtils"
private const val CALLBACK_TIMEOUT_SEC = 30L
private const val MESSAGE_CALLBACK_TIMEOUT_SEC = 5L
class AssociationCallback : CompanionDeviceManager.Callback() {
private val pending = CountDownLatch(1)
private val created = CountDownLatch(1)
private var pendingIntent: IntentSender? = null
private var associationInfo: AssociationInfo? = null
private var error: String? = null
override fun onAssociationPending(intentSender: IntentSender) {
this.pendingIntent = intentSender
pending.countDown()
}
override fun onAssociationCreated(associationInfo: AssociationInfo) {
this.associationInfo = associationInfo
created.countDown()
}
override fun onFailure(error: CharSequence?) {
this.error = error?.toString() ?: "There was an unexpected failure."
pending.countDown()
created.countDown()
}
fun waitForPendingIntent(): IntentSender? {
if (!pending.await(CALLBACK_TIMEOUT_SEC, SECONDS)) {
throw TimeoutException("Pending association request timed out.")
}
error?.let {
throw CompanionException(it)
}
return pendingIntent
}
fun waitForAssociation(): AssociationInfo? {
if (!created.await(CALLBACK_TIMEOUT_SEC, SECONDS)) {
throw TimeoutException("Association request timed out.")
}
error?.let {
throw CompanionException(it)
}
return associationInfo
}
}
class SystemDataTransferCallback : OutcomeReceiver<Void, CompanionException> {
private val completed = CountDownLatch(1)
private var error: CompanionException? = null
override fun onResult(result: Void?) {
completed.countDown()
}
override fun onError(error: CompanionException) {
this.error = error
completed.countDown()
}
fun waitForCompletion() {
if (!completed.await(CALLBACK_TIMEOUT_SEC, SECONDS)) {
throw TimeoutException("System data transfer timed out.")
}
error?.let {
throw it
}
}
}
}

View File

@@ -0,0 +1,183 @@
/*
* 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.multidevices
import android.app.Instrumentation
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothManager
import android.companion.AssociationInfo
import android.companion.AssociationRequest
import android.companion.BluetoothDeviceFilter
import android.companion.CompanionDeviceManager
import android.companion.CompanionException
import android.companion.cts.common.CompanionActivity
import android.companion.multidevices.CallbackUtils.AssociationCallback
import android.companion.multidevices.CallbackUtils.SystemDataTransferCallback
import android.companion.multidevices.bluetooth.BluetoothConnector
import android.companion.multidevices.bluetooth.BluetoothController
import android.companion.cts.uicommon.CompanionDeviceManagerUi
import android.content.Context
import android.os.Handler
import android.os.HandlerExecutor
import android.os.HandlerThread
import android.util.Log
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import com.google.android.mobly.snippet.Snippet
import com.google.android.mobly.snippet.event.EventCache
import com.google.android.mobly.snippet.rpc.Rpc
import java.util.concurrent.Executor
import java.util.regex.Pattern
/**
* Snippet class that exposes Android APIs in CompanionDeviceManager.
*/
class CompanionDeviceManagerSnippet : Snippet {
private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()!!
private val context: Context = instrumentation.targetContext
private val btAdapter: BluetoothAdapter by lazy {
(context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter
}
private val companionDeviceManager: CompanionDeviceManager by lazy {
context.getSystemService(Context.COMPANION_DEVICE_SERVICE) as CompanionDeviceManager
}
private val btConnector: BluetoothConnector by lazy {
BluetoothConnector(btAdapter, companionDeviceManager)
}
private val uiDevice by lazy { UiDevice.getInstance(instrumentation) }
private val confirmationUi by lazy { CompanionDeviceManagerUi(uiDevice) }
private val btController by lazy { BluetoothController(context, btAdapter, uiDevice) }
private val eventCache = EventCache.getInstance()
private val handlerThread = HandlerThread("Snippet-Aware")
private val handler: Handler
private val executor: Executor
init {
handlerThread.start()
handler = Handler(handlerThread.looper)
executor = HandlerExecutor(handler)
}
/**
* Make device discoverable to other devices via BLE and return device name.
*/
@Rpc(description = "Start advertising device to be discoverable.")
fun becomeDiscoverable(): String {
btController.becomeDiscoverable()
return btAdapter.name
}
/**
* Associate with a nearby device with given name and return newly-created association ID.
*/
@Rpc(description = "Start device association flow.")
@Throws(Exception::class)
fun associate(deviceName: String): Int {
val filter = BluetoothDeviceFilter.Builder()
.setNamePattern(Pattern.compile(deviceName))
.build()
val request = AssociationRequest.Builder()
.setSingleDevice(true)
.addDeviceFilter(filter)
.build()
val callback = AssociationCallback()
companionDeviceManager.associate(request, callback, handler)
val pendingConfirmation = callback.waitForPendingIntent()
?: throw CompanionException("Association is pending but intent sender is null.")
CompanionActivity.launchAndWait(context)
CompanionActivity.startIntentSender(pendingConfirmation)
confirmationUi.waitUntilVisible()
confirmationUi.waitUntilPositiveButtonIsEnabledAndClick()
confirmationUi.waitUntilGone()
val (_, result) = CompanionActivity.waitForActivityResult()
if (result == null) {
throw CompanionException("Association result can't be null.")
}
val association = result.getParcelableExtra(
CompanionDeviceManager.EXTRA_ASSOCIATION,
AssociationInfo::class.java
)
val remoteDevice = association.associatedDevice?.getBluetoothDevice()!!
// Register associated device
btConnector.registerDevice(association.id, remoteDevice)
return association.id
}
/**
* Disassociate an association with given ID.
*/
@Rpc(description = "Disassociate device.")
@Throws(Exception::class)
fun disassociate(associationId: Int) {
companionDeviceManager.disassociate(associationId)
}
/**
* Consent to system data transfer and carry it out using Bluetooth socket.
*/
@Rpc(description = "Start permissions sync.")
fun startPermissionsSync(associationId: Int) {
val pendingIntent = companionDeviceManager
.buildPermissionTransferUserConsentIntent(associationId)
CompanionActivity.launchAndWait(context)
CompanionActivity.startIntentSender(pendingIntent)
confirmationUi.waitUntilSystemDataTransferConfirmationVisible()
confirmationUi.clickPositiveButton()
confirmationUi.waitUntilGone()
CompanionActivity.waitForActivityResult()
val callback = SystemDataTransferCallback()
companionDeviceManager.startSystemDataTransfer(associationId, executor, callback)
callback.waitForCompletion()
}
@Rpc(description = "Attach transport to the BT client socket.")
fun attachClientSocket(id: Int) {
btConnector.attachClientSocket(id)
}
@Rpc(description = "Attach transport to the BT server socket.")
fun attachServerSocket(id: Int) {
btConnector.attachServerSocket(id)
}
@Rpc(description = "Close all open sockets.")
fun closeAllSockets() {
// Close all open sockets
btConnector.closeAllSockets()
}
@Rpc(description = "Disassociate all associations.")
fun disassociateAll() {
companionDeviceManager.myAssociations.forEach {
Log.d(TAG, "Disassociating id=${it.id}.")
companionDeviceManager.disassociate(it.id)
}
}
companion object {
private const val TAG = "CDM_CompanionDeviceManagerSnippet"
}
}

View File

@@ -0,0 +1,155 @@
/*
* 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.multidevices.bluetooth
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothServerSocket
import android.bluetooth.BluetoothSocket
import android.companion.CompanionDeviceManager
import android.util.Log
import java.io.IOException
import java.util.UUID
class BluetoothConnector(
private val adapter: BluetoothAdapter,
private val cdm: CompanionDeviceManager
) {
companion object {
private const val TAG = "CDM_BluetoothServer"
private val SERVICE_NAME = "CDM_BluetoothChannel"
private val SERVICE_UUID = UUID.fromString("435fe1d9-56c5-455d-a516-d5e6b22c52f9")
// Registry of bluetooth server threads
private val serverThreads = mutableMapOf<Int, BluetoothServerThread>()
// Registry of remote bluetooth devices
private val remoteDevices = mutableMapOf<Int, BluetoothDevice>()
// Set of connected client sockets
private val clientSockets = mutableMapOf<Int, BluetoothSocket>()
}
fun attachClientSocket(associationId: Int) {
try {
val device = remoteDevices[associationId]!!
val socket = device.createRfcommSocketToServiceRecord(SERVICE_UUID)
if (clientSockets.containsKey(associationId)) {
detachClientSocket(associationId)
clientSockets[associationId] = socket
} else {
clientSockets += associationId to socket
}
socket.connect()
Log.d(TAG, "Attaching client socket $socket.")
cdm.attachSystemDataTransport(
associationId,
socket.inputStream,
socket.outputStream
)
} catch (e: IOException) {
Log.e(TAG, "Failed to attach client socket.", e)
throw RuntimeException(e)
}
}
fun attachServerSocket(associationId: Int) {
val serverThread: BluetoothServerThread
if (serverThreads.containsKey(associationId)) {
serverThread = serverThreads[associationId]!!
} else {
serverThread = BluetoothServerThread(associationId)
serverThreads += associationId to serverThread
}
// Start thread
if (!serverThread.isOpen) {
serverThread.start()
}
}
fun closeAllSockets() {
val iter = clientSockets.keys.iterator()
while (iter.hasNext()) {
detachClientSocket(iter.next())
}
for (thread in serverThreads.values) {
thread.shutdown()
}
serverThreads.clear()
}
fun registerDevice(associationId: Int, remoteDevice: BluetoothDevice) {
remoteDevices[associationId] = remoteDevice
}
private fun detachClientSocket(associationId: Int) {
try {
Log.d(TAG, "Detaching client socket.")
cdm.detachSystemDataTransport(associationId)
clientSockets[associationId]?.close()
} catch (e: IOException) {
Log.e(TAG, "Failed to detach client socket.", e)
throw RuntimeException(e)
}
}
inner class BluetoothServerThread(
private val associationId: Int
) : Thread() {
private lateinit var mServerSocket: BluetoothServerSocket
var isOpen = false
override fun run() {
try {
Log.d(TAG, "Listening for remote connections...")
mServerSocket = adapter.listenUsingRfcommWithServiceRecord(
SERVICE_NAME,
SERVICE_UUID
)
isOpen = true
do {
val socket = mServerSocket.accept()
Log.d(TAG, "Attaching server socket $socket.")
cdm.attachSystemDataTransport(
associationId,
socket.inputStream,
socket.outputStream
)
} while (isOpen)
} catch (e: IOException) {
throw RuntimeException(e)
}
}
fun shutdown() {
if (!isOpen || !this::mServerSocket.isInitialized) return
try {
Log.d(TAG, "Closing server socket.")
cdm.detachSystemDataTransport(associationId)
mServerSocket.close()
isOpen = false
} catch (e: IOException) {
throw RuntimeException(e)
}
}
}
}

View File

@@ -0,0 +1,109 @@
/*
* 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.multidevices.bluetooth
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.SystemClock
import android.util.Log
import androidx.test.uiautomator.UiDevice
import java.util.concurrent.TimeoutException
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
/** Controls the local Bluetooth adapter for testing. */
class BluetoothController(
private val context: Context,
private val adapter: BluetoothAdapter,
private val ui: UiDevice
) {
companion object {
private const val TAG = "CDM_BluetoothController"
}
private val bluetoothUi by lazy { BluetoothUi(ui) }
init {
Log.d(TAG, "Registering pairing listener.")
context.registerReceiver(
PairingBroadcastReceiver(),
IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST)
)
}
val isEnabled: Boolean
get() = adapter.isEnabled
/** Turns on the local Bluetooth adapter */
fun enableBluetooth() {
if (isEnabled) return
val intent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context.startActivity(intent)
bluetoothUi.clickAllowButton()
waitFor { adapter.state == BluetoothAdapter.STATE_ON }
}
/** Become discoverable for specified duration */
fun becomeDiscoverable(duration: Duration = 15.seconds) {
enableBluetooth()
val intent = Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, duration.inWholeSeconds)
context.startActivity(intent)
bluetoothUi.clickAllowButton()
}
/** Unpair all devices for cleanup */
fun unpairAllDevices() {
for (device in adapter.bondedDevices) {
Log.d(TAG, "Unpairing $device.")
if (!device.removeBond()) continue
waitFor { device.bondState == BluetoothDevice.BOND_NONE }
}
}
private fun waitFor(
interval: Duration = 1.seconds,
timeout: Duration = 5.seconds,
condition: () -> Boolean
) {
var elapsed = 0L
while (elapsed < timeout.inWholeMilliseconds) {
if (condition.invoke()) return
SystemClock.sleep(interval.inWholeMilliseconds)
elapsed += interval.inWholeMilliseconds
}
throw TimeoutException("Bluetooth did not become an expected state.")
}
inner class PairingBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Log.d(TAG, "Received broadcast for ${intent.action}")
// onReceive() somehow blocks pairing prompt from launching
Thread { bluetoothUi.confirmPairingRequest() }.start()
context.unregisterReceiver(this)
}
}
}

View File

@@ -0,0 +1,66 @@
/*
* 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.multidevices.bluetooth
import android.companion.cts.uicommon.CompanionDeviceManagerUi
import android.util.Log
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.Until
import java.util.regex.Pattern
class BluetoothUi(private val ui: UiDevice) : CompanionDeviceManagerUi(ui) {
fun clickAllowButton() = click(ALLOW_BUTTON, "Allow button")
fun confirmPairingRequest(): Boolean {
if (ui.hasObject(PAIRING_PIN_ENTRY)) {
// It is prompting for a custom user pin entry
Log.d(TAG, "Is user entry prompt.")
ui.findObject(PAIRING_PIN_ENTRY).text = "0000"
click(OK_BUTTON, "Ok button")
} else {
// It just needs user consent
Log.d(TAG, "Looking for pair button.")
val button = ui.wait(Until.findObject(PAIR_BUTTON), 1_000)
if (button != null) {
Log.d(TAG, "Pair button found.")
button.click()
return true
}
Log.d(TAG, "Pair button not found.")
}
return false
}
companion object {
private const val TAG = "CDM_BluetoothUi"
private val ALLOW_TEXT_PATTERN = caseInsensitive("allow")
private val ALLOW_BUTTON = By.text(ALLOW_TEXT_PATTERN).clickable(true)
private val PAIRING_PIN_ENTRY = By.clazz(".EditText")
private val OK_TEXT_PATTERN = caseInsensitive("ok")
private val OK_BUTTON = By.text(OK_TEXT_PATTERN).clickable(true)
private val PAIR_TEXT_PATTERN = caseInsensitive("pair")
private val PAIR_BUTTON = By.text(PAIR_TEXT_PATTERN).clickable(true)
private fun caseInsensitive(text: String): Pattern =
Pattern.compile(text, Pattern.CASE_INSENSITIVE)
}
}

View File

@@ -0,0 +1,51 @@
// 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 {
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "frameworks_base_license"
// to get the below license kinds:
// SPDX-license-identifier-Apache-2.0
default_applicable_licenses: ["frameworks_base_license"],
}
python_test_host {
name: "CompanionDeviceManagerMultiDeviceTestCases",
main: "cdm_transport_test.py",
srcs: ["*.py"],
libs: [
"mobly",
],
test_suites: [
"general-tests",
],
test_options: {
unit_test: false,
tags: ["mobly"],
},
data: [
":cdm_snippet",
"requirements.txt",
],
version: {
py2: {
enabled: false,
},
py3: {
enabled: true,
embedded_launcher: true,
},
},
}

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<configuration description="Config for CDM multi-device test cases">
<option name="test-tag" value="CompanionDeviceMultiDeviceTests" />
<option name="config-descriptor:metadata" key="component" value="framework" />
<option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
<option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
<option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
<object class="com.android.tradefed.testtype.suite.module.DeviceFeatureModuleController"
type="module_controller">
<option name="required-feature" value="android.software.companion_device_setup" />
</object>
<device name="device1">
<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
<option name="test-file-name" value="cdm_snippet.apk" />
</target_preparer>
</device>
<device name="device2">
<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
<option name="test-file-name" value="cdm_snippet.apk" />
</target_preparer>
</device>
<test class="com.android.tradefed.testtype.mobly.MoblyBinaryHostTest">
<!-- The mobly-par-file-name should match the module name -->
<option name="mobly-par-file-name" value="CompanionDeviceManagerMultiDeviceTestCases" />
<!-- Timeout limit in milliseconds for all test cases of the python binary -->
<option name="mobly-test-timeout" value="60000" />
</test>
</configuration>

View File

@@ -0,0 +1,63 @@
# Lint as: python3
"""
Base class for setting up devices for CDM functionalities.
"""
from mobly import base_test
from mobly import utils
from mobly.controllers import android_device
CDM_SNIPPET_PACKAGE = 'android.companion.multidevices'
class BaseTestClass(base_test.BaseTestClass):
def setup_class(self):
# Declare that two Android devices are needed.
self.sender, self.receiver = self.register_controller(
android_device, min_number=2)
self.sender_id = None
self.receiver_id = None
def _setup_device(device):
device.load_snippet('cdm', CDM_SNIPPET_PACKAGE)
device.adb.shell('input keyevent KEYCODE_WAKEUP')
device.adb.shell('input keyevent KEYCODE_MENU')
device.adb.shell('input keyevent KEYCODE_HOME')
# Clean up existing associations
device.cdm.disassociateAll()
# Sets up devices in parallel to save time.
utils.concurrent_exec(
_setup_device,
((self.sender,), (self.receiver,)),
max_workers=2,
raise_on_exception=True)
def associate_devices(self) -> tuple[int, int]:
"""Associate devices with each other and return association IDs for both"""
# If association already exists, don't need another
if self.sender_id and self.receiver_id:
return (self.sender_id, self.receiver_id)
receiver_name = self.receiver.cdm.becomeDiscoverable()
self.receiver_id = self.sender.cdm.associate(receiver_name)
sender_name = self.sender.cdm.becomeDiscoverable()
self.sender_id = self.receiver.cdm.associate(sender_name)
return (self.sender_id, self.receiver_id)
def attach_transports(self):
"""Attach transports to both devices"""
self.associate_devices()
self.receiver.cdm.attachServerSocket(self.sender_id)
self.sender.cdm.attachClientSocket(self.receiver_id)
def teardown_class(self):
"""Clean up the opened sockets"""
self.sender.cdm.closeAllSockets()
self.receiver.cdm.closeAllSockets()

View File

@@ -0,0 +1,36 @@
# Lint as: python3
"""
Test E2E CDM functions on mobly.
"""
import cdm_base_test
import sys
from mobly import asserts
from mobly import test_runner
CDM_SNIPPET_PACKAGE = 'android.companion.multidevices'
class TransportTestClass(cdm_base_test.BaseTestClass):
def test_permissions_sync(self):
"""This tests permissions sync from one device to another."""
# associate and attach transports
self.attach_transports()
# start permissions sync
self.sender.cdm.startPermissionsSync(self.receiver_id)
if __name__ == '__main__':
try:
# Take test args and remove standalone '--' from the list
index = sys.argv.index('--')
sys.argv = sys.argv[:1] + sys.argv[index + 1:]
except ValueError:
# Ignore if '--' is not in args
pass
test_runner.main()

View File

@@ -0,0 +1 @@
mobly==1.12.1