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 com.android.pandora
18 
19 import android.bluetooth.BluetoothDevice
20 import android.bluetooth.BluetoothGatt
21 import android.bluetooth.BluetoothGattCallback
22 import android.bluetooth.BluetoothGattCharacteristic
23 import android.bluetooth.BluetoothGattDescriptor
24 import android.bluetooth.BluetoothGattService
25 import android.bluetooth.BluetoothProfile
26 import android.bluetooth.BluetoothStatusCodes
27 import android.content.Context
28 import android.util.Log
29 import com.google.protobuf.ByteString
30 import java.util.UUID
31 import kotlinx.coroutines.flow.MutableStateFlow
32 import kotlinx.coroutines.flow.first
33 import pandora.GattProto.*
34 
35 /** GattInstance extends and simplifies Android GATT APIs without re-implementing them. */
36 @kotlinx.coroutines.ExperimentalCoroutinesApi
37 class GattInstance(val mDevice: BluetoothDevice, val mTransport: Int, val mContext: Context) {
38     private val TAG = "GattInstance"
39     public val mGatt: BluetoothGatt
40 
41     private var mServiceDiscovered = MutableStateFlow(false)
42     private var mConnectionState = MutableStateFlow(BluetoothProfile.STATE_DISCONNECTED)
43     private var mValuesRead = MutableStateFlow(0)
44     private var mValueWrote = MutableStateFlow(false)
45     private var mOnCharacteristicChanged = MutableStateFlow(false)
46     private var mCharacteristicChangedMap: MutableMap<BluetoothGattCharacteristic, Boolean> =
47         mutableMapOf()
48 
49     /**
50      * Wrapper for characteristic and descriptor reading. Uuid, startHandle and endHandle are used
51      * to compare with the callback returned object. Value and status can be read once the read has
52      * been done. ByteString and AttStatusCode are used to ensure compatibility with proto.
53      */
54     class GattInstanceValueRead(
55         var uuid: UUID?,
56         var handle: Int,
57         var value: ByteString?,
58         var status: AttStatusCode
59     ) {}
60     private var mGattInstanceValuesRead = arrayListOf<GattInstanceValueRead>()
61 
62     class GattInstanceValueWrote(var uuid: UUID?, var handle: Int, var status: AttStatusCode) {}
63     private var mGattInstanceValueWrote =
64         GattInstanceValueWrote(null, 0, AttStatusCode.UNKNOWN_ERROR)
65 
66     companion object GattManager {
67         val gattInstances: MutableMap<String, GattInstance> = mutableMapOf<String, GattInstance>()
getnull68         fun get(address: String): GattInstance {
69             val instance = gattInstances.get(address)
70             requireNotNull(instance) { "Unable to find GATT instance for $address" }
71             return instance
72         }
getnull73         fun get(address: ByteString): GattInstance {
74             val instance = gattInstances.get(address.toByteArray().decodeToString())
75             requireNotNull(instance) { "Unable to find GATT instance for $address" }
76             return instance
77         }
clearAllInstancesnull78         fun clearAllInstances() {
79             gattInstances.clear()
80         }
81     }
82 
83     private val mCallback =
84         object : BluetoothGattCallback() {
onConnectionStateChangenull85             override fun onConnectionStateChange(
86                 bluetoothGatt: BluetoothGatt,
87                 status: Int,
88                 newState: Int
89             ) {
90                 Log.i(TAG, "$mDevice connection state changed to $newState")
91                 mConnectionState.value = newState
92                 if (newState == BluetoothProfile.STATE_DISCONNECTED) {
93                     gattInstances.remove(mDevice.address)
94                 }
95             }
96 
onServicesDiscoverednull97             override fun onServicesDiscovered(bluetoothGatt: BluetoothGatt, status: Int) {
98                 if (status == BluetoothGatt.GATT_SUCCESS) {
99                     Log.i(TAG, "Services have been discovered for $mDevice")
100                     mServiceDiscovered.value = true
101                 }
102             }
103 
onCharacteristicReadnull104             override fun onCharacteristicRead(
105                 bluetoothGatt: BluetoothGatt,
106                 characteristic: BluetoothGattCharacteristic,
107                 value: ByteArray,
108                 status: Int
109             ) {
110                 Log.i(TAG, "onCharacteristicRead, status: $status")
111                 for (gattInstanceValueRead: GattInstanceValueRead in mGattInstanceValuesRead) {
112                     if (
113                         characteristic.getUuid() == gattInstanceValueRead.uuid &&
114                             characteristic.getInstanceId() == gattInstanceValueRead.handle
115                     ) {
116                         gattInstanceValueRead.value = ByteString.copyFrom(value)
117                         gattInstanceValueRead.status = AttStatusCode.forNumber(status)
118                         mValuesRead.value++
119                     }
120                 }
121             }
122 
onDescriptorReadnull123             override fun onDescriptorRead(
124                 bluetoothGatt: BluetoothGatt,
125                 descriptor: BluetoothGattDescriptor,
126                 status: Int,
127                 value: ByteArray
128             ) {
129                 Log.i(TAG, "onDescriptorRead, status: $status")
130                 for (gattInstanceValueRead: GattInstanceValueRead in mGattInstanceValuesRead) {
131                     if (
132                         descriptor.getUuid() == gattInstanceValueRead.uuid &&
133                             descriptor.getInstanceId() >= gattInstanceValueRead.handle
134                     ) {
135                         gattInstanceValueRead.value = ByteString.copyFrom(value)
136                         gattInstanceValueRead.status = AttStatusCode.forNumber(status)
137                         mValuesRead.value++
138                     }
139                 }
140             }
141 
onCharacteristicWritenull142             override fun onCharacteristicWrite(
143                 bluetoothGatt: BluetoothGatt,
144                 characteristic: BluetoothGattCharacteristic,
145                 status: Int
146             ) {
147                 Log.i(TAG, "onCharacteristicWrite, status: $status")
148                 mGattInstanceValueWrote.status = AttStatusCode.forNumber(status)
149                 mValueWrote.value = true
150             }
151 
onDescriptorWritenull152             override fun onDescriptorWrite(
153                 bluetoothGatt: BluetoothGatt,
154                 descriptor: BluetoothGattDescriptor,
155                 status: Int
156             ) {
157                 Log.i(TAG, "onDescriptorWrite, status: $status")
158                 mGattInstanceValueWrote.status = AttStatusCode.forNumber(status)
159                 mValueWrote.value = true
160             }
161 
onCharacteristicChangednull162             override fun onCharacteristicChanged(
163                 bluetoothGatt: BluetoothGatt,
164                 characteristic: BluetoothGattCharacteristic,
165                 value: ByteArray
166             ) {
167                 Log.i(
168                     TAG,
169                     "onCharacteristicChanged, characteristic: " +
170                         characteristic.getUuid().toString().uppercase()
171                 )
172                 mCharacteristicChangedMap[characteristic] = true
173                 mOnCharacteristicChanged.value = true
174             }
175         }
176 
177     init {
178         if (!isBLETransport()) {
<lambda>null179             require(isBonded()) { "Trying to connect non BLE GATT on a not bonded device $mDevice" }
180         }
<lambda>null181         require(gattInstances.get(mDevice.address) == null) {
182             "Trying to connect GATT on an already connected device $mDevice"
183         }
184 
185         mGatt = mDevice.connectGatt(mContext, false, mCallback, mTransport)
186 
<lambda>null187         checkNotNull(mGatt) { "Failed to connect GATT on $mDevice" }
188         gattInstances.put(mDevice.address, this)
189     }
190 
isConnectednull191     public fun isConnected(): Boolean {
192         return mConnectionState.value == BluetoothProfile.STATE_CONNECTED
193     }
194 
isDisconnectednull195     public fun isDisconnected(): Boolean {
196         return mConnectionState.value == BluetoothProfile.STATE_DISCONNECTED
197     }
198 
isBondednull199     public fun isBonded(): Boolean {
200         return mDevice.getBondState() == BluetoothDevice.BOND_BONDED
201     }
202 
isBLETransportnull203     public fun isBLETransport(): Boolean {
204         return mTransport == BluetoothDevice.TRANSPORT_LE
205     }
206 
servicesDiscoverednull207     public fun servicesDiscovered(): Boolean {
208         return mServiceDiscovered.value
209     }
210 
waitForOnCharacteristicChangednull211     public suspend fun waitForOnCharacteristicChanged(
212         characteristic: BluetoothGattCharacteristic
213     ): Boolean {
214         if (mOnCharacteristicChanged.value == false) {
215             mOnCharacteristicChanged.first { it == true }
216         }
217         return mCharacteristicChangedMap[characteristic] == true
218     }
219 
waitForStatenull220     public suspend fun waitForState(newState: Int) {
221         if (mConnectionState.value != newState) {
222             mConnectionState.first { it == newState }
223         }
224     }
225 
waitForDiscoveryEndnull226     public suspend fun waitForDiscoveryEnd() {
227         if (mServiceDiscovered.value != true) {
228             mServiceDiscovered.first { it == true }
229         }
230     }
231 
waitForValuesReadEndnull232     public suspend fun waitForValuesReadEnd() {
233         if (mValuesRead.value < mGattInstanceValuesRead.size) {
234             mValuesRead.first { it == mGattInstanceValuesRead.size }
235         }
236         mValuesRead.value = 0
237     }
238 
waitForValuesReadnull239     public suspend fun waitForValuesRead() {
240         if (mValuesRead.value < mGattInstanceValuesRead.size) {
241             mValuesRead.first { it == mGattInstanceValuesRead.size }
242         }
243     }
244 
waitForWriteEndnull245     public suspend fun waitForWriteEnd() {
246         if (mValueWrote.value != true) {
247             mValueWrote.first { it == true }
248         }
249         mValueWrote.value = false
250     }
251 
readCharacteristicBlockingnull252     public suspend fun readCharacteristicBlocking(
253         characteristic: BluetoothGattCharacteristic
254     ): GattInstanceValueRead {
255         // Init mGattInstanceValuesRead with characteristic values.
256         mGattInstanceValuesRead =
257             arrayListOf(
258                 GattInstanceValueRead(
259                     characteristic.getUuid(),
260                     characteristic.getInstanceId(),
261                     ByteString.EMPTY,
262                     AttStatusCode.UNKNOWN_ERROR
263                 )
264             )
265         if (mGatt.readCharacteristic(characteristic)) {
266             waitForValuesReadEnd()
267         }
268         // This method read only one characteristic.
269         return mGattInstanceValuesRead.get(0)
270     }
271 
readCharacteristicUuidBlockingnull272     public suspend fun readCharacteristicUuidBlocking(
273         uuid: UUID,
274         startHandle: Int,
275         endHandle: Int
276     ): ArrayList<GattInstanceValueRead> {
277         mGattInstanceValuesRead = arrayListOf()
278         // Init mGattInstanceValuesRead with characteristics values.
279         for (service: BluetoothGattService in mGatt.services.orEmpty()) {
280             for (characteristic: BluetoothGattCharacteristic in service.characteristics) {
281                 if (
282                     characteristic.getUuid() == uuid &&
283                         characteristic.getInstanceId() >= startHandle &&
284                         characteristic.getInstanceId() <= endHandle
285                 ) {
286                     mGattInstanceValuesRead.add(
287                         GattInstanceValueRead(
288                             uuid,
289                             characteristic.getInstanceId(),
290                             ByteString.EMPTY,
291                             AttStatusCode.UNKNOWN_ERROR
292                         )
293                     )
294                     check(
295                         mGatt.readUsingCharacteristicUuid(
296                             uuid,
297                             characteristic.getInstanceId(),
298                             characteristic.getInstanceId()
299                         )
300                     )
301                     waitForValuesRead()
302                 }
303             }
304         }
305         // All needed characteristics are read.
306         mValuesRead.value = 0
307 
308         // When PTS tests with wrong UUID, we return an empty GattInstanceValueRead
309         // with UNKNOWN_ERROR so the MMI can confirm the fail. We also have to try
310         // and read the characteristic anyway for the PTS to validate the test.
311         if (mGattInstanceValuesRead.size == 0) {
312             mGattInstanceValuesRead.add(
313                 GattInstanceValueRead(
314                     uuid,
315                     startHandle,
316                     ByteString.EMPTY,
317                     AttStatusCode.UNKNOWN_ERROR
318                 )
319             )
320             mGatt.readUsingCharacteristicUuid(uuid, startHandle, endHandle)
321         }
322         return mGattInstanceValuesRead
323     }
324 
readDescriptorBlockingnull325     public suspend fun readDescriptorBlocking(
326         descriptor: BluetoothGattDescriptor
327     ): GattInstanceValueRead {
328         // Init mGattInstanceValuesRead with descriptor values.
329         mGattInstanceValuesRead =
330             arrayListOf(
331                 GattInstanceValueRead(
332                     descriptor.getUuid(),
333                     descriptor.getInstanceId(),
334                     ByteString.EMPTY,
335                     AttStatusCode.UNKNOWN_ERROR
336                 )
337             )
338         if (mGatt.readDescriptor(descriptor)) {
339             waitForValuesReadEnd()
340         }
341         // This method read only one descriptor.
342         return mGattInstanceValuesRead.get(0)
343     }
344 
writeCharacteristicBlockingnull345     public suspend fun writeCharacteristicBlocking(
346         characteristic: BluetoothGattCharacteristic,
347         value: ByteArray
348     ): GattInstanceValueWrote {
349         GattInstanceValueWrote(
350             characteristic.getUuid(),
351             characteristic.getInstanceId(),
352             AttStatusCode.UNKNOWN_ERROR
353         )
354         if (
355             mGatt.writeCharacteristic(
356                 characteristic,
357                 value,
358                 BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
359             ) == BluetoothStatusCodes.SUCCESS
360         ) {
361             waitForWriteEnd()
362         }
363         return mGattInstanceValueWrote
364     }
365 
writeDescriptorBlockingnull366     public suspend fun writeDescriptorBlocking(
367         descriptor: BluetoothGattDescriptor,
368         value: ByteArray
369     ): GattInstanceValueWrote {
370         GattInstanceValueWrote(
371             descriptor.getUuid(),
372             descriptor.getInstanceId(),
373             AttStatusCode.UNKNOWN_ERROR
374         )
375         if (mGatt.writeDescriptor(descriptor, value) == BluetoothStatusCodes.SUCCESS) {
376             waitForWriteEnd()
377         }
378         return mGattInstanceValueWrote
379     }
380 
disconnectInstancenull381     public fun disconnectInstance() {
382         require(isConnected()) { "Trying to disconnect an already disconnected device $mDevice" }
383         mGatt.disconnect()
384         gattInstances.remove(mDevice.address)
385     }
386 
toStringnull387     override fun toString(): String {
388         return "GattInstance($mDevice)"
389     }
390 }
391