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 package com.android.devicediagnostics.bluetooth
17 
18 import android.bluetooth.BluetoothSocket
19 import android.util.Base64
20 import com.android.devicediagnostics.Protos.BluetoothPacket
21 import java.nio.ByteBuffer
22 import java.nio.ByteOrder
23 import kotlin.math.min
24 import org.json.JSONObject
25 
26 const val BT_READ_SIZE = 4096
27 const val BT_MAX_PACKET_SIZE = 1024 * 1024
28 const val BT_VERSION = 1
29 
30 private const val QRCODE_VERSION = 4
31 
readFullynull32 private fun readFully(socket: BluetoothSocket, n: Int): ByteArray {
33     val b = ByteArray(n)
34     val stream = socket.inputStream
35 
36     var offset = 0
37     while (offset < n) {
38         val maxRead = min(n - offset, BT_READ_SIZE)
39         offset += stream.read(b, offset, maxRead)
40     }
41     return b
42 }
43 
readBluetoothPacketOrEofnull44 fun readBluetoothPacketOrEof(socket: BluetoothSocket): BluetoothPacket? {
45     val payloadSizeBytes = readFully(socket, 4)
46     val payloadSize = ByteBuffer.wrap(payloadSizeBytes).order(ByteOrder.LITTLE_ENDIAN).getInt()
47     if (payloadSize == 0) {
48         return null
49     }
50     if (payloadSize > BT_MAX_PACKET_SIZE) {
51         throw Exception("Maximum packet size exceeded: $payloadSize")
52     }
53     val payloadBytes = readFully(socket, payloadSize)
54     return BluetoothPacket.parseFrom(payloadBytes)
55 }
56 
readBluetoothPacketnull57 fun readBluetoothPacket(socket: BluetoothSocket): BluetoothPacket {
58     val packet = readBluetoothPacketOrEof(socket)
59     if (packet == null) {
60         throw Exception("Expected packet, got EOF")
61     }
62     return packet
63 }
64 
writeBluetoothPacketnull65 fun writeBluetoothPacket(socket: BluetoothSocket, packetBuilder: BluetoothPacket.Builder) {
66     val packet = packetBuilder.setVersion(BT_VERSION).build()
67     val bytes = packet.toByteArray()
68     val header = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(bytes.size).array()
69     socket.outputStream.write(header)
70     socket.outputStream.write(bytes)
71 }
72 
73 class BluetoothConnectionData(val psm: Int = 0, val challenge: ByteArray) {
74     companion object Helpers {
75         private const val JSON_PSM_KEY = "PSM"
76         private const val JSON_VERSION_KEY = "version"
77         private const val JSON_CHALLENGE_KEY = "challenge"
78 
fromJsonnull79         fun fromJson(json: String): BluetoothConnectionData {
80             val obj = JSONObject(json)
81             var version: Int = 0
82             if (obj.has(JSON_VERSION_KEY)) {
83                 version = obj.getInt(JSON_VERSION_KEY)
84             }
85             if (version < QRCODE_VERSION) {
86                 throw Exception("Unsupported version")
87             }
88             val psm = obj.getInt(JSON_PSM_KEY)
89             val challengeBase64 = obj.getString(JSON_CHALLENGE_KEY)
90             val challenge = Base64.decode(challengeBase64, Base64.URL_SAFE)
91             return BluetoothConnectionData(psm, challenge)
92         }
93     }
94 
toStringnull95     override fun toString(): String {
96         val challengeBase64 = Base64.encodeToString(challenge, Base64.URL_SAFE)
97         return JSONObject()
98             .put(JSON_PSM_KEY, psm)
99             .put(JSON_VERSION_KEY, QRCODE_VERSION)
100             .put(JSON_CHALLENGE_KEY, challengeBase64)
101             .toString()
102     }
103 }
104