1 /* 2 * 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.multidevices 18 19 import android.app.Instrumentation 20 import android.bluetooth.BluetoothAdapter 21 import android.bluetooth.BluetoothManager 22 import android.companion.AssociationInfo 23 import android.companion.AssociationRequest 24 import android.companion.BluetoothDeviceFilter 25 import android.companion.CompanionDeviceManager 26 import android.companion.CompanionException 27 import android.companion.cts.common.CompanionActivity 28 import android.companion.multidevices.CallbackUtils.AssociationCallback 29 import android.companion.multidevices.CallbackUtils.SystemDataTransferCallback 30 import android.companion.multidevices.bluetooth.BluetoothConnector 31 import android.companion.multidevices.bluetooth.BluetoothController 32 import android.companion.cts.uicommon.CompanionDeviceManagerUi 33 import android.content.Context 34 import android.os.Handler 35 import android.os.HandlerExecutor 36 import android.os.HandlerThread 37 import android.util.Log 38 import androidx.test.platform.app.InstrumentationRegistry 39 import androidx.test.uiautomator.UiDevice 40 import com.google.android.mobly.snippet.Snippet 41 import com.google.android.mobly.snippet.event.EventCache 42 import com.google.android.mobly.snippet.rpc.Rpc 43 import java.util.concurrent.Executor 44 import java.util.regex.Pattern 45 46 /** 47 * Snippet class that exposes Android APIs in CompanionDeviceManager. 48 */ 49 class CompanionDeviceManagerSnippet : Snippet { 50 private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()!! 51 private val context: Context = instrumentation.targetContext 52 <lambda>null53 private val btAdapter: BluetoothAdapter by lazy { 54 (context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter 55 } <lambda>null56 private val companionDeviceManager: CompanionDeviceManager by lazy { 57 context.getSystemService(Context.COMPANION_DEVICE_SERVICE) as CompanionDeviceManager 58 } <lambda>null59 private val btConnector: BluetoothConnector by lazy { 60 BluetoothConnector(btAdapter, companionDeviceManager) 61 } 62 <lambda>null63 private val uiDevice by lazy { UiDevice.getInstance(instrumentation) } <lambda>null64 private val confirmationUi by lazy { CompanionDeviceManagerUi(uiDevice) } <lambda>null65 private val btController by lazy { BluetoothController(context, btAdapter, uiDevice) } 66 67 private val eventCache = EventCache.getInstance() 68 private val handlerThread = HandlerThread("Snippet-Aware") 69 private val handler: Handler 70 private val executor: Executor 71 72 init { 73 handlerThread.start() 74 handler = Handler(handlerThread.looper) 75 executor = HandlerExecutor(handler) 76 } 77 78 /** 79 * Make device discoverable to other devices via BLE and return device name. 80 */ 81 @Rpc(description = "Start advertising device to be discoverable.") becomeDiscoverablenull82 fun becomeDiscoverable(): String { 83 btController.becomeDiscoverable() 84 return btAdapter.name 85 } 86 87 /** 88 * Associate with a nearby device with given name and return newly-created association ID. 89 */ 90 @Rpc(description = "Start device association flow.") 91 @Throws(Exception::class) associatenull92 fun associate(deviceName: String): Int { 93 val filter = BluetoothDeviceFilter.Builder() 94 .setNamePattern(Pattern.compile(deviceName)) 95 .build() 96 val request = AssociationRequest.Builder() 97 .setSingleDevice(true) 98 .addDeviceFilter(filter) 99 .build() 100 val callback = AssociationCallback() 101 companionDeviceManager.associate(request, callback, handler) 102 val pendingConfirmation = callback.waitForPendingIntent() 103 ?: throw CompanionException("Association is pending but intent sender is null.") 104 CompanionActivity.launchAndWait(context) 105 CompanionActivity.startIntentSender(pendingConfirmation) 106 confirmationUi.waitUntilVisible() 107 confirmationUi.waitUntilPositiveButtonIsEnabledAndClick() 108 confirmationUi.waitUntilGone() 109 110 val (_, result) = CompanionActivity.waitForActivityResult() 111 if (result == null) { 112 throw CompanionException("Association result can't be null.") 113 } 114 115 val association = checkNotNull(result.getParcelableExtra( 116 CompanionDeviceManager.EXTRA_ASSOCIATION, 117 AssociationInfo::class.java 118 )) 119 val remoteDevice = association.associatedDevice?.getBluetoothDevice()!! 120 121 // Register associated device 122 btConnector.registerDevice(association.id, remoteDevice) 123 124 return association.id 125 } 126 127 /** 128 * Disassociate an association with given ID. 129 */ 130 @Rpc(description = "Disassociate device.") 131 @Throws(Exception::class) disassociatenull132 fun disassociate(associationId: Int) { 133 companionDeviceManager.disassociate(associationId) 134 } 135 136 /** 137 * Consent to system data transfer and carry it out using Bluetooth socket. 138 */ 139 @Rpc(description = "Start permissions sync.") startPermissionsSyncnull140 fun startPermissionsSync(associationId: Int) { 141 val pendingIntent = checkNotNull(companionDeviceManager 142 .buildPermissionTransferUserConsentIntent(associationId)) 143 CompanionActivity.launchAndWait(context) 144 CompanionActivity.startIntentSender(pendingIntent) 145 confirmationUi.waitUntilSystemDataTransferConfirmationVisible() 146 confirmationUi.clickPositiveButton() 147 confirmationUi.waitUntilGone() 148 149 CompanionActivity.waitForActivityResult() 150 151 val callback = SystemDataTransferCallback() 152 companionDeviceManager.startSystemDataTransfer(associationId, executor, callback) 153 callback.waitForCompletion() 154 } 155 156 @Rpc(description = "Attach transport to the BT client socket.") attachClientSocketnull157 fun attachClientSocket(id: Int) { 158 btConnector.attachClientSocket(id) 159 } 160 161 @Rpc(description = "Attach transport to the BT server socket.") attachServerSocketnull162 fun attachServerSocket(id: Int) { 163 btConnector.attachServerSocket(id) 164 } 165 166 @Rpc(description = "Close all open sockets.") closeAllSocketsnull167 fun closeAllSockets() { 168 // Close all open sockets 169 btConnector.closeAllSockets() 170 } 171 172 @Rpc(description = "Disassociate all associations.") disassociateAllnull173 fun disassociateAll() { 174 companionDeviceManager.myAssociations.forEach { 175 Log.d(TAG, "Disassociating id=${it.id}.") 176 companionDeviceManager.disassociate(it.id) 177 } 178 } 179 180 companion object { 181 private const val TAG = "CDM_CompanionDeviceManagerSnippet" 182 } 183 } 184