feat: reflect bt connection state in ui
This commit is contained in:
parent
afc3378828
commit
a74d0ddef5
@ -27,7 +27,9 @@ class SettingsViewModel(
|
||||
private set
|
||||
var connectionState by mutableStateOf(Settings.ConnectionState.Unregistered)
|
||||
private set
|
||||
var controllerConnectionState by mutableIntStateOf(controllerService.connectionState)
|
||||
var bluetoothConnectionState by mutableStateOf(bluetoothService.state)
|
||||
private set
|
||||
var currentBluetoothDevice by mutableStateOf<BluetoothDevice?>(bluetoothService.currentDevice)
|
||||
private set
|
||||
|
||||
var pairedDevices = bluetoothService.pairedDevices
|
||||
@ -37,8 +39,9 @@ class SettingsViewModel(
|
||||
viewModelScope.launch {
|
||||
updateDeviceInfo()
|
||||
}
|
||||
controllerService.onConnectionStateChanged {
|
||||
controllerConnectionState = it
|
||||
bluetoothService.onBluetoothStateChanged {
|
||||
currentBluetoothDevice = bluetoothService.currentDevice
|
||||
bluetoothConnectionState = it
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,6 +79,12 @@ class SettingsViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun disconnectBluetoothDevice() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
bluetoothService.closeBluetoothConnection()
|
||||
}
|
||||
}
|
||||
|
||||
fun onServerAddressChanged(url: String) {
|
||||
serverAddress = url
|
||||
}
|
||||
@ -89,8 +98,8 @@ class SettingsViewModel(
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MAIN_SETTINGS_VIEW = 0
|
||||
const val CONNECT_CONTROLLER_VIEW = 1
|
||||
const val MAIN_SETTINGS_VIEW = "main_settings_view"
|
||||
const val CONNECT_CONTROLLER_VIEW = "connect_controller_view"
|
||||
|
||||
fun provideFactory(
|
||||
deviceService: DeviceService,
|
||||
|
||||
@ -11,6 +11,7 @@ import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.core.content.ContextCompat.getSystemService
|
||||
import com.example.tvcontroller.data.BluetoothDevice
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@ -32,30 +33,52 @@ class BluetoothService(private val context: Context) {
|
||||
var pairedDevices: StateFlow<List<BluetoothDevice>> = _pairedDevices.asStateFlow()
|
||||
private var clientSocket: BluetoothSocket? = null
|
||||
|
||||
private var bluetoothStateChangedCallbacks: MutableList<(Int) -> Unit> = mutableListOf()
|
||||
private var bluetoothStateChangedCallbacks: MutableList<(String) -> Unit> = mutableListOf()
|
||||
private var bluetoothStateReceiver: BroadcastReceiver? = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)
|
||||
bluetoothStateChangedCallbacks.forEach { it(state) }
|
||||
val btState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)
|
||||
Log.i(TAG, "Bluetooth state changed to: $btState")
|
||||
state = parseBluetoothState(btState)
|
||||
}
|
||||
}
|
||||
var currentDevice: BluetoothDevice? = null
|
||||
private set
|
||||
|
||||
var state: String = STATE_OFF
|
||||
private set(value) {
|
||||
if (value != STATE_CONNECTED && value != STATE_CONNECTING) currentDevice = null
|
||||
Log.i(TAG, "Bluetooth state changed to: $value")
|
||||
bluetoothStateChangedCallbacks.forEach { it(value) }
|
||||
field = value
|
||||
}
|
||||
|
||||
init {
|
||||
state = parseBluetoothState(bluetoothAdapter.state)
|
||||
context.registerReceiver(
|
||||
bluetoothStateReceiver, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)
|
||||
)
|
||||
updatePairedDevices()
|
||||
}
|
||||
|
||||
private fun parseBluetoothState(state: Int): String {
|
||||
return when (state) {
|
||||
BluetoothAdapter.STATE_OFF -> STATE_OFF
|
||||
BluetoothAdapter.STATE_TURNING_OFF -> STATE_OFF
|
||||
BluetoothAdapter.STATE_TURNING_ON -> STATE_OFF
|
||||
BluetoothAdapter.STATE_ON -> STATE_DISCONNECTED
|
||||
else -> STATE_DISCONNECTED
|
||||
}
|
||||
}
|
||||
|
||||
fun isBluetoothEnabled(): Boolean {
|
||||
return bluetoothAdapter.isEnabled
|
||||
}
|
||||
|
||||
fun onBluetoothStateChanged(callback: (Int) -> Unit) {
|
||||
fun onBluetoothStateChanged(callback: (String) -> Unit) {
|
||||
bluetoothStateChangedCallbacks.add(callback)
|
||||
}
|
||||
|
||||
fun offBluetoothStateChanged(callback: (Int) -> Unit) {
|
||||
fun offBluetoothStateChanged(callback: (String) -> Unit) {
|
||||
bluetoothStateChangedCallbacks.remove(callback)
|
||||
}
|
||||
|
||||
@ -77,9 +100,12 @@ class BluetoothService(private val context: Context) {
|
||||
}
|
||||
|
||||
fun connectToDevice(device: BluetoothDevice) {
|
||||
currentDevice = device
|
||||
state = STATE_CONNECTING
|
||||
Log.i(TAG, "Initiating connection process")
|
||||
if (context.checkSelfPermission(Manifest.permission.BLUETOOTH_ADMIN) != PackageManager.PERMISSION_GRANTED) {
|
||||
Log.e(TAG, "Bluetooth permission not granted")
|
||||
state = STATE_DISCONNECTED
|
||||
return
|
||||
}
|
||||
Log.i(TAG, "Connecting to device: $device")
|
||||
@ -91,8 +117,11 @@ class BluetoothService(private val context: Context) {
|
||||
)
|
||||
Log.i(TAG, "Connecting to socket")
|
||||
clientSocket?.connect()
|
||||
currentDevice = device
|
||||
state = STATE_CONNECTED
|
||||
Log.i(TAG, "Connected to device: $device")
|
||||
} catch (e: IOException) {
|
||||
state = STATE_DISCONNECTED
|
||||
Log.e(TAG, "Error connecting to device: $e")
|
||||
}
|
||||
}
|
||||
@ -103,16 +132,18 @@ class BluetoothService(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
fun getBluetoothState(): Int {
|
||||
return bluetoothAdapter.state
|
||||
}
|
||||
|
||||
fun closeBluetoothConnection() {
|
||||
clientSocket?.close()
|
||||
clientSocket = null
|
||||
currentDevice = null
|
||||
state = STATE_DISCONNECTED
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val STATE_OFF = "off"
|
||||
const val STATE_DISCONNECTED = "disconnected"
|
||||
const val STATE_CONNECTING = "connecting"
|
||||
const val STATE_CONNECTED = "connected"
|
||||
val BLUETOOTH_PERMISSIONS = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||
arrayOf(
|
||||
Manifest.permission.BLUETOOTH, Manifest.permission.BLUETOOTH_ADMIN
|
||||
|
||||
@ -9,7 +9,7 @@ class ControllerService(bluetoothService: BluetoothService) {
|
||||
private set
|
||||
|
||||
init {
|
||||
handleBluetoothStateChanged(bluetoothService.getBluetoothState())
|
||||
handleBluetoothStateChanged(bluetoothService.state)
|
||||
bluetoothService.onBluetoothStateChanged {
|
||||
handleBluetoothStateChanged(it)
|
||||
}
|
||||
@ -24,9 +24,9 @@ class ControllerService(bluetoothService: BluetoothService) {
|
||||
connectionStateChangedListener.forEach { it(state) }
|
||||
}
|
||||
|
||||
private fun handleBluetoothStateChanged(state: Int) {
|
||||
private fun handleBluetoothStateChanged(state: String) {
|
||||
when (state) {
|
||||
BluetoothAdapter.STATE_OFF -> setConnectionState(BLUETOOTH_DISABLED)
|
||||
BluetoothService.STATE_OFF -> setConnectionState(BLUETOOTH_DISABLED)
|
||||
else -> setConnectionState(NOT_CONNECTED)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
package com.example.tvcontroller.ui.views
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@ -19,12 +19,9 @@ import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarColors
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
@ -40,7 +37,6 @@ import com.example.tvcontroller.SettingsViewModel.Companion.MAIN_SETTINGS_VIEW
|
||||
import com.example.tvcontroller.services.BluetoothService
|
||||
import com.example.tvcontroller.services.ControllerService
|
||||
import com.example.tvcontroller.services.DeviceService
|
||||
import kotlinx.coroutines.flow.toList
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@ -104,18 +100,28 @@ fun SettingsView(
|
||||
style = MaterialTheme.typography.headlineSmall
|
||||
)
|
||||
Text(
|
||||
text = "Controller status: " + getControllerConnectionStateString(viewModel.controllerConnectionState) + ".",
|
||||
text = "Controller status: " + getBluetoothConnectionStateString(viewModel.bluetoothConnectionState) + ".",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
OutlinedButton(
|
||||
onClick = { navController.navigate(CONNECT_CONTROLLER_VIEW.toString()) },
|
||||
enabled = viewModel.controllerConnectionState != ControllerService.BLUETOOTH_DISABLED && viewModel.controllerConnectionState != ControllerService.CONNECTED,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
stringResource(id = R.string.connect_button_label)
|
||||
)
|
||||
}
|
||||
if (viewModel.bluetoothConnectionState == BluetoothService.STATE_CONNECTED)
|
||||
OutlinedButton(
|
||||
onClick = viewModel::disconnectBluetoothDevice,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
stringResource(id = R.string.disconnect_button_label)
|
||||
)
|
||||
}
|
||||
else
|
||||
OutlinedButton(
|
||||
onClick = { navController.navigate(CONNECT_CONTROLLER_VIEW.toString()) },
|
||||
enabled = viewModel.bluetoothConnectionState != BluetoothService.STATE_OFF,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
stringResource(id = R.string.connect_button_label)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -145,6 +151,7 @@ fun SettingsView(
|
||||
modifier = Modifier.padding(start = 16.dp)
|
||||
)
|
||||
pairedDevices.value.forEach { device ->
|
||||
if (device == null) return
|
||||
ListItem(
|
||||
headlineContent = { Text(device.getName()) },
|
||||
supportingContent = {
|
||||
@ -156,7 +163,21 @@ fun SettingsView(
|
||||
viewModel.connectBluetoothDevice(
|
||||
device
|
||||
)
|
||||
})
|
||||
}),
|
||||
trailingContent = {
|
||||
if (device == viewModel.currentBluetoothDevice)
|
||||
if (viewModel.bluetoothConnectionState == BluetoothService.STATE_CONNECTED)
|
||||
Icon(
|
||||
painterResource(id = R.drawable.baseline_check_24),
|
||||
contentDescription = "state"
|
||||
)
|
||||
else
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.width(16.dp),
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
trackColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -178,12 +199,12 @@ fun getConnectionStateString(state: Settings.ConnectionState): String {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getControllerConnectionStateString(state: Int): String {
|
||||
fun getBluetoothConnectionStateString(state: String): String {
|
||||
return when (state) {
|
||||
ControllerService.BLUETOOTH_DISABLED -> stringResource(id = R.string.controller_state_bluetooth_disabled)
|
||||
ControllerService.NOT_PAIRED -> stringResource(id = R.string.controller_state_not_paired)
|
||||
ControllerService.NOT_CONNECTED -> stringResource(id = R.string.controller_state_not_connected)
|
||||
ControllerService.CONNECTED -> stringResource(id = R.string.controller_state_connected)
|
||||
BluetoothService.STATE_OFF -> stringResource(id = R.string.controller_state_bluetooth_disabled)
|
||||
BluetoothService.STATE_DISCONNECTED -> stringResource(id = R.string.bluetooth_state_disconnected)
|
||||
BluetoothService.STATE_CONNECTING -> stringResource(id = R.string.bluetooth_state_connecting)
|
||||
BluetoothService.STATE_CONNECTED -> stringResource(id = R.string.bluetooth_state_connected)
|
||||
else -> "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
5
app/src/main/res/drawable/baseline_check_24.xml
Normal file
5
app/src/main/res/drawable/baseline_check_24.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
|
||||
|
||||
</vector>
|
||||
@ -14,10 +14,11 @@
|
||||
<string name="connection_state_unregistered">unregistered</string>
|
||||
<string name="connection_state_registered">registered</string>
|
||||
<string name="controller_state_bluetooth_disabled">Bluetooth is turned off</string>
|
||||
<string name="controller_state_not_paired">Not paired</string>
|
||||
<string name="controller_state_not_connected">Not connected</string>
|
||||
<string name="controller_state_connected">Connected</string>
|
||||
<string name="paired_devices_label">Paired devices</string>
|
||||
<string name="scanned_devices_label">Scanned devices</string>
|
||||
<string name="connect_controller_title">Connect controller</string>
|
||||
<string name="bluetooth_state_connecting">connecting</string>
|
||||
<string name="bluetooth_state_connected">connected</string>
|
||||
<string name="bluetooth_state_disconnected">disconnected</string>
|
||||
<string name="disconnect_button_label">Disconnect</string>
|
||||
</resources>
|
||||
Loading…
Reference in New Issue
Block a user