diff --git a/app/src/main/java/com/example/tvcontroller/SettingsViewModel.kt b/app/src/main/java/com/example/tvcontroller/SettingsViewModel.kt index ff6d44b..67c88ec 100644 --- a/app/src/main/java/com/example/tvcontroller/SettingsViewModel.kt +++ b/app/src/main/java/com/example/tvcontroller/SettingsViewModel.kt @@ -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(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, diff --git a/app/src/main/java/com/example/tvcontroller/services/BluetoothService.kt b/app/src/main/java/com/example/tvcontroller/services/BluetoothService.kt index 12d6fa1..c8428a3 100644 --- a/app/src/main/java/com/example/tvcontroller/services/BluetoothService.kt +++ b/app/src/main/java/com/example/tvcontroller/services/BluetoothService.kt @@ -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> = _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 diff --git a/app/src/main/java/com/example/tvcontroller/services/ControllerService.kt b/app/src/main/java/com/example/tvcontroller/services/ControllerService.kt index accb8e6..4880587 100644 --- a/app/src/main/java/com/example/tvcontroller/services/ControllerService.kt +++ b/app/src/main/java/com/example/tvcontroller/services/ControllerService.kt @@ -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) } } diff --git a/app/src/main/java/com/example/tvcontroller/ui/views/SettingsView.kt b/app/src/main/java/com/example/tvcontroller/ui/views/SettingsView.kt index 177e4e2..db6591e 100644 --- a/app/src/main/java/com/example/tvcontroller/ui/views/SettingsView.kt +++ b/app/src/main/java/com/example/tvcontroller/ui/views/SettingsView.kt @@ -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" } } diff --git a/app/src/main/res/drawable/baseline_check_24.xml b/app/src/main/res/drawable/baseline_check_24.xml new file mode 100644 index 0000000..356e998 --- /dev/null +++ b/app/src/main/res/drawable/baseline_check_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 70f2331..7148af9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -14,10 +14,11 @@ unregistered registered Bluetooth is turned off - Not paired - Not connected - Connected Paired devices Scanned devices Connect controller + connecting + connected + disconnected + Disconnect \ No newline at end of file