• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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