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