1 /*
2  * Copyright 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  *      https://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.devicediagnostics.bluetooth
18 
19 import android.bluetooth.BluetoothAdapter
20 import android.bluetooth.BluetoothDevice
21 import android.bluetooth.BluetoothSocket
22 import android.bluetooth.le.BluetoothLeScanner
23 import android.bluetooth.le.ScanCallback
24 import android.bluetooth.le.ScanFilter
25 import android.bluetooth.le.ScanResult
26 import android.bluetooth.le.ScanSettings
27 import android.os.ParcelUuid
28 import android.util.Log
29 import com.android.devicediagnostics.Protos.BluetoothPacket
30 import com.android.devicediagnostics.Protos.DeviceReport
31 import com.android.devicediagnostics.Protos.PacketCommand
32 import com.android.devicediagnostics.Protos.TrustedDeviceInfo
33 import kotlinx.coroutines.sync.Mutex
34 
35 private const val TAG = "BluetoothClient"
36 
37 abstract class BluetoothClient {
38     interface ScanListener {
onBluetoothConnectednull39         fun onBluetoothConnected(trustedDeviceInfo: TrustedDeviceInfo)
40 
41         fun onBluetoothConnectionFailed()
42     }
43 
44     var connectionData: BluetoothConnectionData? = null
45 
46     abstract fun start(listener: ScanListener)
47 
48     abstract fun sendReport(report: DeviceReport)
49 
50     abstract fun reset()
51 }
52 
53 class BluetoothClientImpl : BluetoothClient() {
54     override fun start(listener: ScanListener) {
55         synchronized(lock) { startImpl(listener) }
56     }
57 
58     private fun startImpl(listener: ScanListener) {
59         this.listener = listener
60         scanner = adapter.bluetoothLeScanner
61         scanCallback = MyScanCallback()
62 
63         val filters = listOf(ScanFilter.Builder().setServiceUuid(ParcelUuid(SERVICE_UUID)).build())
64         val settings = ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_POWER).build()
65         scanner!!.startScan(filters, settings, scanCallback!!)
66         Log.d(TAG, "Started scan $scanner")
67     }
68 
69     private fun readTrustedDeviceInfo(): TrustedDeviceInfo {
70         val message = readBluetoothPacket(socket!!)
71         if (!message.hasTrustedDeviceInfo()) {
72             throw Exception("Expected TrustedDeviceInfo message")
73         }
74         return message.trustedDeviceInfo
75     }
76 
77     private var socket: BluetoothSocket? = null
78     private var listener: ScanListener? = null
79     private val adapter = BluetoothAdapter.getDefaultAdapter()
80     private var scanner: BluetoothLeScanner? = null
81     private var scanCallback: ScanCallback? = null
82     private var lock = Mutex()
83 
84     inner class MyScanCallback : ScanCallback() {
85         override fun onBatchScanResults(results: List<ScanResult>) {
86             for (item in results) {
87                 onScanResult(0, item)
88             }
89         }
90 
91         override fun onScanResult(callbackType: Int, result: ScanResult) {
92             if (result.device == null) {
93                 return
94             }
95 
96             synchronized(lock) {
97                 if (scanCallback != this) {
98                     return
99                 }
100                 connect(result.device)
101             }
102         }
103 
104         override fun onScanFailed(errorCode: Int) {
105             synchronized(lock) {
106                 if (scanCallback != this) {
107                     return
108                 }
109                 Log.d(TAG, "Scan failed with error: $errorCode")
110                 listener?.onBluetoothConnectionFailed()
111             }
112         }
113     }
114 
115     override fun reset() {
116         synchronized(lock) { resetImpl() }
117     }
118 
119     private fun resetImpl() {
120         connectionData = null
121         try {
122             stopScanning()
123             socket?.close()
124         } catch (_: Exception) {}
125         socket = null
126         listener = null
127         scanner = null
128     }
129 
130     private fun stopScanning() {
131         Log.d(TAG, "stopped scan")
132         if (scanCallback != null) {
133             scanner?.stopScan(scanCallback!!)
134             scanCallback = null
135         }
136         adapter.cancelDiscovery()
137         listener = null
138     }
139 
140     override fun sendReport(report: DeviceReport) {
141         synchronized(lock) {
142             val packet = BluetoothPacket.newBuilder().setDeviceReport(report)
143             writeBluetoothPacket(socket!!, packet)
144             val reply = readBluetoothPacket(socket!!)
145             if (reply.command != PacketCommand.COMMAND_ACK) {
146                 throw Exception("Expected acknowledgement packet")
147             }
148             resetImpl()
149         }
150     }
151 
152     private fun connect(device: BluetoothDevice) {
153         var trustedDeviceInfo: TrustedDeviceInfo? = null
154         try {
155             Log.d(TAG, "Found device ${device.name}")
156             socket = device.createInsecureL2capChannel(connectionData!!.psm)
157             socket!!.connect()
158 
159             trustedDeviceInfo = readTrustedDeviceInfo()
160             val challenge = trustedDeviceInfo.challenge.toByteArray()
161             if (!challenge.contentEquals(connectionData!!.challenge)) {
162                 throw Exception("Trusted device has unexpected challenge")
163             }
164             Log.i(TAG, "Bluetooth connection succeeded")
165         } catch (e: Exception) {
166             Log.e(TAG, "Bluetooth connection error: $e")
167             return
168         }
169         listener?.onBluetoothConnected(trustedDeviceInfo)
170         stopScanning()
171     }
172 }
173