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.cts.common.CompanionActivity 26 import android.companion.cts.multidevice.CallbackUtils.SystemDataTransferCallback 27 import android.companion.cts.uicommon.CompanionDeviceManagerUi 28 import android.content.Context 29 import android.os.Handler 30 import android.os.HandlerExecutor 31 import android.os.HandlerThread 32 import android.util.Log 33 import androidx.test.platform.app.InstrumentationRegistry 34 import androidx.test.uiautomator.UiDevice 35 import com.google.android.mobly.snippet.Snippet 36 import com.google.android.mobly.snippet.rpc.Rpc 37 import java.util.concurrent.Executor 38 39 /** 40 * Snippet class that exposes Android APIs in CompanionDeviceManager. 41 */ 42 class CompanionDeviceManagerSnippet : Snippet { 43 private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()!! 44 private val context: Context = instrumentation.targetContext 45 private val companionDeviceManager = context.getSystemService(Context.COMPANION_DEVICE_SERVICE) 46 as CompanionDeviceManager 47 48 private val btManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager 49 private val btConnector = BluetoothConnector(btManager.adapter, companionDeviceManager) 50 <lambda>null51 private val uiDevice by lazy { UiDevice.getInstance(instrumentation) } <lambda>null52 private val confirmationUi by lazy { CompanionDeviceManagerUi(uiDevice) } 53 54 private val handlerThread = HandlerThread("Snippet-Aware") 55 private val handler: Handler 56 private val executor: Executor 57 58 init { 59 handlerThread.start() 60 handler = Handler(handlerThread.looper) 61 executor = HandlerExecutor(handler) 62 } 63 64 /** 65 * Associate with a nearby device with given name and return newly-created association ID. 66 */ 67 @Rpc(description = "Start device association flow.") 68 @Throws(Exception::class) associatenull69 fun associate(deviceAddress: String): Int { 70 val filter = BluetoothDeviceFilter.Builder() 71 .setAddress(deviceAddress) 72 .build() 73 val request = AssociationRequest.Builder() 74 .setSingleDevice(true) 75 .addDeviceFilter(filter) 76 .build() 77 val callback = CallbackUtils.AssociationCallback() 78 companionDeviceManager.associate(request, callback, handler) 79 val pendingConfirmation = callback.waitForPendingIntent() 80 ?: throw RuntimeException("Association is pending but intent sender is null.") 81 CompanionActivity.launchAndWait(context) 82 CompanionActivity.startIntentSender(pendingConfirmation) 83 confirmationUi.waitUntilVisible() 84 confirmationUi.waitUntilPositiveButtonIsEnabledAndClick() 85 confirmationUi.waitUntilGone() 86 87 val (_, result) = CompanionActivity.waitForActivityResult() 88 CompanionActivity.safeFinish() 89 CompanionActivity.waitUntilGone() 90 91 if (result == null) { 92 throw RuntimeException("Association result can't be null.") 93 } 94 95 val association = checkNotNull(result.getParcelableExtra( 96 CompanionDeviceManager.EXTRA_ASSOCIATION, 97 AssociationInfo::class.java 98 )) 99 100 return association.id 101 } 102 103 /** 104 * Request user consent to system data transfer and accept. 105 */ 106 @Rpc(description = "Start permissions sync.") requestPermissionTransferUserConsentnull107 fun requestPermissionTransferUserConsent(associationId: Int) { 108 val pendingIntent = checkNotNull( 109 companionDeviceManager.buildPermissionTransferUserConsentIntent(associationId) 110 ) 111 CompanionActivity.launchAndWait(context) 112 CompanionActivity.startIntentSender(pendingIntent) 113 confirmationUi.waitUntilSystemDataTransferConfirmationVisible() 114 confirmationUi.clickPositiveButton() 115 confirmationUi.waitUntilGone() 116 117 CompanionActivity.waitForActivityResult() 118 CompanionActivity.safeFinish() 119 CompanionActivity.waitUntilGone() 120 } 121 122 /** 123 * Returns the list of association IDs owned by the test app. 124 */ 125 @Rpc(description = "Get my association IDs.") 126 @Throws(Exception::class) getMyAssociationsnull127 fun getMyAssociations(): List<Int> { 128 return companionDeviceManager.myAssociations.stream().map { it.id }.toList() 129 } 130 131 /** 132 * Disassociate an association with given ID. 133 */ 134 @Rpc(description = "Disassociate device.") 135 @Throws(Exception::class) disassociatenull136 fun disassociate(associationId: Int) { 137 companionDeviceManager.disassociate(associationId) 138 } 139 140 /** 141 * Clean up all associations. 142 */ 143 @Rpc(description = "Disassociate all associations.") disassociateAllnull144 fun disassociateAll() { 145 companionDeviceManager.myAssociations.forEach { 146 Log.d(TAG, "Disassociating id=${it.id}.") 147 companionDeviceManager.disassociate(it.id) 148 } 149 } 150 151 /** 152 * Initiate system data transfer using Bluetooth socket. 153 */ 154 @Rpc(description = "Start permissions sync.") startPermissionsSyncnull155 fun startPermissionsSync(associationId: Int) { 156 val callback = SystemDataTransferCallback() 157 companionDeviceManager.startSystemDataTransfer(associationId, executor, callback) 158 callback.waitForCompletion() 159 } 160 161 @Rpc(description = "Remove bluetooth bond.") removeBondnull162 fun removeBond(associationId: Int): Boolean { 163 return companionDeviceManager.removeBond(associationId) 164 } 165 166 @Rpc(description = "Attach client socket.") attachClientSocketnull167 fun attachClientSocket(associationId: Int) { 168 btConnector.attachClientSocket(associationId) 169 } 170 171 @Rpc(description = "Attach server socket.") attachServerSocketnull172 fun attachServerSocket(associationId: Int) { 173 btConnector.attachServerSocket(associationId) 174 } 175 176 @Rpc(description = "Remove all sockets.") detachAllSocketsnull177 fun detachAllSockets() { 178 btConnector.closeAllSockets() 179 } 180 181 companion object { 182 private const val TAG = "CDM_CompanionDeviceManagerSnippet" 183 } 184 } 185