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