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.bluetooth.BluetoothAdapter
20 import android.bluetooth.BluetoothDevice
21 import android.bluetooth.BluetoothServerSocket
22 import android.bluetooth.BluetoothSocket
23 import android.companion.CompanionDeviceManager
24 import android.util.Log
25 import java.io.IOException
26 import java.util.UUID
27 
28 class BluetoothConnector(
29     private val adapter: BluetoothAdapter,
30     private val cdm: CompanionDeviceManager
31 ) {
32     companion object {
33         private const val TAG = "CDM_BluetoothServer"
34 
35         private val SERVICE_NAME = "CDM_BluetoothChannel"
36         private val SERVICE_UUID = UUID.fromString("435fe1d9-56c5-455d-a516-d5e6b22c52f9")
37 
38         // Registry of bluetooth server threads
39         private val serverThreads = mutableMapOf<Int, BluetoothServerThread>()
40 
41         // Set of connected client sockets
42         private val clientSockets = mutableMapOf<Int, BluetoothSocket>()
43     }
44 
attachClientSocketnull45     fun attachClientSocket(associationId: Int) {
46         try {
47             val device = getRemoteDevice(associationId)
48             val socket = device.createRfcommSocketToServiceRecord(SERVICE_UUID)
49             if (clientSockets.containsKey(associationId)) {
50                 detachClientSocket(associationId)
51                 clientSockets[associationId] = socket
52             } else {
53                 clientSockets += associationId to socket
54             }
55 
56             socket.connect()
57             Log.d(TAG, "Attaching client socket $socket.")
58             cdm.attachSystemDataTransport(
59                     associationId,
60                     socket.inputStream,
61                     socket.outputStream
62             )
63         } catch (e: IOException) {
64             Log.e(TAG, "Failed to attach client socket.", e)
65             throw RuntimeException(e)
66         }
67     }
68 
attachServerSocketnull69     fun attachServerSocket(associationId: Int) {
70         val serverThread: BluetoothServerThread
71         if (serverThreads.containsKey(associationId)) {
72             serverThread = serverThreads[associationId]!!
73         } else {
74             serverThread = BluetoothServerThread(associationId)
75             serverThreads += associationId to serverThread
76         }
77 
78         // Start thread
79         if (!serverThread.isOpen) {
80             serverThread.start()
81         }
82     }
83 
closeAllSocketsnull84     fun closeAllSockets() {
85         val iter = clientSockets.keys.iterator()
86         while (iter.hasNext()) {
87             detachClientSocket(iter.next())
88         }
89         for (thread in serverThreads.values) {
90             thread.shutdown()
91         }
92         serverThreads.clear()
93     }
94 
getRemoteDevicenull95     private fun getRemoteDevice(associationId: Int): BluetoothDevice {
96         val association = cdm.myAssociations.stream()
97                 .filter { it.id == associationId }
98                 .findAny()
99                 .orElseThrow()
100         val address = association.deviceMacAddress
101         return adapter.getRemoteDevice(address.toString().uppercase())
102     }
103 
detachClientSocketnull104     private fun detachClientSocket(associationId: Int) {
105         try {
106             Log.d(TAG, "Detaching client socket.")
107             cdm.detachSystemDataTransport(associationId)
108             clientSockets[associationId]?.close()
109         } catch (e: IOException) {
110             Log.e(TAG, "Failed to detach client socket.", e)
111             throw RuntimeException(e)
112         }
113     }
114 
115     inner class BluetoothServerThread(
116         private val associationId: Int
117     ) : Thread() {
118         private lateinit var mServerSocket: BluetoothServerSocket
119 
120         var isOpen = false
121 
runnull122         override fun run() {
123             try {
124                 Log.d(TAG, "Listening for remote connections...")
125                 mServerSocket = adapter.listenUsingRfcommWithServiceRecord(
126                         SERVICE_NAME,
127                         SERVICE_UUID
128                 )
129                 isOpen = true
130                 do {
131                     val socket = mServerSocket.accept()
132                     Log.d(TAG, "Attaching server socket $socket.")
133                     cdm.attachSystemDataTransport(
134                             associationId,
135                             socket.inputStream,
136                             socket.outputStream
137                     )
138                 } while (isOpen)
139             } catch (e: IOException) {
140                 throw RuntimeException(e)
141             }
142         }
143 
shutdownnull144         fun shutdown() {
145             if (!isOpen || !this::mServerSocket.isInitialized) return
146 
147             try {
148                 Log.d(TAG, "Closing server socket.")
149                 cdm.detachSystemDataTransport(associationId)
150                 mServerSocket.close()
151                 isOpen = false
152             } catch (e: IOException) {
153                 throw RuntimeException(e)
154             }
155         }
156     }
157 }
158