Skip to content

Commit 6afe091

Browse files
committed
WIP Add a server component
1 parent 7cfd959 commit 6afe091

34 files changed

+1316
-33
lines changed

.idea/codeStyles/Project.xml

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/compiler.xml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/gradle.xml

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/misc.xml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/src/main/AndroidManifest.xml

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,15 @@
1919
android:dataExtractionRules="@xml/data_extraction_rules"
2020
android:fullBackupContent="@xml/backup_rules"
2121
android:icon="@mipmap/ic_launcher"
22-
android:label="@string/app_name"
22+
android:label="@string/app_name_client"
2323
android:roundIcon="@mipmap/ic_launcher_round"
2424
android:supportsRtl="true"
2525
android:theme="@style/Theme.BclClient"
2626
tools:targetApi="31">
2727
<activity
28-
android:name=".MainActivity"
28+
android:name=".BclClientActivity"
2929
android:exported="true"
30-
android:label="@string/app_name"
30+
android:label="@string/app_name_client"
3131
android:theme="@style/Theme.BclClient.NoActionBar">
3232
<intent-filter>
3333
<action android:name="android.intent.action.MAIN" />
@@ -40,7 +40,23 @@
4040
android:value="" />
4141
</activity>
4242

43-
<service android:name="io.bimmergestalt.bclclient.MainService"
43+
<activity
44+
android:name="io.bimmergestalt.bclserver.BclServerActivity"
45+
android:exported="true"
46+
android:label="@string/app_name_server"
47+
android:theme="@style/Theme.BclClient.NoActionBar">
48+
<intent-filter>
49+
<action android:name="android.intent.action.MAIN" />
50+
51+
<category android:name="android.intent.category.LAUNCHER" />
52+
</intent-filter>
53+
54+
<meta-data
55+
android:name="android.app.lib_name"
56+
android:value="" />
57+
</activity>
58+
59+
<service android:name="io.bimmergestalt.bclclient.BclClientService"
4460
android:exported="false"
4561
android:foregroundServiceType="connectedDevice"/>
4662
<service android:name="io.bimmergestalt.bcl.android.BtClientService"

app/src/main/java/io/bimmergestalt/bcl/BclPacket.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,13 @@ open class BclPacket(
7171
override fun toString(): String {
7272
return "BclPacket\$Handshake(version=$version, instanceId=$instanceId, bufferSize=0x${bufferSize.toString(16)})"
7373
}
74-
74+
}
75+
fun Handshake(version: Short, instanceId: Short, bufferSize: Int): Handshake {
76+
return Handshake().apply {
77+
this.version = version
78+
this.instanceId = instanceId
79+
this.bufferSize = bufferSize
80+
}
7581
}
7682
class SelectProto(data: ByteArray = ByteArray(2)): BclPacket(COMMAND.SELECTPROTO, 0, 0, data) {
7783
var version by ShortField(data, 0)
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package io.bimmergestalt.bcl.android
2+
3+
import android.bluetooth.BluetoothAdapter
4+
import android.bluetooth.BluetoothServerSocket
5+
import android.bluetooth.BluetoothSocket
6+
import io.bimmergestalt.bcl.ConnectionState
7+
import io.bimmergestalt.bcl.MutableConnectionState
8+
import io.bimmergestalt.bcl.multiplex.BclDemuxer
9+
import io.bimmergestalt.bcl.protocols.DemuxedProtocolFactory
10+
import io.bimmergestalt.bcl.server.BclServerTransport
11+
import org.tinylog.kotlin.Logger
12+
import java.io.IOException
13+
14+
class BtServerConnection(val adapter: BluetoothAdapter, val connectionState: MutableConnectionState,
15+
val protocolFactories: Iterable<DemuxedProtocolFactory>): Thread() {
16+
17+
private var socket: BluetoothServerSocket? = null
18+
19+
var bclConnection: BclServerTransport? = null
20+
private set
21+
var bclDemuxer: BclDemuxer? = null
22+
private set
23+
24+
override fun run() {
25+
connectionState.transportState = ConnectionState.TransportState.OPENING
26+
val serverSocket = try {
27+
adapter.listenUsingRfcommWithServiceRecord("BCL", BtConnection.SDP_BCL)
28+
} catch (_: SecurityException) {
29+
return
30+
}
31+
this.socket = serverSocket
32+
33+
try {
34+
val socket = connectSocket(serverSocket)
35+
connectBcl(socket)
36+
} catch (_: SecurityException) {
37+
return
38+
} catch (_: InterruptedException) {
39+
return
40+
}
41+
}
42+
43+
private fun connectSocket(serverSocket: BluetoothServerSocket): BluetoothSocket {
44+
connectionState.transportState = ConnectionState.TransportState.OPENING
45+
try {
46+
return serverSocket.accept()
47+
} catch (e: SecurityException) {
48+
throw e
49+
} catch (e: IOException) {
50+
connectionState.transportState = ConnectionState.TransportState.FAILED
51+
Logger.warn(e) { "IOException opening BluetoothSocket" }
52+
throw e
53+
}
54+
}
55+
56+
private fun connectBcl(socket: BluetoothSocket) {
57+
while (socket.isConnected) {
58+
try {
59+
connectionState.transportState = ConnectionState.TransportState.ACTIVE
60+
val bclConnection = BclServerTransport(socket.inputStream, socket.outputStream, connectionState)
61+
this.bclConnection = bclConnection
62+
bclConnection.connect()
63+
val bclDemuxer = BclDemuxer(bclConnection, protocolFactories)
64+
this.bclDemuxer = bclDemuxer
65+
bclDemuxer.run()
66+
} catch (_: SecurityException) {
67+
} catch (e: IOException) {
68+
Logger.warn(e) { "IOException communicating BCL" }
69+
} catch (_: InterruptedException) {
70+
} finally {
71+
bclDemuxer?.shutdown()
72+
bclDemuxer = null
73+
bclConnection?.shutdown()
74+
bclConnection = null
75+
}
76+
connectionState.transportState = ConnectionState.TransportState.WAITING
77+
sleep(1000)
78+
}
79+
}
80+
81+
fun shutdown() {
82+
bclConnection?.shutdown()
83+
socket?.close()
84+
interrupt()
85+
}
86+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package io.bimmergestalt.bcl.android
2+
3+
import android.Manifest
4+
import android.annotation.SuppressLint
5+
import android.app.Service
6+
import android.bluetooth.BluetoothDevice
7+
import android.bluetooth.BluetoothManager
8+
import android.bluetooth.BluetoothServerSocket
9+
import android.content.Intent
10+
import android.os.*
11+
import android.util.ArrayMap
12+
import androidx.core.content.PermissionChecker
13+
import androidx.lifecycle.LiveData
14+
import io.bimmergestalt.bcl.ConnectionState
15+
import io.bimmergestalt.bcl.android.BtConnection.Companion.SDP_BCL
16+
import org.tinylog.kotlin.Logger
17+
import java.io.IOException
18+
import java.util.*
19+
20+
21+
class BtServerService: Service() {
22+
23+
companion object {
24+
private val _state = ConnectionStateLiveData()
25+
val state: LiveData<ConnectionState> = _state
26+
private const val INTERVAL_SCAN: Int = 2000
27+
private const val INTERVAL_KEEPALIVE: Int = 10000
28+
private const val ETCH_PROXY_PORT: Short = 4007
29+
private const val ETCH_DEST_PORT: Short = 4004
30+
val ACTION_SHUTDOWN = "io.bimmergestalt.bcl.android.BtServerService.SHUTDOWN"
31+
}
32+
33+
// on startup, check for Bluetooth Connect privilege, stop self if not
34+
// prune any disconnected sockets
35+
// scan all the Adapters for car devices
36+
// create connections if not already connected
37+
// socket connection is blocking for up to 12s timeout
38+
// each socket needs its own Thread
39+
40+
private val handlerThread = HandlerThread("BtServerService")
41+
private val handler = Handler(handlerThread.looper)
42+
private var serverSocket: BluetoothServerSocket? = null
43+
private val btThreads = ArrayMap<String, BtConnection>() // bt address to thread
44+
45+
46+
override fun onBind(intent: Intent?): IBinder? {
47+
return null
48+
}
49+
50+
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
51+
if (!hasBluetoothPermission()) {
52+
Logger.warn { "Bluetooth permission is missing, not starting" }
53+
stopSelf(startId)
54+
return START_NOT_STICKY
55+
}
56+
if (intent?.action != ACTION_SHUTDOWN) {
57+
startServer()
58+
} else {
59+
stopSelf()
60+
}
61+
return START_STICKY
62+
}
63+
64+
private fun hasBluetoothPermission(): Boolean {
65+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
66+
if (PermissionChecker.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) == PermissionChecker.PERMISSION_GRANTED) {
67+
return true
68+
}
69+
} else {
70+
if (PermissionChecker.checkSelfPermission(this, Manifest.permission.BLUETOOTH) == PermissionChecker.PERMISSION_GRANTED) {
71+
return true
72+
}
73+
}
74+
return false
75+
}
76+
77+
@SuppressLint("MissingPermission")
78+
private fun startServer() {
79+
val server = this.getSystemService(BluetoothManager::class.java).adapter.listenUsingRfcommWithServiceRecord("BCL", SDP_BCL)
80+
serverSocket = server
81+
handlerThread.start()
82+
handler.post {
83+
acceptConnections()
84+
}
85+
}
86+
87+
private fun acceptConnections() {
88+
while (true) {
89+
try {
90+
val connection = serverSocket?.accept()
91+
// wrap in BtConnection
92+
} catch (e: IOException) {
93+
Logger.info("Received error when accepting server connection, shutting down")
94+
return
95+
}
96+
}
97+
}
98+
99+
private fun tryConnection(device: BluetoothDevice): BtConnection {
100+
return BtConnection(device, _state, listOf(
101+
BclTunnelReport.Factory(this, device.brand ?: "bmw"),
102+
EtchProxyService.Factory(this, ETCH_PROXY_PORT, ETCH_DEST_PORT,
103+
device.brand ?: "bmw", _state)
104+
))
105+
}
106+
107+
private fun stopServer() {
108+
serverSocket?.close()
109+
handlerThread.looper.quit()
110+
}
111+
112+
private fun disconnect() {
113+
Logger.info {"Shutting down BtServerService"}
114+
synchronized(btThreads) {
115+
btThreads.values.forEach {
116+
it?.shutdown()
117+
}
118+
btThreads.clear()
119+
}
120+
}
121+
122+
override fun onDestroy() {
123+
super.onDestroy()
124+
stopServer()
125+
disconnect()
126+
}
127+
}

app/src/main/java/io/bimmergestalt/bcl/client/BclClientTransport.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,10 @@ class BclClientTransport(input: InputStream, output: OutputStream, val connectio
133133
try {
134134
packetOutput.writePacket(BclPacket(BclPacket.COMMAND.HANGUP, 0, 0, ByteArray(0)))
135135
output.flush()
136-
state = ConnectionState.BclState.SHUTDOWN
137136
} catch (e: Exception) {
138137
Logger.warn(e) {"Error while shutting down BCL"}
138+
} finally {
139+
state = ConnectionState.BclState.SHUTDOWN
139140
}
140141
}
141142
}

0 commit comments

Comments
 (0)