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.BluetoothManager
20 import android.bluetooth.BluetoothServerSocket
21 import android.bluetooth.BluetoothSocket
22 import android.content.Context
23 import android.util.Log
24 import com.google.protobuf.ByteString
25 import io.grpc.Status
26 import io.grpc.stub.StreamObserver
27 import java.io.Closeable
28 import java.io.IOException
29 import java.io.InputStream
30 import java.io.OutputStream
31 import java.util.UUID
32 import kotlinx.coroutines.CoroutineScope
33 import kotlinx.coroutines.Dispatchers
34 import kotlinx.coroutines.cancel
35 import kotlinx.coroutines.withContext
36 import pandora.RFCOMMGrpc.RFCOMMImplBase
37 import pandora.RfcommProto.*
38 
39 @kotlinx.coroutines.ExperimentalCoroutinesApi
40 class Rfcomm(val context: Context) : RFCOMMImplBase(), Closeable {
41 
42     private val _bufferSize = 512
43 
44     private val TAG = "PandoraRfcomm"
45 
46     private val scope: CoroutineScope = CoroutineScope(Dispatchers.Default.limitedParallelism(1))
47 
48     private val bluetoothManager = context.getSystemService(BluetoothManager::class.java)!!
49     private val bluetoothAdapter = bluetoothManager.adapter
50 
51     private var currentCookie = 0x12FC0 // Non-zero cookie RFCo(mm)
52 
53     data class Connection(
54         val connection: BluetoothSocket,
55         val inputStream: InputStream,
56         val outputStream: OutputStream
57     )
58 
59     private var serverMap: HashMap<Int, BluetoothServerSocket> = hashMapOf()
60     private var connectionMap: HashMap<Int, Connection> = hashMapOf()
61 
closenull62     override fun close() {
63         scope.cancel()
64     }
65 
connectToServernull66     override fun connectToServer(
67         request: ConnectionRequest,
68         responseObserver: StreamObserver<ConnectionResponse>,
69     ) {
70         grpcUnary(scope, responseObserver) {
71             Log.i(TAG, "RFCOMM: connect: request=${request.address}")
72             val device = request.address.toBluetoothDevice(bluetoothAdapter)
73             val clientSocket =
74                 device.createInsecureRfcommSocketToServiceRecord(UUID.fromString(request.uuid))
75             try {
76                 clientSocket.connect()
77             } catch (e: IOException) {
78                 Log.e(TAG, "connect threw ${e}.")
79                 throw Status.UNKNOWN.asException()
80             }
81             Log.i(TAG, "connected.")
82             val connectedClientSocket = currentCookie++
83             // Get the BluetoothSocket input and output streams
84             try {
85                 val tmpIn = clientSocket.inputStream!!
86                 val tmpOut = clientSocket.outputStream!!
87                 connectionMap[connectedClientSocket] = Connection(clientSocket, tmpIn, tmpOut)
88             } catch (e: IOException) {
89                 Log.e(TAG, "temp sockets not created", e)
90             }
91 
92             ConnectionResponse.newBuilder()
93                 .setConnection(RfcommConnection.newBuilder().setId(connectedClientSocket).build())
94                 .build()
95         }
96     }
97 
disconnectnull98     override fun disconnect(
99         request: DisconnectionRequest,
100         responseObserver: StreamObserver<DisconnectionResponse>,
101     ) {
102         grpcUnary(scope, responseObserver) {
103             val id = request.connection.id
104             Log.i(TAG, "RFCOMM: disconnect: request=${id}")
105             if (connectionMap.containsKey(id)) {
106                 connectionMap[id]!!.connection.close()
107                 connectionMap.remove(id)
108             } else {
109                 throw Status.UNKNOWN.asException()
110             }
111             DisconnectionResponse.newBuilder().build()
112         }
113     }
114 
startServernull115     override fun startServer(
116         request: StartServerRequest,
117         responseObserver: StreamObserver<StartServerResponse>,
118     ) {
119         grpcUnary(scope, responseObserver) {
120             Log.i(TAG, "startServer")
121             val serverSocket =
122                 bluetoothAdapter.listenUsingInsecureRfcommWithServiceRecord(
123                     request.name,
124                     UUID.fromString(request.uuid)
125                 )
126             val serverSocketCookie = currentCookie++
127             serverMap[serverSocketCookie] = serverSocket
128 
129             StartServerResponse.newBuilder()
130                 .setServer(ServerId.newBuilder().setId(serverSocketCookie).build())
131                 .build()
132         }
133     }
134 
acceptConnectionnull135     override fun acceptConnection(
136         request: AcceptConnectionRequest,
137         responseObserver: StreamObserver<AcceptConnectionResponse>,
138     ) {
139         grpcUnary(scope, responseObserver) {
140             Log.i(TAG, "accepting: serverSocket= $(request.id)")
141             val acceptedSocketCookie = currentCookie++
142             try {
143                 val acceptedSocket: BluetoothSocket = serverMap[request.server.id]!!.accept(2000)
144                 Log.i(TAG, "accepted: acceptedSocket= $acceptedSocket")
145                 val tmpIn = acceptedSocket.inputStream!!
146                 val tmpOut = acceptedSocket.outputStream!!
147                 connectionMap[acceptedSocketCookie] = Connection(acceptedSocket, tmpIn, tmpOut)
148             } catch (e: IOException) {
149                 Log.e(TAG, "Caught an IOException while trying to accept and create streams.")
150             }
151 
152             Log.i(TAG, "after accept")
153             AcceptConnectionResponse.newBuilder()
154                 .setConnection(RfcommConnection.newBuilder().setId(acceptedSocketCookie).build())
155                 .build()
156         }
157     }
158 
sendnull159     override fun send(
160         request: TxRequest,
161         responseObserver: StreamObserver<TxResponse>,
162     ) {
163         grpcUnary(scope, responseObserver) {
164             if (request.data.isEmpty) {
165                 throw Status.UNKNOWN.asException()
166             }
167             val data = request.data!!.toByteArray()
168 
169             val socketOut = connectionMap[request.connection.id]!!.outputStream
170             withContext(Dispatchers.IO) {
171                 try {
172                     socketOut.write(data)
173                     socketOut.flush()
174                 } catch (e: IOException) {
175                     Log.e(TAG, "Exception while writing output stream", e)
176                 }
177             }
178             Log.i(TAG, "Sent data")
179             TxResponse.newBuilder().build()
180         }
181     }
182 
receivenull183     override fun receive(
184         request: RxRequest,
185         responseObserver: StreamObserver<RxResponse>,
186     ) {
187         grpcUnary(scope, responseObserver) {
188             val data = ByteArray(_bufferSize)
189 
190             val socketIn = connectionMap[request.connection.id]!!.inputStream
191             withContext(Dispatchers.IO) {
192                 try {
193                     socketIn.read(data)
194                 } catch (e: IOException) {
195                     Log.e(TAG, "Exception while reading from input stream", e)
196                 }
197             }
198             Log.i(TAG, "Read data")
199             RxResponse.newBuilder().setData(ByteString.copyFrom(data)).build()
200         }
201     }
202 }
203