1 /* 2 * Copyright (C) 2024 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.multidevice 18 19 import android.app.Instrumentation 20 import android.bluetooth.BluetoothManager 21 import android.companion.AssociationInfo 22 import android.companion.AssociationRequest 23 import android.companion.BluetoothDeviceFilter 24 import android.companion.CompanionDeviceManager 25 import android.companion.ObservingDevicePresenceRequest 26 import android.companion.cts.common.CompanionActivity 27 import android.companion.cts.common.PrimaryCompanionService 28 import android.companion.cts.multidevice.CallbackUtils.SystemDataTransferCallback 29 import android.companion.cts.uicommon.CompanionDeviceManagerUi 30 import android.content.Context 31 import android.content.pm.PackageManager 32 import android.os.Handler 33 import android.os.HandlerExecutor 34 import android.os.HandlerThread 35 import android.util.Log 36 import androidx.test.platform.app.InstrumentationRegistry 37 import androidx.test.uiautomator.UiDevice 38 import com.google.android.mobly.snippet.Snippet 39 import com.google.android.mobly.snippet.rpc.Rpc 40 import java.util.concurrent.Executor 41 42 /** 43 * Snippet class that exposes Android APIs in CompanionDeviceManager. 44 */ 45 class CompanionDeviceManagerSnippet : Snippet { 46 private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()!! 47 private val context: Context = instrumentation.targetContext 48 private val companionDeviceManager = context.getSystemService(Context.COMPANION_DEVICE_SERVICE) 49 as CompanionDeviceManager 50 51 private val btManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager 52 private val btConnector = BluetoothConnector(btManager.adapter, companionDeviceManager) 53 <lambda>null54 private val uiDevice by lazy { UiDevice.getInstance(instrumentation) } <lambda>null55 private val confirmationUi by lazy { CompanionDeviceManagerUi(uiDevice) } 56 57 private val handlerThread = HandlerThread("Snippet-Aware") 58 private val handler: Handler 59 private val executor: Executor 60 61 init { 62 handlerThread.start() 63 handler = Handler(handlerThread.looper) 64 executor = HandlerExecutor(handler) 65 } 66 67 /** 68 * Associate with a nearby device with given name and return newly-created association ID. 69 */ 70 @Rpc(description = "Start device association flow.") 71 @Throws(Exception::class) associatenull72 fun associate(deviceAddress: String): Int { 73 val filter = BluetoothDeviceFilter.Builder() 74 .setAddress(deviceAddress) 75 .build() 76 val request = AssociationRequest.Builder() 77 .setSingleDevice(true) 78 .addDeviceFilter(filter) 79 .build() 80 val callback = CallbackUtils.AssociationCallback() 81 companionDeviceManager.associate(request, callback, handler) 82 val pendingConfirmation = callback.waitForPendingIntent() 83 ?: throw RuntimeException("Association is pending but intent sender is null.") 84 CompanionActivity.launchAndWait(context) 85 CompanionActivity.startIntentSender(pendingConfirmation) 86 confirmationUi.waitUntilVisible() 87 confirmationUi.waitUntilPositiveButtonIsEnabledAndClick() 88 confirmationUi.waitUntilGone() 89 90 val (_, result) = CompanionActivity.waitForActivityResult() 91 CompanionActivity.safeFinish() 92 CompanionActivity.waitUntilGone() 93 94 if (result == null) { 95 throw RuntimeException("Association result can't be null.") 96 } 97 98 val association = checkNotNull(result.getParcelableExtra( 99 CompanionDeviceManager.EXTRA_ASSOCIATION, 100 AssociationInfo::class.java 101 )) 102 103 return association.id 104 } 105 106 /** 107 * Request user consent to system data transfer and accept. 108 */ 109 @Rpc(description = "Start permissions sync.") requestPermissionTransferUserConsentnull110 fun requestPermissionTransferUserConsent(associationId: Int) { 111 val pendingIntent = checkNotNull( 112 companionDeviceManager.buildPermissionTransferUserConsentIntent(associationId) 113 ) 114 CompanionActivity.launchAndWait(context) 115 CompanionActivity.startIntentSender(pendingIntent) 116 confirmationUi.waitUntilSystemDataTransferConfirmationVisible() 117 confirmationUi.clickPositiveButton() 118 confirmationUi.waitUntilGone() 119 120 CompanionActivity.waitForActivityResult() 121 CompanionActivity.safeFinish() 122 CompanionActivity.waitUntilGone() 123 } 124 125 /** 126 * Returns the list of association IDs owned by the test app. 127 */ 128 @Rpc(description = "Get my association IDs.") 129 @Throws(Exception::class) getMyAssociationsnull130 fun getMyAssociations(): List<Int> { 131 return companionDeviceManager.myAssociations.stream().map { it.id }.toList() 132 } 133 134 /** 135 * Disassociate an association with given ID. 136 */ 137 @Rpc(description = "Disassociate device.") 138 @Throws(Exception::class) disassociatenull139 fun disassociate(associationId: Int) { 140 companionDeviceManager.disassociate(associationId) 141 } 142 143 /** 144 * Clean up all associations. 145 */ 146 @Rpc(description = "Disassociate all associations.") disassociateAllnull147 fun disassociateAll() { 148 companionDeviceManager.myAssociations.forEach { 149 Log.d(TAG, "Disassociating id=${it.id}.") 150 companionDeviceManager.disassociate(it.id) 151 } 152 } 153 154 /** 155 * Initiate system data transfer using Bluetooth socket. 156 */ 157 @Rpc(description = "Start permissions sync.") startPermissionsSyncnull158 fun startPermissionsSync(associationId: Int) { 159 val callback = SystemDataTransferCallback() 160 companionDeviceManager.startSystemDataTransfer(associationId, executor, callback) 161 callback.waitForCompletion() 162 } 163 164 @Rpc(description = "Remove bluetooth bond.") removeBondnull165 fun removeBond(associationId: Int): Boolean { 166 return companionDeviceManager.removeBond(associationId) 167 } 168 169 @Rpc(description = "Start listening for device presence event.") startObservingDevicePresencenull170 fun startObservingDevicePresence(associationId: Int) { 171 val request = ObservingDevicePresenceRequest.Builder() 172 .setAssociationId(associationId) 173 .build() 174 companionDeviceManager.startObservingDevicePresence(request) 175 } 176 177 @Rpc(description = "Stop listening for device presence event.") stopObservingDevicePresencenull178 fun stopObservingDevicePresence(associationId: Int) { 179 val request = ObservingDevicePresenceRequest.Builder() 180 .setAssociationId(associationId) 181 .build() 182 companionDeviceManager.stopObservingDevicePresence(request) 183 } 184 185 @Rpc(description = "Wait for a BT classic device to connect to a test service.") isAssociationBtConnectednull186 fun isAssociationBtConnected(associationId: Int): Boolean { 187 return PrimaryCompanionService.connectedBtBondDevices.stream().anyMatch { 188 it.id == associationId 189 } 190 } 191 192 @Rpc(description = "Attach client socket.") attachClientSocketnull193 fun attachClientSocket(associationId: Int) { 194 btConnector.attachClientSocket(associationId) 195 } 196 197 @Rpc(description = "Attach server socket.") attachServerSocketnull198 fun attachServerSocket(associationId: Int) { 199 btConnector.attachServerSocket(associationId) 200 } 201 202 @Rpc(description = "Remove all sockets.") detachAllSocketsnull203 fun detachAllSockets() { 204 btConnector.closeAllSockets() 205 } 206 207 @Rpc(description = "Check if device is a watch.") isWatchnull208 fun isWatch(): Boolean { 209 return context.packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH) 210 } 211 212 companion object { 213 private const val TAG = "CDM_CompanionDeviceManagerSnippet" 214 } 215 } 216