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 com.android.pandora
18 
19 import android.bluetooth.BluetoothDevice
20 import android.bluetooth.BluetoothManager
21 import android.bluetooth.BluetoothSocket
22 import android.content.Context
23 import android.util.Log
24 import com.google.protobuf.Any
25 import com.google.protobuf.ByteString
26 import io.grpc.stub.StreamObserver
27 import java.io.Closeable
28 import java.io.IOException
29 import java.util.concurrent.atomic.AtomicLong
30 import kotlinx.coroutines.CoroutineScope
31 import kotlinx.coroutines.Dispatchers
32 import kotlinx.coroutines.cancel
33 import kotlinx.coroutines.flow.flow
34 import pandora.l2cap.L2CAPGrpc.L2CAPImplBase
35 import pandora.l2cap.L2CAPProto.*
36 
37 @kotlinx.coroutines.ExperimentalCoroutinesApi
38 class L2cap(val context: Context) : L2CAPImplBase(), Closeable {
39     private val TAG = "PandoraL2cap"
40     private val scope: CoroutineScope
41     private val BLUETOOTH_SERVER_SOCKET_TIMEOUT: Int = 10000
42     private val channelIdCounter = AtomicLong(1)
43     private val BUFFER_SIZE = 512
44 
45     private val bluetoothManager =
46         context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
47     private val bluetoothAdapter = bluetoothManager.adapter
48     private val channels: HashMap<Long, BluetoothSocket> = hashMapOf()
49 
50     init {
51         // Init the CoroutineScope
52         scope = CoroutineScope(Dispatchers.Default.limitedParallelism(1))
53     }
54 
closenull55     override fun close() {
56         // Deinit the CoroutineScope
57         scope.cancel()
58     }
59 
connectnull60     override fun connect(
61         request: ConnectRequest,
62         responseObserver: StreamObserver<ConnectResponse>,
63     ) {
64         grpcUnary(scope, responseObserver) {
65             val device = request.connection.toBluetoothDevice(bluetoothAdapter)
66 
67             val psm =
68                 when {
69                     request.hasBasic() -> request.basic.psm
70                     request.hasLeCreditBased() -> request.leCreditBased.spsm
71                     request.hasEnhancedCreditBased() -> request.enhancedCreditBased.spsm
72                     else -> throw RuntimeException("unreachable")
73                 }
74             Log.i(TAG, "connect: $device psm: $psm")
75 
76             val bluetoothSocket = device.createInsecureL2capChannel(psm)
77             bluetoothSocket.connect()
78             val channelId = getNewChannelId()
79             channels.put(channelId, bluetoothSocket)
80 
81             Log.d(TAG, "connect: channelId=$channelId")
82             ConnectResponse.newBuilder().setChannel(craftChannel(channelId)).build()
83         }
84     }
85 
waitConnectionnull86     override fun waitConnection(
87         request: WaitConnectionRequest,
88         responseObserver: StreamObserver<WaitConnectionResponse>,
89     ) {
90         grpcUnary(scope, responseObserver) {
91             val device: BluetoothDevice? =
92                 try {
93                     request.connection.toBluetoothDevice(bluetoothAdapter)
94                 } catch (e: Exception) {
95                     Log.w(TAG, e)
96                     null
97                 }
98 
99             Log.i(TAG, "waitConnection: $device")
100 
101             val psm =
102                 when {
103                     request.hasBasic() -> request.basic.psm
104                     request.hasLeCreditBased() -> request.leCreditBased.spsm
105                     request.hasEnhancedCreditBased() -> request.enhancedCreditBased.spsm
106                     else -> throw RuntimeException("unreachable")
107                 }
108 
109             var bluetoothSocket: BluetoothSocket?
110 
111             while (true) {
112                 val bluetoothServerSocket =
113                     if (psm == 0) {
114                         bluetoothAdapter.listenUsingInsecureL2capChannel()
115                     } else {
116                         bluetoothAdapter.listenUsingInsecureL2capOn(psm)
117                     }
118                 bluetoothSocket = bluetoothServerSocket.accept()
119                 bluetoothServerSocket.close()
120                 if (device != null && !bluetoothSocket.getRemoteDevice().equals(device)) continue
121                 break
122             }
123 
124             val channelId = getNewChannelId()
125             channels.put(channelId, bluetoothSocket!!)
126 
127             Log.d(TAG, "waitConnection: channelId=$channelId")
128             WaitConnectionResponse.newBuilder().setChannel(craftChannel(channelId)).build()
129         }
130     }
131 
disconnectnull132     override fun disconnect(
133         request: DisconnectRequest,
134         responseObserver: StreamObserver<DisconnectResponse>,
135     ) {
136         grpcUnary(scope, responseObserver) {
137             val channel = request.channel
138             val bluetoothSocket = channel.toBluetoothSocket(channels)
139             Log.i(TAG, "disconnect: ${channel.id()} ")
140 
141             try {
142                 bluetoothSocket.close()
143                 DisconnectResponse.getDefaultInstance()
144             } catch (e: IOException) {
145                 Log.e(TAG, "disconnect: exception while closing the socket: $e")
146 
147                 DisconnectResponse.newBuilder()
148                     .setErrorValue(CommandRejectReason.COMMAND_NOT_UNDERSTOOD_VALUE)
149                     .build()
150             } finally {
151                 channels.remove(channel.id())
152             }
153         }
154     }
155 
waitDisconnectionnull156     override fun waitDisconnection(
157         request: WaitDisconnectionRequest,
158         responseObserver: StreamObserver<WaitDisconnectionResponse>,
159     ) {
160         grpcUnary(scope, responseObserver) {
161             Log.i(TAG, "waitDisconnection: ${request.channel.id()}")
162             val bluetoothSocket = request.channel.toBluetoothSocket(channels)
163 
164             while (bluetoothSocket.isConnected()) Thread.sleep(100)
165 
166             WaitDisconnectionResponse.getDefaultInstance()
167         }
168     }
169 
sendnull170     override fun send(request: SendRequest, responseObserver: StreamObserver<SendResponse>) {
171         grpcUnary(scope, responseObserver) {
172             Log.i(TAG, "send")
173             val bluetoothSocket = request.channel.toBluetoothSocket(channels)
174             val outputStream = bluetoothSocket.outputStream
175 
176             outputStream.write(request.data.toByteArray())
177             outputStream.flush()
178 
179             SendResponse.newBuilder().build()
180         }
181     }
182 
receivenull183     override fun receive(
184         request: ReceiveRequest,
185         responseObserver: StreamObserver<ReceiveResponse>,
186     ) {
187         Log.i(TAG, "receive")
188         val bluetoothSocket = request.channel.toBluetoothSocket(channels)
189         val inputStream = bluetoothSocket.inputStream
190         grpcServerStream(scope, responseObserver) {
191             flow {
192                 val buffer = ByteArray(BUFFER_SIZE)
193                 inputStream.read(buffer, 0, BUFFER_SIZE)
194                 val data = ByteString.copyFrom(buffer)
195                 val response = ReceiveResponse.newBuilder().setData(data).build()
196                 emit(response)
197             }
198         }
199     }
200 
getNewChannelIdnull201     fun getNewChannelId(): Long = channelIdCounter.getAndIncrement()
202 
203     fun craftChannel(id: Long): Channel {
204         val cookie = Any.newBuilder().setValue(ByteString.copyFromUtf8(id.toString())).build()
205         val channel = Channel.newBuilder().setCookie(cookie).build()
206         return channel
207     }
208 
Channelnull209     fun Channel.id(): Long = this.cookie.value.toStringUtf8().toLong()
210 
211     fun Channel.toBluetoothSocket(channels: HashMap<Long, BluetoothSocket>): BluetoothSocket =
212         channels.get(this.id())!!
213 }
214