311 lines
11 KiB
Kotlin
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
|
|
}
|
|
}
|