Files
HeliBLE/app/src/main/java/com/helible/pilot/controllers/BluetoothController.kt

311 lines
11 KiB
Kotlin

package com.helible.pilot.controllers
import android.Manifest
import android.annotation.SuppressLint
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothSocket
import android.content.Context
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.location.LocationManager
import android.os.Build
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import com.helible.pilot.viewmodels.BluetoothDataTransferService
import com.helible.pilot.KMessage
import com.helible.pilot.dataclasses.BluetoothDeviceDomain
import com.helible.pilot.receivers.BluetoothAdapterStateReceiver
import com.helible.pilot.receivers.BluetoothStateReceiver
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import java.io.IOException
import java.util.UUID
sealed interface ConnectionResult {
object ConnectionEstablished : ConnectionResult
data class TransferSucceded(val message: String) : ConnectionResult
data class Error(val message: String) : ConnectionResult
}
interface BluetoothController {
val isEnabled: StateFlow<Boolean>
val isLocationEnabled: StateFlow<Boolean>
val isConnected: StateFlow<Boolean>
val isScanning: StateFlow<Boolean>
val scannedDevices: StateFlow<List<BluetoothDeviceDomain>>
val pairedDevices: StateFlow<List<BluetoothDeviceDomain>>
val errors: SharedFlow<String>
fun startDiscovery()
fun cancelDiscovery()
fun connectToDevice(device: String): Flow<ConnectionResult>
suspend fun trySendMessage(message: ByteArray): Boolean
fun closeConnection()
fun onDestroy()
}
class AndroidBluetoothController(private val context: Context) : BluetoothController {
private val bluetoothManager by lazy {
context.getSystemService(BluetoothManager::class.java)
}
private val bluetoothAdapter by lazy {
bluetoothManager.adapter
}
private val locationManager: LocationManager? by lazy {
context.getSystemService(ComponentActivity.LOCATION_SERVICE) as LocationManager
}
private var dataTransferService: BluetoothDataTransferService? = null
private val _isConnected: MutableStateFlow<Boolean> = MutableStateFlow(false)
override val isConnected: StateFlow<Boolean>
get() = _isConnected.asStateFlow()
private val _errors = MutableSharedFlow<String>()
override val errors: SharedFlow<String>
get() = _errors.asSharedFlow()
private val _isScanning: MutableStateFlow<Boolean> = MutableStateFlow(false)
override val isScanning: StateFlow<Boolean>
get() = _isScanning.asStateFlow()
private val _isEnabled: MutableStateFlow<Boolean> = MutableStateFlow(false)
override val isEnabled: StateFlow<Boolean>
get() = _isEnabled.asStateFlow()
private val _isLocationEnabled: MutableStateFlow<Boolean> = MutableStateFlow(false)
override val isLocationEnabled: StateFlow<Boolean>
get() = _isLocationEnabled.asStateFlow()
private val _pairedDevices = MutableStateFlow<List<BluetoothDeviceDomain>>(emptyList())
override val pairedDevices: StateFlow<List<BluetoothDeviceDomain>>
get() = _pairedDevices.asStateFlow()
private val _scannedDevices: MutableStateFlow<List<BluetoothDeviceDomain>> =
MutableStateFlow(emptyList())
override val scannedDevices: StateFlow<List<BluetoothDeviceDomain>>
get() = _scannedDevices.asStateFlow()
private var currentClientSocket: BluetoothSocket? = null
@SuppressLint("MissingPermission")
private val bluetoothAdapterStateReceiver = BluetoothAdapterStateReceiver(
onBluetoothEnabledChanged = { isEnabled ->
_isEnabled.update { _ -> isEnabled }
startDiscovery()
Log.i("ScanActivity", "Bluetooth enabled status: $isEnabled")
},
onDiscoveryRunningChanged = { isDiscovering ->
_isScanning.update { isDiscovering }
},
onLocationEnabledChanged = {
if (locationManager?.isLocationEnabled == true) {
_isLocationEnabled.update { true }
} else {
_isLocationEnabled.update { false }
}
}
)
@SuppressLint("MissingPermission")
private val bluetoothStateReceiver = BluetoothStateReceiver(
onDeviceFound = { device, rssi ->
if (!hasAllPermissions()) return@BluetoothStateReceiver
val newDevice =
BluetoothDeviceDomain(device.name ?: "null", device.address, rssi, isScanned = true)
_scannedDevices.update { devices ->
if (newDevice in devices) devices else devices + newDevice
}
Log.i(
"ScanActivity",
"Found new device: ${device.name} ${device.address} $rssi"
)
},
onConnectedStateChanged = { isConnected, device ->
if (bluetoothAdapter?.bondedDevices?.contains(device) == true) {
_isConnected.update { isConnected }
} else {
CoroutineScope(Dispatchers.IO).launch {
_errors.emit("Can't connect to a non-paired device.")
}
}
}
)
companion object {
// SPP service UUID
const val SERVICE_UUID = "00001101-0000-1000-8000-00805F9B34FB"
}
init {
updatePairedDevices()
_isEnabled.update { bluetoothAdapter.isEnabled }
_isLocationEnabled.update { locationManager?.isLocationEnabled == true }
context.registerReceiver(
bluetoothAdapterStateReceiver,
IntentFilter().apply {
addAction(BluetoothAdapter.ACTION_STATE_CHANGED)
addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED)
addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
addAction(LocationManager.PROVIDERS_CHANGED_ACTION)
}
}
)
context.registerReceiver(
bluetoothStateReceiver,
IntentFilter().apply {
addAction(BluetoothDevice.ACTION_ACL_CONNECTED)
addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)
addAction(BluetoothDevice.ACTION_FOUND)
}
)
}
@SuppressLint("MissingPermission")
override fun startDiscovery() {
if (!hasAllPermissions()) {
Toast.makeText(context, "Ошибка: недостаточно разрешений", Toast.LENGTH_SHORT).show()
return
}
if (!_isEnabled.value) {
return
}
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
if (locationManager?.isLocationEnabled != true) return
}
updatePairedDevices()
_scannedDevices.update { emptyList() }
if (!bluetoothAdapter.isDiscovering) {
bluetoothAdapter.startDiscovery()
}
}
@SuppressLint("MissingPermission")
override fun cancelDiscovery() {
if (!hasAllPermissions()) return
if (bluetoothAdapter.isDiscovering) {
bluetoothAdapter.cancelDiscovery()
}
}
@SuppressLint("MissingPermission")
override fun connectToDevice(device: String): Flow<ConnectionResult> {
if (!hasAllPermissions()) {
Toast.makeText(context, "Ошибка: нет разрешений", Toast.LENGTH_SHORT).show()
return flow {}
}
return flow {
Log.i("BluetoothController", "Connecting to device...")
currentClientSocket =
bluetoothAdapter.getRemoteDevice(device).createRfcommSocketToServiceRecord(
UUID.fromString(SERVICE_UUID)
)
currentClientSocket?.let { socket ->
try {
socket.connect()
emit(ConnectionResult.ConnectionEstablished)
BluetoothDataTransferService(socket).also { it ->
dataTransferService = it
emitAll(
it.listenForIncomingMessages()
.map { ConnectionResult.TransferSucceded(it) }
)
}
} catch (e: IOException) {
socket.close()
currentClientSocket = null
Log.e("BluetoothController", e.toString())
emit(ConnectionResult.Error("Connection was interrupted"))
}
}
}.onCompletion { closeConnection() }.flowOn(Dispatchers.IO)
}
override suspend fun trySendMessage(message: ByteArray): Boolean {
if (!hasAllPermissions()) {
return false
}
if (dataTransferService == null) {
return false
}
dataTransferService?.sendMessage(message)
return true
}
override fun closeConnection() {
currentClientSocket?.close()
currentClientSocket = null
Log.i("BluetoothController", "Connection closed")
}
override fun onDestroy() {
context.unregisterReceiver(bluetoothAdapterStateReceiver)
context.unregisterReceiver(bluetoothStateReceiver)
closeConnection()
}
@SuppressLint("MissingPermission")
private fun updatePairedDevices() {
if (!hasAllPermissions()) return
Log.i("ScanActivity", "${bluetoothAdapter?.bondedDevices}")
bluetoothAdapter?.bondedDevices?.onEach { device ->
_pairedDevices.update {
val currentDevice = BluetoothDeviceDomain(
name = device.name ?: "null",
macAddress = device.address,
rssi = 0,
isScanned = false
)
if (currentDevice in pairedDevices.value) {
pairedDevices.value
} else {
_pairedDevices.value + currentDevice
}
}
}
}
private fun hasAllPermissions(): Boolean {
val perms = if (Build.VERSION.SDK_INT <= 30) {
arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION
)
} else {
arrayOf(
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_CONNECT
)
}
perms.forEach { perm ->
if (context.checkSelfPermission(perm) != PackageManager.PERMISSION_GRANTED) {
return false
}
}
return true
}
}