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