Critical bugs were fixed
More correct permissions processing and compact code with flows
This commit is contained in:
234
app/src/main/java/com/helible/pilot/BluetoothController.kt
Normal file
234
app/src/main/java/com/helible/pilot/BluetoothController.kt
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
package com.helible.pilot
|
||||||
|
|
||||||
|
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 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.flow
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.flow.onCompletion
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
sealed interface ConnectionResult {
|
||||||
|
object ConnectionEstablished: 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<Device>>
|
||||||
|
val pairedDevices: StateFlow<Set<BluetoothDevice>>
|
||||||
|
val errors: SharedFlow<String>
|
||||||
|
|
||||||
|
fun startDiscovery()
|
||||||
|
fun cancelDiscovery()
|
||||||
|
fun connectToDevice(device: Device?): Flow<ConnectionResult>
|
||||||
|
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 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<Set<BluetoothDevice>>(emptySet())
|
||||||
|
override val pairedDevices: StateFlow<Set<BluetoothDevice>>
|
||||||
|
get() = _pairedDevices.asStateFlow()
|
||||||
|
|
||||||
|
private val _scannedDevices: MutableStateFlow<List<Device>> = MutableStateFlow(emptyList())
|
||||||
|
override val scannedDevices: StateFlow<List<Device>>
|
||||||
|
get() = _scannedDevices.asStateFlow()
|
||||||
|
|
||||||
|
private var currentClientSocket: BluetoothSocket? = null
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
private val bluetoothIntentReceiver = BluetoothIntentReceiver(
|
||||||
|
onDeviceFound = {device, rssi ->
|
||||||
|
if(!hasAllPermissions()) return@BluetoothIntentReceiver
|
||||||
|
val newDevice = Device(device, rssi)
|
||||||
|
_scannedDevices.update { devices ->
|
||||||
|
if(newDevice in devices) devices else devices + newDevice
|
||||||
|
}
|
||||||
|
Log.i(
|
||||||
|
"ScanActivity",
|
||||||
|
"Found new device: ${device.name} ${device.address} $rssi"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val SERVICE_UUID = "af7cc14b-cffa-4a3d-b677-01b0ff0a93d7"
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
updatePairedDevices()
|
||||||
|
_isEnabled.update { bluetoothAdapter.isEnabled }
|
||||||
|
_isLocationEnabled.update { locationManager?.isLocationEnabled == true }
|
||||||
|
context.registerReceiver(bluetoothIntentReceiver, IntentFilter(BluetoothDevice.ACTION_FOUND))
|
||||||
|
context.registerReceiver(bluetoothIntentReceiver, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED))
|
||||||
|
context.registerReceiver(bluetoothIntentReceiver, IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_STARTED))
|
||||||
|
context.registerReceiver(bluetoothIntentReceiver, IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED))
|
||||||
|
if(Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
|
||||||
|
context.registerReceiver(bluetoothIntentReceiver, IntentFilter(LocationManager.PROVIDERS_CHANGED_ACTION))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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: Device?): Flow<ConnectionResult> {
|
||||||
|
if(!hasAllPermissions()){
|
||||||
|
Toast.makeText(context, "Ошибка: нет разрешений", Toast.LENGTH_SHORT).show()
|
||||||
|
return flow {}
|
||||||
|
}
|
||||||
|
return flow {
|
||||||
|
currentClientSocket = device?.bluetoothDevice?.createRfcommSocketToServiceRecord(
|
||||||
|
UUID.fromString(SERVICE_UUID)
|
||||||
|
)
|
||||||
|
currentClientSocket?.let { socket ->
|
||||||
|
try {
|
||||||
|
socket.connect()
|
||||||
|
emit(ConnectionResult.ConnectionEstablished)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
closeConnection()
|
||||||
|
emit(ConnectionResult.Error("Connection was interrupted"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.onCompletion { closeConnection() }.flowOn(Dispatchers.IO)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun closeConnection() {
|
||||||
|
currentClientSocket?.close()
|
||||||
|
currentClientSocket = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
context.unregisterReceiver(bluetoothIntentReceiver)
|
||||||
|
closeConnection()
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
private fun updatePairedDevices() {
|
||||||
|
if(!hasAllPermissions()) return
|
||||||
|
bluetoothAdapter?.bondedDevices.also { devices ->
|
||||||
|
if(devices != null) {
|
||||||
|
_pairedDevices.update { devices }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
package com.helible.pilot
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.bluetooth.BluetoothSocket
|
|
||||||
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.flow
|
|
||||||
import kotlinx.coroutines.flow.flowOn
|
|
||||||
import kotlinx.coroutines.flow.onCompletion
|
|
||||||
import java.io.IOException
|
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
sealed interface ConnectionResult {
|
|
||||||
object ConnectionEstablished: ConnectionResult
|
|
||||||
data class Error(val message: String) : ConnectionResult
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BluetoothController {
|
|
||||||
val isConnected: StateFlow<Boolean>
|
|
||||||
val errors: SharedFlow<String>
|
|
||||||
|
|
||||||
fun connectToDevice(device: Device?): Flow<ConnectionResult>
|
|
||||||
fun closeConnection()
|
|
||||||
}
|
|
||||||
|
|
||||||
class AndroidBluetoothController : BluetoothController {
|
|
||||||
|
|
||||||
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 var currentClientSocket: BluetoothSocket? = null
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val SERVICE_UUID = "af7cc14b-cffa-4a3d-b677-01b0ff0a93d7"
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
|
||||||
override fun connectToDevice(device: Device?): Flow<ConnectionResult> {
|
|
||||||
return flow {
|
|
||||||
currentClientSocket = device?.bluetoothDevice?.createRfcommSocketToServiceRecord(
|
|
||||||
UUID.fromString(SERVICE_UUID)
|
|
||||||
)
|
|
||||||
currentClientSocket?.let { socket ->
|
|
||||||
try {
|
|
||||||
socket.connect()
|
|
||||||
emit(ConnectionResult.ConnectionEstablished)
|
|
||||||
} catch (e: IOException) {
|
|
||||||
closeConnection()
|
|
||||||
emit(ConnectionResult.Error("Connection was interrupted"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.onCompletion { closeConnection() }.flowOn(Dispatchers.IO)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun closeConnection() {
|
|
||||||
currentClientSocket?.close()
|
|
||||||
currentClientSocket = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package com.helible.pilot
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.bluetooth.BluetoothAdapter
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.location.LocationManager
|
||||||
|
import android.os.Build
|
||||||
|
|
||||||
|
class BluetoothIntentReceiver(
|
||||||
|
private val onDeviceFound: (device: BluetoothDevice, rssi: Short) -> Unit,
|
||||||
|
private val onBluetoothEnabledChanged: (isBluetoothEnabled: Boolean) -> Unit,
|
||||||
|
private val onDiscoveryRunningChanged: (isDiscoveryRunning: Boolean) -> Unit,
|
||||||
|
private val onLocationEnabledChanged: () -> Unit
|
||||||
|
) : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
|
when (intent?.action) {
|
||||||
|
BluetoothDevice.ACTION_FOUND -> {
|
||||||
|
val device = if (Build.VERSION.SDK_INT >= 33) {
|
||||||
|
intent.getParcelableExtra(
|
||||||
|
BluetoothDevice.EXTRA_DEVICE,
|
||||||
|
BluetoothDevice::class.java
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION") intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
|
||||||
|
}
|
||||||
|
|
||||||
|
val rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, Short.MIN_VALUE)
|
||||||
|
@SuppressLint("MissingPermission") if (device?.name != null)
|
||||||
|
onDeviceFound(device, rssi)
|
||||||
|
}
|
||||||
|
|
||||||
|
BluetoothAdapter.ACTION_STATE_CHANGED -> {
|
||||||
|
when (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1)) {
|
||||||
|
BluetoothAdapter.STATE_ON -> {
|
||||||
|
onBluetoothEnabledChanged(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
BluetoothAdapter.STATE_OFF -> {
|
||||||
|
onBluetoothEnabledChanged(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LocationManager.PROVIDERS_CHANGED_ACTION -> {
|
||||||
|
onLocationEnabledChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
BluetoothAdapter.ACTION_DISCOVERY_FINISHED -> {
|
||||||
|
onDiscoveryRunningChanged(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
BluetoothAdapter.ACTION_DISCOVERY_STARTED -> {
|
||||||
|
onDiscoveryRunningChanged(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
143
app/src/main/java/com/helible/pilot/BluetoothViewModel.kt
Normal file
143
app/src/main/java/com/helible/pilot/BluetoothViewModel.kt
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
package com.helible.pilot
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
|
||||||
|
data class BluetoothUiState(
|
||||||
|
val isEnabled: Boolean = false,
|
||||||
|
val isLocationEnabled: Boolean = false,
|
||||||
|
val isDiscovering: Boolean = false,
|
||||||
|
val isConnected: Boolean = false,
|
||||||
|
val isConnecting: Boolean = false,
|
||||||
|
val errorMessage: String? = null,
|
||||||
|
val scannedDevices: List<Device> = emptyList(),
|
||||||
|
val pairedDevices: List<BluetoothDevice> = emptyList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
class BluetoothViewModel(
|
||||||
|
private val bluetoothController: BluetoothController
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val _selectedDevice: MutableStateFlow<Device?> = MutableStateFlow(null)
|
||||||
|
val selectedDevice: StateFlow<Device?>
|
||||||
|
get () = _selectedDevice.asStateFlow()
|
||||||
|
|
||||||
|
private val _state: MutableStateFlow<BluetoothUiState> = MutableStateFlow(BluetoothUiState())
|
||||||
|
val state: StateFlow<BluetoothUiState> = combine(bluetoothController.scannedDevices, bluetoothController.pairedDevices, _state)
|
||||||
|
{ scannedDevices, pairedDevices, state ->
|
||||||
|
state.copy(
|
||||||
|
scannedDevices = scannedDevices.toList(),
|
||||||
|
pairedDevices = pairedDevices.toList()
|
||||||
|
)
|
||||||
|
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), _state.value)
|
||||||
|
|
||||||
|
init {
|
||||||
|
bluetoothController.isConnected.onEach {
|
||||||
|
isConnected -> _state.update { it.copy(isConnected = isConnected) }
|
||||||
|
}.launchIn(viewModelScope)
|
||||||
|
bluetoothController.errors.onEach { error ->
|
||||||
|
_state.update {
|
||||||
|
it.copy(errorMessage = error)
|
||||||
|
}
|
||||||
|
}.launchIn(viewModelScope)
|
||||||
|
bluetoothController.isScanning.onEach { result ->
|
||||||
|
_state.update {it.copy(
|
||||||
|
isDiscovering = result,
|
||||||
|
)}
|
||||||
|
}.launchIn(viewModelScope)
|
||||||
|
bluetoothController.isEnabled.onEach { result ->
|
||||||
|
_state.update {it.copy(
|
||||||
|
isEnabled = result,
|
||||||
|
)}
|
||||||
|
}.launchIn(viewModelScope)
|
||||||
|
bluetoothController.isLocationEnabled.onEach { result ->
|
||||||
|
_state.update { it.copy(
|
||||||
|
isLocationEnabled = result
|
||||||
|
) }
|
||||||
|
}.launchIn(viewModelScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Flow<ConnectionResult>.listen(): Job {
|
||||||
|
return onEach { result ->
|
||||||
|
when(result) {
|
||||||
|
ConnectionResult.ConnectionEstablished -> {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isConnected = true,
|
||||||
|
isConnecting = false,
|
||||||
|
errorMessage = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is ConnectionResult.Error -> {
|
||||||
|
_state.update { it.copy(
|
||||||
|
isConnected = false,
|
||||||
|
isConnecting = false,
|
||||||
|
errorMessage = result.message
|
||||||
|
) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.catch { throwable ->
|
||||||
|
bluetoothController.closeConnection()
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isConnected = false,
|
||||||
|
isConnecting = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.launchIn(viewModelScope)
|
||||||
|
}
|
||||||
|
private var deviceConnectionJob: Job? = null
|
||||||
|
|
||||||
|
fun connectToDevice(device: Device) {
|
||||||
|
_state.update {it.copy(isConnecting = true)}
|
||||||
|
deviceConnectionJob = bluetoothController
|
||||||
|
.connectToDevice(device)
|
||||||
|
.listen()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun disconnectFromDevice() {
|
||||||
|
deviceConnectionJob?.cancel()
|
||||||
|
bluetoothController.closeConnection()
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isConnecting = false,
|
||||||
|
isConnected = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectDevice(selectedDevice: Device?) {
|
||||||
|
_selectedDevice.update { selectedDevice }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onDestroy() {
|
||||||
|
cancelScan()
|
||||||
|
bluetoothController.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startScan(){
|
||||||
|
selectDevice(null)
|
||||||
|
bluetoothController.startDiscovery()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancelScan() {
|
||||||
|
bluetoothController.cancelDiscovery()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +1,13 @@
|
|||||||
package com.helible.pilot
|
package com.helible.pilot
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.bluetooth.BluetoothAdapter
|
|
||||||
import android.bluetooth.BluetoothManager
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.pm.PackageManager
|
||||||
import android.location.LocationManager
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.ManagedActivityResultLauncher
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
@@ -32,36 +24,29 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.navigation.NavType
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import androidx.navigation.navArgument
|
||||||
import com.helible.pilot.components.BluetoothScannerScreen
|
import com.helible.pilot.components.BluetoothScannerScreen
|
||||||
import com.helible.pilot.components.PreferencesCacheImpl
|
import com.helible.pilot.components.PreferencesCacheImpl
|
||||||
import com.helible.pilot.components.SavedPreferences
|
import com.helible.pilot.components.SavedPreferences
|
||||||
import com.helible.pilot.ui.theme.TestblueTheme
|
import com.helible.pilot.ui.theme.TestblueTheme
|
||||||
import java.util.concurrent.Executors
|
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
// TODO: delegate part of Intent filters logic to BluetoothController
|
|
||||||
// TODO: move bluetooth states and stateFlow to new BluetoothViewModel
|
|
||||||
// TODO: replace field bluetoothDevice in Device to deviceAddress field
|
// TODO: replace field bluetoothDevice in Device to deviceAddress field
|
||||||
// TODO: replace some mutableStates to stateFlows
|
|
||||||
// TODO: share selected device via PersistentViewModel
|
// TODO: share selected device via PersistentViewModel
|
||||||
// TODO: check permissions inside other classes (and throw an exception, if one of this isn't granted)
|
|
||||||
// TODO: add stub instead of the DevicesList, if there aren't nearby devices
|
// TODO: add stub instead of the DevicesList, if there aren't nearby devices
|
||||||
// TODO: add Bluetooth data transfer...
|
// TODO: add Bluetooth data transfer...
|
||||||
// TODO: add text strings to resource
|
// TODO: add text strings to resource
|
||||||
val mainViewModel: MainViewModel = MainViewModel(AndroidBluetoothController())
|
|
||||||
|
|
||||||
private val bluetoothManager: BluetoothManager by lazy {
|
private val bluetoothViewModel by lazy {
|
||||||
getSystemService(BluetoothManager::class.java)
|
BluetoothViewModel(AndroidBluetoothController(applicationContext))
|
||||||
}
|
|
||||||
private val bluetoothAdapter: BluetoothAdapter? by lazy {
|
|
||||||
bluetoothManager.adapter
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var permissionsViewModel = PermissionDialogViewModel()
|
private var permissionsViewModel = PermissionDialogViewModel()
|
||||||
private lateinit var permissionLauncher: ManagedActivityResultLauncher<Array<String>, Map<String, Boolean>>
|
|
||||||
|
|
||||||
private val permissionsToRequest: Array<String> by lazy {
|
private val permissionsToRequest: Array<String> by lazy {
|
||||||
if (Build.VERSION.SDK_INT <= 30) {
|
if (Build.VERSION.SDK_INT <= 30) {
|
||||||
@@ -71,15 +56,11 @@ class MainActivity : ComponentActivity() {
|
|||||||
} else {
|
} else {
|
||||||
arrayOf(
|
arrayOf(
|
||||||
Manifest.permission.BLUETOOTH_SCAN,
|
Manifest.permission.BLUETOOTH_SCAN,
|
||||||
Manifest.permission.BLUETOOTH_CONNECT,
|
Manifest.permission.BLUETOOTH_CONNECT
|
||||||
Manifest.permission.BLUETOOTH_ADMIN
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val locationManager: LocationManager by lazy {
|
|
||||||
getSystemService(LOCATION_SERVICE) as LocationManager
|
|
||||||
}
|
|
||||||
private val preferencesCache by lazy {
|
private val preferencesCache by lazy {
|
||||||
PreferencesCacheImpl(getSharedPreferences(packageName, Context.MODE_PRIVATE))
|
PreferencesCacheImpl(getSharedPreferences(packageName, Context.MODE_PRIVATE))
|
||||||
}
|
}
|
||||||
@@ -90,12 +71,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
mainViewModel.bluetoothTurnOnState.value = bluetoothAdapter?.isEnabled
|
|
||||||
mainViewModel.locationTurnOnState.value =
|
|
||||||
locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
|
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
this.permissionLauncher = rememberLauncherForActivityResult(
|
val permissionLauncher = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.RequestMultiplePermissions(),
|
contract = ActivityResultContracts.RequestMultiplePermissions(),
|
||||||
onResult = { perms ->
|
onResult = { perms ->
|
||||||
permissionsToRequest.forEach { permission ->
|
permissionsToRequest.forEach { permission ->
|
||||||
@@ -104,18 +81,25 @@ class MainActivity : ComponentActivity() {
|
|||||||
isGranted = perms[permission] == true
|
isGranted = perms[permission] == true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if(hasAllPermissions() && !bluetoothViewModel.state.value.isDiscovering)
|
||||||
|
bluetoothViewModel.startScan()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
val state by mainViewModel.state.collectAsState()
|
val bluetoothState by bluetoothViewModel.state.collectAsState()
|
||||||
|
val selectedDevice by bluetoothViewModel.selectedDevice.collectAsState()
|
||||||
|
|
||||||
LaunchedEffect(key1 = state.errorMessage) {
|
LaunchedEffect(key1 = null) {
|
||||||
state.errorMessage?.let { message ->
|
permissionLauncher.launch(permissionsToRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = bluetoothState.errorMessage) {
|
||||||
|
bluetoothState.errorMessage?.let { message ->
|
||||||
Toast.makeText(applicationContext, message, Toast.LENGTH_LONG).show()
|
Toast.makeText(applicationContext, message, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LaunchedEffect(key1 = state) {
|
LaunchedEffect(key1 = bluetoothState) {
|
||||||
if (state.isConnected) {
|
if (bluetoothState.isConnected) {
|
||||||
Toast.makeText(applicationContext, "Подключение завершено", Toast.LENGTH_LONG)
|
Toast.makeText(applicationContext, "Подключение завершено", Toast.LENGTH_LONG)
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
@@ -133,26 +117,26 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
RequestHardwareFeatures(
|
RequestHardwareFeatures(
|
||||||
activity = this,
|
activity = this,
|
||||||
turnOnLocation = Manifest.permission.ACCESS_FINE_LOCATION in permissionsToRequest,
|
bluetoothUiState = bluetoothState
|
||||||
bluetoothTurnOnState = mainViewModel.bluetoothTurnOnState,
|
|
||||||
locationTurnOnState = mainViewModel.locationTurnOnState
|
|
||||||
)
|
)
|
||||||
|
|
||||||
NavHost(navController = navController, startDestination = "scanner") {
|
NavHost(navController = navController, startDestination = "scanner") {
|
||||||
composable("scanner") {
|
composable("scanner") {
|
||||||
BluetoothScannerScreen(
|
BluetoothScannerScreen(
|
||||||
devices = mainViewModel.devices,
|
bluetoothState = bluetoothState,
|
||||||
selectedDevice = mainViewModel.selectedDevice,
|
selectedDevice = selectedDevice,
|
||||||
bluetoothIsDiscoveringState = mainViewModel.isBluetoothDiscoveryRunning,
|
startScan = { bluetoothViewModel.startScan() },
|
||||||
bluetoothAdapter = bluetoothAdapter,
|
cancelScan = { bluetoothViewModel.cancelScan() },
|
||||||
|
choiceDevice = {device -> bluetoothViewModel.selectDevice(device)},
|
||||||
onScreenChanged = {
|
onScreenChanged = {
|
||||||
bluetoothAdapter?.cancelDiscovery()
|
bluetoothViewModel.cancelScan()
|
||||||
|
val deviceAddress = selectedDevice?.bluetoothDevice?.address
|
||||||
preferencesViewModel.savePreferences(
|
preferencesViewModel.savePreferences(
|
||||||
SavedPreferences(
|
SavedPreferences(
|
||||||
mainViewModel.selectedDevice.value?.bluetoothDevice?.address
|
deviceAddress
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
navController.navigate("flight")
|
navController.navigate("flight/$deviceAddress")
|
||||||
Log.i(
|
Log.i(
|
||||||
"ScanActivity",
|
"ScanActivity",
|
||||||
"Preferences: ${preferencesViewModel.preferences}"
|
"Preferences: ${preferencesViewModel.preferences}"
|
||||||
@@ -160,15 +144,21 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable("flight") {
|
composable(
|
||||||
|
"flight/{device_address}",
|
||||||
|
arguments = listOf(navArgument("device_address"){type = NavType.StringType})
|
||||||
|
) {
|
||||||
|
backstackEntry ->
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
// TODO: refactor
|
val device: Device? = selectedDevice
|
||||||
val device: Device = mainViewModel.selectedDevice.value!!
|
if(device == null){
|
||||||
mainViewModel.connectToDevice(device)
|
navController.navigate("scanner")
|
||||||
|
} else {
|
||||||
|
bluetoothViewModel.connectToDevice(device)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
when {
|
when {
|
||||||
state.isConnecting -> {
|
bluetoothState.isConnecting -> {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
@@ -181,7 +171,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
Text(
|
Text(
|
||||||
text = "Device name: ${mainViewModel.selectedDevice.value?.bluetoothDevice?.name}",
|
text = "Device name: ${backstackEntry.arguments?.getString("device_address")}",
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
@@ -192,58 +182,35 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
registerIntentFilters(this, receiver)
|
|
||||||
requestPermissions()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val receiver = object : BroadcastReceiver() {
|
|
||||||
override fun onReceive(context: Context?, intent: Intent) {
|
|
||||||
receiveIntentChanges(
|
|
||||||
intent,
|
|
||||||
mainViewModel,
|
|
||||||
bluetoothAdapter,
|
|
||||||
locationManager
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun requestPermissions() {
|
|
||||||
Executors.newSingleThreadExecutor().execute {
|
|
||||||
Handler(Looper.getMainLooper()).post {
|
|
||||||
permissionLauncher.launch(permissionsToRequest)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
unregisterReceiver(receiver)
|
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
Log.i("ScanActivity", "ACTIVITY DESTROYED")
|
bluetoothViewModel.onDestroy()
|
||||||
bluetoothAdapter?.cancelDiscovery()
|
|
||||||
try {
|
|
||||||
unregisterReceiver(receiver)
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
Log.e(
|
|
||||||
"ScanActivity",
|
|
||||||
"Receiver wasn't registered ${e.localizedMessage}\nStackTrace: ${e.stackTrace}"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
if (bluetoothAdapter?.isDiscovering != true)
|
bluetoothViewModel.startScan()
|
||||||
bluetoothAdapter?.startDiscovery()
|
|
||||||
Log.i("ScanActivity", "ACTIVITY STARTED")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
super.onStop()
|
super.onStop()
|
||||||
bluetoothAdapter?.cancelDiscovery()
|
if(!hasAllPermissions()) return
|
||||||
mainViewModel.devices.clear()
|
bluetoothViewModel.cancelScan()
|
||||||
mainViewModel.selectedDevice.value = null
|
bluetoothViewModel.selectDevice(null)
|
||||||
Log.i("ScanActivity", "ACTIVITY STOPPED")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun hasAllPermissions(): Boolean {
|
||||||
|
permissionsToRequest.forEach { perm ->
|
||||||
|
if(checkSelfPermission(perm) != PackageManager.PERMISSION_GRANTED){
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -24,89 +24,6 @@ data class Device(
|
|||||||
val rssi: Short,
|
val rssi: Short,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class BluetoothUiState(
|
|
||||||
val isConnected: Boolean = false,
|
|
||||||
val isConnecting: Boolean = false,
|
|
||||||
val errorMessage: String? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
class MainViewModel(
|
|
||||||
private val bluetoothController: BluetoothController
|
|
||||||
) : ViewModel() {
|
|
||||||
val devices: MutableList<Device> = mutableStateListOf()
|
|
||||||
val selectedDevice: MutableState<Device?> = mutableStateOf(null)
|
|
||||||
val bluetoothTurnOnState: MutableState<Boolean?> = mutableStateOf(false)
|
|
||||||
val locationTurnOnState: MutableState<Boolean?> = mutableStateOf(null)
|
|
||||||
val isBluetoothDiscoveryRunning: MutableState<Boolean> = mutableStateOf(false)
|
|
||||||
|
|
||||||
private val _bluetoothState = MutableStateFlow(BluetoothUiState())
|
|
||||||
val state: StateFlow<BluetoothUiState>
|
|
||||||
get() = _bluetoothState.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), _bluetoothState.value)
|
|
||||||
|
|
||||||
init {
|
|
||||||
bluetoothController.isConnected.onEach {
|
|
||||||
isConnected -> _bluetoothState.update { it.copy(isConnected = isConnected) }
|
|
||||||
}.launchIn(viewModelScope)
|
|
||||||
bluetoothController.errors.onEach { error ->
|
|
||||||
_bluetoothState.update {
|
|
||||||
it.copy(errorMessage = error)
|
|
||||||
}
|
|
||||||
}.launchIn(viewModelScope)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Flow<ConnectionResult>.listen(): Job {
|
|
||||||
return onEach { result ->
|
|
||||||
when(result) {
|
|
||||||
ConnectionResult.ConnectionEstablished -> {
|
|
||||||
_bluetoothState.update {
|
|
||||||
it.copy(
|
|
||||||
isConnected = true,
|
|
||||||
isConnecting = false,
|
|
||||||
errorMessage = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is ConnectionResult.Error -> {
|
|
||||||
_bluetoothState.update { it.copy(
|
|
||||||
isConnected = false,
|
|
||||||
isConnecting = false,
|
|
||||||
errorMessage = result.message
|
|
||||||
) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.catch { throwable ->
|
|
||||||
bluetoothController.closeConnection()
|
|
||||||
_bluetoothState.update {
|
|
||||||
it.copy(
|
|
||||||
isConnected = false,
|
|
||||||
isConnecting = false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.launchIn(viewModelScope)
|
|
||||||
}
|
|
||||||
private var deviceConnectionJob: Job? = null
|
|
||||||
|
|
||||||
fun connectToDevice(device: Device) {
|
|
||||||
_bluetoothState.update {it.copy(isConnecting = true)}
|
|
||||||
deviceConnectionJob = bluetoothController
|
|
||||||
.connectToDevice(device)
|
|
||||||
.listen()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun disconnectFromDevice() {
|
|
||||||
deviceConnectionJob?.cancel()
|
|
||||||
bluetoothController.closeConnection()
|
|
||||||
_bluetoothState.update {
|
|
||||||
it.copy(
|
|
||||||
isConnecting = false,
|
|
||||||
isConnected = false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class PermissionDialogViewModel: ViewModel() {
|
class PermissionDialogViewModel: ViewModel() {
|
||||||
val visiblePermissionDialogQueue = mutableStateListOf<String>()
|
val visiblePermissionDialogQueue = mutableStateListOf<String>()
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.content.Intent
|
|||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
|
import android.util.Log
|
||||||
import androidx.activity.compose.ManagedActivityResultLauncher
|
import androidx.activity.compose.ManagedActivityResultLauncher
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
package com.helible.pilot
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Activity
|
|
||||||
import android.bluetooth.BluetoothAdapter
|
|
||||||
import android.bluetooth.BluetoothDevice
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.location.LocationManager
|
|
||||||
import android.os.Build
|
|
||||||
import android.util.Log
|
|
||||||
|
|
||||||
|
|
||||||
fun registerIntentFilters(activity: Activity, receiver: BroadcastReceiver) {
|
|
||||||
activity.registerReceiver(receiver, IntentFilter(BluetoothDevice.ACTION_FOUND))
|
|
||||||
activity.registerReceiver(receiver, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED))
|
|
||||||
activity.registerReceiver(receiver, IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_STARTED))
|
|
||||||
activity.registerReceiver(receiver, IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED))
|
|
||||||
if (Build.VERSION.SDK_INT <= 30)
|
|
||||||
activity.registerReceiver(receiver, IntentFilter(LocationManager.PROVIDERS_CHANGED_ACTION))
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
|
||||||
fun receiveIntentChanges(
|
|
||||||
intent: Intent,
|
|
||||||
mainViewModel: MainViewModel,
|
|
||||||
bluetoothAdapter: BluetoothAdapter?,
|
|
||||||
locationManager: LocationManager,
|
|
||||||
) {
|
|
||||||
when (intent.action) {
|
|
||||||
BluetoothDevice.ACTION_FOUND -> {
|
|
||||||
val device = if (Build.VERSION.SDK_INT >= 33) {
|
|
||||||
intent.getParcelableExtra(
|
|
||||||
BluetoothDevice.EXTRA_DEVICE,
|
|
||||||
BluetoothDevice::class.java
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
@Suppress("DEPRECATION") intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
|
|
||||||
}
|
|
||||||
|
|
||||||
val rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, Short.MIN_VALUE)
|
|
||||||
if (device?.name != null)
|
|
||||||
mainViewModel.devices.add(Device(device, rssi))
|
|
||||||
Log.i(
|
|
||||||
"ScanActivity",
|
|
||||||
"Found new device: ${device?.name} ${device?.address} $rssi"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
BluetoothAdapter.ACTION_STATE_CHANGED -> {
|
|
||||||
val state: Int = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1)
|
|
||||||
Log.i("ScanActvity", "Bluetooth state: $state")
|
|
||||||
when (state) {
|
|
||||||
BluetoothAdapter.STATE_ON -> {
|
|
||||||
Log.i("ScanActvity", "Bluetooth turned on")
|
|
||||||
mainViewModel.bluetoothTurnOnState.value = true
|
|
||||||
mainViewModel.devices.clear()
|
|
||||||
bluetoothAdapter?.startDiscovery()
|
|
||||||
}
|
|
||||||
|
|
||||||
BluetoothAdapter.STATE_OFF -> {
|
|
||||||
Log.i("ScanActvity", "Bluetooth turned off")
|
|
||||||
mainViewModel.bluetoothTurnOnState.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LocationManager.PROVIDERS_CHANGED_ACTION -> {
|
|
||||||
mainViewModel.locationTurnOnState.value = locationManager.isLocationEnabled
|
|
||||||
if (mainViewModel.locationTurnOnState.value == true) {
|
|
||||||
Log.i("ScanActivity", "LOCATION IS ON")
|
|
||||||
} else if (mainViewModel.locationTurnOnState.value == false)
|
|
||||||
Log.i("ScanActivity", "LOCATION IS OFF")
|
|
||||||
}
|
|
||||||
|
|
||||||
BluetoothAdapter.ACTION_DISCOVERY_FINISHED -> {
|
|
||||||
mainViewModel.isBluetoothDiscoveryRunning.value = false
|
|
||||||
Log.i("ScanActivity", "DISCOVERY FINISHED")
|
|
||||||
}
|
|
||||||
|
|
||||||
BluetoothAdapter.ACTION_DISCOVERY_STARTED -> {
|
|
||||||
mainViewModel.isBluetoothDiscoveryRunning.value = true
|
|
||||||
Log.i("ScanActivity", "DISCOVERY STARTED")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,25 +4,23 @@ import android.annotation.SuppressLint
|
|||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.bluetooth.BluetoothAdapter
|
import android.bluetooth.BluetoothAdapter
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.MutableState
|
|
||||||
import com.helible.pilot.components.RequiredHardwareFeatures
|
import com.helible.pilot.components.RequiredHardwareFeatures
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
@Composable
|
@Composable
|
||||||
fun RequestHardwareFeatures(
|
fun RequestHardwareFeatures(
|
||||||
activity: Activity,
|
activity: Activity,
|
||||||
turnOnLocation: Boolean,
|
bluetoothUiState: BluetoothUiState
|
||||||
bluetoothTurnOnState: MutableState<Boolean?>,
|
|
||||||
locationTurnOnState: MutableState<Boolean?>
|
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
RequiredHardwareFeatures(
|
RequiredHardwareFeatures(
|
||||||
title = "Включите Bluetooth",
|
title = "Включите Bluetooth",
|
||||||
description = "Для работы приложения требуется Bluetooth",
|
description = "Для работы приложения требуется Bluetooth",
|
||||||
confirmButtonText = "Включить Bluetooth",
|
confirmButtonText = "Включить Bluetooth",
|
||||||
featureState = bluetoothTurnOnState,
|
featureState = bluetoothUiState.isEnabled,
|
||||||
requestFeature = {
|
requestFeature = {
|
||||||
val intent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
|
val intent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
|
||||||
activity.startActivity(intent)
|
activity.startActivity(intent)
|
||||||
@@ -30,13 +28,13 @@ fun RequestHardwareFeatures(
|
|||||||
onDismissRequest = {}
|
onDismissRequest = {}
|
||||||
)
|
)
|
||||||
|
|
||||||
if (turnOnLocation) {
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
|
||||||
RequiredHardwareFeatures(
|
RequiredHardwareFeatures(
|
||||||
title = "Пожалуйста, включите геолокацию",
|
title = "Пожалуйста, включите геолокацию",
|
||||||
description = "Для работы с Bluetooth на устройствах с Android 11 и более ранних версиях, " +
|
description = "Для работы с Bluetooth на устройствах с Android 11 и более ранних версиях, " +
|
||||||
"требуется геолокация.",
|
"требуется геолокация.",
|
||||||
confirmButtonText = "Включить геолокацию",
|
confirmButtonText = "Включить геолокацию",
|
||||||
featureState = locationTurnOnState,
|
featureState = bluetoothUiState.isLocationEnabled,
|
||||||
requestFeature = {
|
requestFeature = {
|
||||||
val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
|
val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
|
||||||
activity.startActivity(intent)
|
activity.startActivity(intent)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.helible.pilot.components
|
package com.helible.pilot.components
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.bluetooth.BluetoothAdapter
|
import android.util.Log
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -18,22 +18,23 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.MutableState
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.constraintlayout.compose.ConstraintLayout
|
import androidx.constraintlayout.compose.ConstraintLayout
|
||||||
import androidx.constraintlayout.compose.Dimension
|
import androidx.constraintlayout.compose.Dimension
|
||||||
|
import com.helible.pilot.BluetoothUiState
|
||||||
import com.helible.pilot.Device
|
import com.helible.pilot.Device
|
||||||
|
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
@Composable
|
@Composable
|
||||||
fun BluetoothScannerScreen(
|
fun BluetoothScannerScreen(
|
||||||
devices: MutableList<Device>,
|
bluetoothState: BluetoothUiState,
|
||||||
selectedDevice: MutableState<Device?>,
|
selectedDevice: Device?,
|
||||||
bluetoothIsDiscoveringState: MutableState<Boolean>,
|
startScan: () -> Unit,
|
||||||
bluetoothAdapter: BluetoothAdapter?,
|
cancelScan: () -> Unit,
|
||||||
|
choiceDevice: (device: Device?) -> Unit,
|
||||||
onScreenChanged: () -> Unit,
|
onScreenChanged: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
@@ -52,8 +53,9 @@ fun BluetoothScannerScreen(
|
|||||||
)
|
)
|
||||||
|
|
||||||
DiscoveredDevicesList(
|
DiscoveredDevicesList(
|
||||||
devices = devices,
|
devices = bluetoothState.scannedDevices,
|
||||||
selectedDevice = selectedDevice,
|
selectedDevice = selectedDevice,
|
||||||
|
choiceDevice = choiceDevice,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.constrainAs(devicesList) {
|
.constrainAs(devicesList) {
|
||||||
top.linkTo(title.bottom)
|
top.linkTo(title.bottom)
|
||||||
@@ -62,7 +64,7 @@ fun BluetoothScannerScreen(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if (devices.isEmpty() && bluetoothIsDiscoveringState.value) {
|
if (bluetoothState.scannedDevices.isEmpty() && bluetoothState.isDiscovering) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
@@ -81,21 +83,20 @@ fun BluetoothScannerScreen(
|
|||||||
horizontalArrangement = Arrangement.Center
|
horizontalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
FilledIconToggleButton(
|
FilledIconToggleButton(
|
||||||
checked = bluetoothIsDiscoveringState.value,
|
checked = bluetoothState.isDiscovering,
|
||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
selectedDevice.value = null
|
if (bluetoothState.isDiscovering) {
|
||||||
if (bluetoothIsDiscoveringState.value)
|
cancelScan()
|
||||||
bluetoothAdapter?.cancelDiscovery()
|
Log.i("ScanActivity", "Trying to start scan via button")
|
||||||
else {
|
} else {
|
||||||
devices.clear()
|
startScan()
|
||||||
bluetoothAdapter?.startDiscovery()
|
|
||||||
}
|
}
|
||||||
}, modifier = Modifier
|
}, modifier = Modifier
|
||||||
.align(Alignment.Bottom)
|
.align(Alignment.Bottom)
|
||||||
.padding(5.dp)
|
.padding(5.dp)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
if (bluetoothIsDiscoveringState.value) Icons.Filled.Close
|
if (bluetoothState.isDiscovering) Icons.Filled.Close
|
||||||
else Icons.Filled.Refresh,
|
else Icons.Filled.Refresh,
|
||||||
contentDescription = null
|
contentDescription = null
|
||||||
)
|
)
|
||||||
@@ -107,7 +108,7 @@ fun BluetoothScannerScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.Bottom)
|
.align(Alignment.Bottom)
|
||||||
.padding(5.dp),
|
.padding(5.dp),
|
||||||
enabled = selectedDevice.value != null,
|
enabled = selectedDevice != null,
|
||||||
) {
|
) {
|
||||||
Text(text = "Далее")
|
Text(text = "Далее")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,12 +29,12 @@ import com.helible.pilot.R
|
|||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
@Composable
|
@Composable
|
||||||
fun DeviceItem(deviceInfo: Device, selectedDevice: MutableState<Device?>, modifier: Modifier) {
|
fun DeviceItem(deviceInfo: Device, selectedDevice: Device?, choiceDevice: (device: Device?) -> Unit, modifier: Modifier) {
|
||||||
ElevatedCard(
|
ElevatedCard(
|
||||||
modifier=modifier.clickable {
|
modifier=modifier.clickable {
|
||||||
selectedDevice.value = deviceInfo
|
choiceDevice(deviceInfo)
|
||||||
},
|
},
|
||||||
colors = CardDefaults.elevatedCardColors(containerColor = if (deviceInfo.bluetoothDevice == selectedDevice.value?.bluetoothDevice)
|
colors = CardDefaults.elevatedCardColors(containerColor = if (deviceInfo.bluetoothDevice == selectedDevice?.bluetoothDevice)
|
||||||
MaterialTheme.colorScheme.secondaryContainer
|
MaterialTheme.colorScheme.secondaryContainer
|
||||||
else MaterialTheme.colorScheme.surface
|
else MaterialTheme.colorScheme.surface
|
||||||
)
|
)
|
||||||
@@ -42,12 +42,7 @@ fun DeviceItem(deviceInfo: Device, selectedDevice: MutableState<Device?>, modifi
|
|||||||
Row(modifier=Modifier.padding(8.dp)) {
|
Row(modifier=Modifier.padding(8.dp)) {
|
||||||
Column(verticalArrangement = Arrangement.Center) {
|
Column(verticalArrangement = Arrangement.Center) {
|
||||||
Text(text=deviceInfo.bluetoothDevice.name, fontWeight = FontWeight.Bold, softWrap = true)
|
Text(text=deviceInfo.bluetoothDevice.name, fontWeight = FontWeight.Bold, softWrap = true)
|
||||||
AndroidView(factory = { context ->
|
Text(text="MAC: ${deviceInfo.bluetoothDevice.address}", fontWeight = FontWeight.Thin)
|
||||||
TextView(context).apply {
|
|
||||||
// Using old TextView for text formatting
|
|
||||||
text = HtmlCompat.fromHtml("<b>MAC</b>: ${deviceInfo.bluetoothDevice.address}", HtmlCompat.FROM_HTML_MODE_LEGACY)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
Box(contentAlignment = Alignment.CenterEnd, modifier = Modifier.fillMaxSize()) {
|
Box(contentAlignment = Alignment.CenterEnd, modifier = Modifier.fillMaxSize()) {
|
||||||
Icon(
|
Icon(
|
||||||
|
|||||||
@@ -5,18 +5,18 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.MutableState
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.helible.pilot.Device
|
import com.helible.pilot.Device
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DiscoveredDevicesList(devices: MutableList<Device>, selectedDevice: MutableState<Device?>, modifier: Modifier = Modifier) {
|
fun DiscoveredDevicesList(devices: List<Device>, selectedDevice: Device?, choiceDevice: (device: Device?) -> Unit, modifier: Modifier = Modifier) {
|
||||||
LazyColumn(modifier = modifier) {
|
LazyColumn(modifier = modifier) {
|
||||||
items(devices) { device ->
|
items(devices) { device ->
|
||||||
DeviceItem(
|
DeviceItem(
|
||||||
deviceInfo = device,
|
deviceInfo = device,
|
||||||
selectedDevice = selectedDevice,
|
selectedDevice = selectedDevice,
|
||||||
|
choiceDevice = choiceDevice,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(
|
.padding(
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ fun RequiredHardwareFeatures(
|
|||||||
title: String,
|
title: String,
|
||||||
description: String,
|
description: String,
|
||||||
confirmButtonText: String,
|
confirmButtonText: String,
|
||||||
featureState: MutableState<Boolean?>,
|
featureState: Boolean,
|
||||||
requestFeature: () -> Unit,
|
requestFeature: () -> Unit,
|
||||||
onDismissRequest: () -> Unit
|
onDismissRequest: () -> Unit
|
||||||
) {
|
) {
|
||||||
if (featureState.value == false || featureState.value == null) {
|
if (!featureState) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
Divider()
|
Divider()
|
||||||
|
|||||||
Reference in New Issue
Block a user