feat: scan for bluetooth devices, list in connect view
This commit is contained in:
parent
e3623cb128
commit
8eef70f59b
@ -5,8 +5,10 @@
|
|||||||
<uses-feature
|
<uses-feature
|
||||||
android:name="android.hardware.camera"
|
android:name="android.hardware.camera"
|
||||||
android:required="false" />
|
android:required="false" />
|
||||||
|
<uses-feature android:name="android.hardware.bluetooth" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
|
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import androidx.compose.ui.res.painterResource
|
|||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import com.example.tvcontroller.services.BluetoothService
|
import com.example.tvcontroller.services.BluetoothService
|
||||||
import com.example.tvcontroller.services.CameraService
|
import com.example.tvcontroller.services.CameraService
|
||||||
|
import com.example.tvcontroller.services.ControllerService
|
||||||
import com.example.tvcontroller.services.DeviceService
|
import com.example.tvcontroller.services.DeviceService
|
||||||
import com.example.tvcontroller.ui.views.CameraView
|
import com.example.tvcontroller.ui.views.CameraView
|
||||||
import com.example.tvcontroller.ui.views.SettingsView
|
import com.example.tvcontroller.ui.views.SettingsView
|
||||||
@ -39,19 +40,22 @@ class MainActivity : ComponentActivity() {
|
|||||||
private lateinit var bluetoothService: BluetoothService
|
private lateinit var bluetoothService: BluetoothService
|
||||||
private lateinit var deviceService: DeviceService
|
private lateinit var deviceService: DeviceService
|
||||||
private lateinit var cameraService: CameraService
|
private lateinit var cameraService: CameraService
|
||||||
|
private lateinit var controllerService: ControllerService
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
bluetoothService = BluetoothService(this.applicationContext)
|
bluetoothService = BluetoothService(this.applicationContext)
|
||||||
deviceService = DeviceService(this.applicationContext)
|
deviceService = DeviceService(this.applicationContext)
|
||||||
cameraService = CameraService(this.applicationContext)
|
cameraService = CameraService(this.applicationContext)
|
||||||
|
controllerService = ControllerService(bluetoothService)
|
||||||
checkPermissions()
|
checkPermissions()
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
TVControllerTheme {
|
TVControllerTheme {
|
||||||
TvControllerApp(
|
TvControllerApp(
|
||||||
bluetoothService = bluetoothService,
|
deviceService = deviceService,
|
||||||
deviceService = deviceService
|
controllerService = controllerService,
|
||||||
|
bluetoothService = bluetoothService
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -61,14 +65,18 @@ class MainActivity : ComponentActivity() {
|
|||||||
if (!cameraService.hasRequiredPermissions()) {
|
if (!cameraService.hasRequiredPermissions()) {
|
||||||
ActivityCompat.requestPermissions(this, CameraService.CAMERAX_PERMISSIONS, 0)
|
ActivityCompat.requestPermissions(this, CameraService.CAMERAX_PERMISSIONS, 0)
|
||||||
}
|
}
|
||||||
|
if (!bluetoothService.hasRequiredPermissions()) {
|
||||||
|
ActivityCompat.requestPermissions(this, BluetoothService.BLUETOOTH_PERMISSIONS, 0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TvControllerApp(
|
fun TvControllerApp(
|
||||||
navController: NavHostController = rememberNavController(),
|
navController: NavHostController = rememberNavController(),
|
||||||
bluetoothService: BluetoothService,
|
deviceService: DeviceService,
|
||||||
deviceService: DeviceService
|
controllerService: ControllerService,
|
||||||
|
bluetoothService: BluetoothService
|
||||||
) {
|
) {
|
||||||
val backStackEntry by navController.currentBackStackEntryAsState()
|
val backStackEntry by navController.currentBackStackEntryAsState()
|
||||||
val currentScreen = Screen.valueOf(backStackEntry?.destination?.route ?: Screen.Camera.name)
|
val currentScreen = Screen.valueOf(backStackEntry?.destination?.route ?: Screen.Camera.name)
|
||||||
@ -112,24 +120,23 @@ fun TvControllerApp(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}) { innerPadding ->
|
}) { innerPadding ->
|
||||||
Column {
|
NavHost(
|
||||||
NavHost(
|
navController = navController,
|
||||||
navController = navController,
|
startDestination = Screen.Camera.name,
|
||||||
startDestination = Screen.Camera.name,
|
modifier = Modifier.padding(innerPadding)
|
||||||
modifier = Modifier.padding(innerPadding)
|
) {
|
||||||
) {
|
composable(route = Screen.Camera.name) {
|
||||||
composable(route = Screen.Camera.name) {
|
CameraView()
|
||||||
CameraView()
|
}
|
||||||
}
|
composable(route = Screen.Remote.name) {
|
||||||
composable(route = Screen.Remote.name) {
|
RemoteScreen()
|
||||||
RemoteScreen()
|
}
|
||||||
}
|
composable(route = Screen.Settings.name) {
|
||||||
composable(route = Screen.Settings.name) {
|
SettingsView(
|
||||||
SettingsView(
|
deviceService = deviceService,
|
||||||
deviceService = deviceService,
|
controllerService = controllerService,
|
||||||
bluetoothService = bluetoothService
|
bluetoothService = bluetoothService
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,18 +1,22 @@
|
|||||||
package com.example.tvcontroller
|
package com.example.tvcontroller
|
||||||
|
|
||||||
import android.bluetooth.BluetoothAdapter
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.example.tvcontroller.services.BluetoothService
|
import com.example.tvcontroller.services.BluetoothService
|
||||||
|
import com.example.tvcontroller.services.ControllerService
|
||||||
import com.example.tvcontroller.services.DeviceService
|
import com.example.tvcontroller.services.DeviceService
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class SettingsViewModel(
|
class SettingsViewModel(
|
||||||
private val deviceService: DeviceService,
|
private val deviceService: DeviceService,
|
||||||
|
controllerService: ControllerService,
|
||||||
private val bluetoothService: BluetoothService
|
private val bluetoothService: BluetoothService
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
var serverAddress by mutableStateOf(deviceService.getServerAddress())
|
var serverAddress by mutableStateOf(deviceService.getServerAddress())
|
||||||
@ -23,16 +27,20 @@ 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 bluetoothEnabled by mutableStateOf(bluetoothService.isBluetoothEnabled())
|
var controllerConnectionState by mutableIntStateOf(controllerService.connectionState)
|
||||||
private set
|
private set
|
||||||
|
var activeView by mutableStateOf(MAIN_SETTINGS_VIEW)
|
||||||
|
|
||||||
|
var scannedDevices = bluetoothService.scannedDevices
|
||||||
|
var pairedDevices = bluetoothService.pairedDevices
|
||||||
|
|
||||||
init {
|
init {
|
||||||
updateConnectionState()
|
updateConnectionState()
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
updateDeviceInfo()
|
updateDeviceInfo()
|
||||||
}
|
}
|
||||||
bluetoothService.onBluetoothStateChanged {
|
controllerService.onConnectionStateChanged {
|
||||||
bluetoothEnabled = it == BluetoothAdapter.STATE_ON
|
controllerConnectionState = it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,6 +53,11 @@ class SettingsViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun connectController() {
|
||||||
|
//Log.i("SettingsScreen", "Connect controller")
|
||||||
|
//controllerService.connect()
|
||||||
|
}
|
||||||
|
|
||||||
private fun updateConnectionState() {
|
private fun updateConnectionState() {
|
||||||
connectionState = if (deviceService.getToken().isEmpty()) {
|
connectionState = if (deviceService.getToken().isEmpty()) {
|
||||||
Settings.ConnectionState.Unregistered
|
Settings.ConnectionState.Unregistered
|
||||||
@ -76,14 +89,26 @@ class SettingsViewModel(
|
|||||||
registrationCode = code
|
registrationCode = code
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun startBluetoothScan() {
|
||||||
|
bluetoothService.startDiscovery()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopBluetoothScan() {
|
||||||
|
bluetoothService.stopDiscovery()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
const val MAIN_SETTINGS_VIEW = 0
|
||||||
|
const val CONNECT_CONTROLLER_VIEW = 1
|
||||||
|
|
||||||
fun provideFactory(
|
fun provideFactory(
|
||||||
deviceService: DeviceService,
|
deviceService: DeviceService,
|
||||||
bluetoothService: BluetoothService,
|
controllerService: ControllerService,
|
||||||
|
bluetoothService: BluetoothService
|
||||||
) = object : ViewModelProvider.Factory {
|
) = object : ViewModelProvider.Factory {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
return SettingsViewModel(deviceService, bluetoothService) as T
|
return SettingsViewModel(deviceService, controllerService, bluetoothService) as T
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,27 @@
|
|||||||
|
package com.example.tvcontroller.data
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
|
||||||
|
class BluetoothDevice(name: String?, address: String?) {
|
||||||
|
private var _name: String? = name
|
||||||
|
private var _address: String? = address
|
||||||
|
|
||||||
|
fun getName(): String {
|
||||||
|
return if (_name.isNullOrEmpty()) getAddress() else _name!!
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAddress(): String {
|
||||||
|
return if (_address == null) "" else _address!!
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromBluetoothDevice(device: android.bluetooth.BluetoothDevice): BluetoothDevice {
|
||||||
|
try {
|
||||||
|
return BluetoothDevice(device.name, device.address)
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
Log.e("BluetoothDevice", "Error creating BluetoothDevice", e)
|
||||||
|
}
|
||||||
|
return BluetoothDevice(null, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,17 +6,53 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
import androidx.core.content.ContextCompat.getSystemService
|
import androidx.core.content.ContextCompat.getSystemService
|
||||||
|
import com.example.tvcontroller.data.BluetoothDevice
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
|
||||||
class BluetoothService(private val context: Context) {
|
class BluetoothService(private val context: Context) {
|
||||||
private var bluetoothManager: BluetoothManager =
|
private var bluetoothManager: BluetoothManager =
|
||||||
getSystemService(context, BluetoothManager::class.java)!!;
|
getSystemService(context, BluetoothManager::class.java)!!;
|
||||||
private var bluetoothAdapter: BluetoothAdapter = bluetoothManager.adapter
|
private var bluetoothAdapter: BluetoothAdapter = bluetoothManager.adapter
|
||||||
private var bluetoothStateReceiver: BroadcastReceiver? = null
|
|
||||||
private var bluetoothStateChangedCallbacks: MutableList<(Int) -> Unit> = mutableListOf()
|
private var bluetoothStateChangedCallbacks: MutableList<(Int) -> Unit> = mutableListOf()
|
||||||
|
private val _scannedDevices = MutableStateFlow<List<BluetoothDevice>>(emptyList())
|
||||||
|
var scannedDevices: StateFlow<List<BluetoothDevice>> = _scannedDevices.asStateFlow()
|
||||||
|
private val _pairedDevices = MutableStateFlow<List<BluetoothDevice>>(emptyList())
|
||||||
|
var pairedDevices: StateFlow<List<BluetoothDevice>> = _pairedDevices.asStateFlow()
|
||||||
|
|
||||||
|
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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private var foundDeviceReceiver: BroadcastReceiver? = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
val device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
intent.getParcelableExtra(
|
||||||
|
android.bluetooth.BluetoothDevice.EXTRA_DEVICE,
|
||||||
|
android.bluetooth.BluetoothDevice::class.java
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
intent.getParcelableExtra(android.bluetooth.BluetoothDevice.EXTRA_DEVICE)
|
||||||
|
}
|
||||||
|
if (device != null) {
|
||||||
|
val newDevice = BluetoothDevice.fromBluetoothDevice(device)
|
||||||
|
Log.i("BluetoothService", "Found device: $newDevice")
|
||||||
|
_scannedDevices.update { devices -> if (newDevice in devices) devices else devices + newDevice }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
registerBluetoothStateReceiver()
|
registerBluetoothStateReceiver()
|
||||||
|
updatePairedDevices()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isBluetoothEnabled(): Boolean {
|
fun isBluetoothEnabled(): Boolean {
|
||||||
@ -24,32 +60,9 @@ class BluetoothService(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun registerBluetoothStateReceiver() {
|
private fun registerBluetoothStateReceiver() {
|
||||||
bluetoothStateReceiver = object : BroadcastReceiver() {
|
context.registerReceiver(
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
bluetoothStateReceiver, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)
|
||||||
val state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)
|
)
|
||||||
bluetoothStateChangedCallbacks.forEach { it(state) }
|
|
||||||
when (state) {
|
|
||||||
BluetoothAdapter.STATE_ON -> {
|
|
||||||
// Handle Bluetooth turned on
|
|
||||||
println("SIGNAL: Bluetooth is enabled")
|
|
||||||
}
|
|
||||||
BluetoothAdapter.STATE_OFF -> {
|
|
||||||
// Handle Bluetooth turned off
|
|
||||||
println("SIGNAL: Bluetooth is disabled")
|
|
||||||
}
|
|
||||||
BluetoothAdapter.STATE_TURNING_ON -> {
|
|
||||||
// Handle Bluetooth turning on
|
|
||||||
println("SIGNAL: Bluetooth is turning on")
|
|
||||||
}
|
|
||||||
BluetoothAdapter.STATE_TURNING_OFF -> {
|
|
||||||
// Handle Bluetooth turning off
|
|
||||||
println("SIGNAL: Bluetooth is turning off")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
context.registerReceiver(bluetoothStateReceiver, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onBluetoothStateChanged(callback: (Int) -> Unit) {
|
fun onBluetoothStateChanged(callback: (Int) -> Unit) {
|
||||||
@ -62,5 +75,69 @@ class BluetoothService(private val context: Context) {
|
|||||||
|
|
||||||
fun cleanUp() {
|
fun cleanUp() {
|
||||||
context.unregisterReceiver(bluetoothStateReceiver)
|
context.unregisterReceiver(bluetoothStateReceiver)
|
||||||
|
context.unregisterReceiver(foundDeviceReceiver)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updatePairedDevices() {
|
||||||
|
try {
|
||||||
|
_pairedDevices.update {
|
||||||
|
bluetoothAdapter.bondedDevices.toList()
|
||||||
|
.map { device -> BluetoothDevice.fromBluetoothDevice(device) }
|
||||||
|
}
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
println("Error updating paired devices: $e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startDiscovery() {
|
||||||
|
try {
|
||||||
|
if (bluetoothAdapter.isDiscovering) return
|
||||||
|
|
||||||
|
Log.i("BluetoothService", "Starting discovery")
|
||||||
|
context.registerReceiver(
|
||||||
|
foundDeviceReceiver,
|
||||||
|
IntentFilter(android.bluetooth.BluetoothDevice.ACTION_FOUND)
|
||||||
|
)
|
||||||
|
updatePairedDevices()
|
||||||
|
bluetoothAdapter.startDiscovery()
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
println("Error starting discovery: $e")
|
||||||
|
Log.e("BluetoothService", "Error starting discovery: $e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopDiscovery() {
|
||||||
|
context.unregisterReceiver(foundDeviceReceiver)
|
||||||
|
try {
|
||||||
|
bluetoothAdapter.cancelDiscovery()
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
println("Error stopping discovery: $e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasRequiredPermissions(): Boolean {
|
||||||
|
return BLUETOOTH_PERMISSIONS.all {
|
||||||
|
context.checkSelfPermission(it) == PackageManager.PERMISSION_GRANTED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getBluetoothState(): Int {
|
||||||
|
return bluetoothAdapter.state
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val BLUETOOTH_PERMISSIONS = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
arrayOf(
|
||||||
|
android.Manifest.permission.BLUETOOTH,
|
||||||
|
android.Manifest.permission.BLUETOOTH_CONNECT,
|
||||||
|
android.Manifest.permission.BLUETOOTH_SCAN,
|
||||||
|
android.Manifest.permission.BLUETOOTH_ADMIN
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
arrayOf(
|
||||||
|
android.Manifest.permission.BLUETOOTH,
|
||||||
|
android.Manifest.permission.BLUETOOTH_ADMIN
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
package com.example.tvcontroller.services
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothAdapter
|
||||||
|
|
||||||
|
class ControllerService(bluetoothService: BluetoothService) {
|
||||||
|
private var connectionStateChangedListener: MutableList<(Int) -> Unit> = mutableListOf()
|
||||||
|
|
||||||
|
var connectionState = BLUETOOTH_DISABLED
|
||||||
|
private set
|
||||||
|
|
||||||
|
init {
|
||||||
|
handleBluetoothStateChanged(bluetoothService.getBluetoothState())
|
||||||
|
bluetoothService.onBluetoothStateChanged {
|
||||||
|
handleBluetoothStateChanged(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onConnectionStateChanged(callback: (Int) -> Unit) {
|
||||||
|
connectionStateChangedListener.add(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setConnectionState(state: Int) {
|
||||||
|
connectionState = state
|
||||||
|
connectionStateChangedListener.forEach { it(state) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleBluetoothStateChanged(state: Int) {
|
||||||
|
when (state) {
|
||||||
|
BluetoothAdapter.STATE_OFF -> setConnectionState(BLUETOOTH_DISABLED)
|
||||||
|
else -> setConnectionState(NOT_PAIRED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val BLUETOOTH_DISABLED = 0
|
||||||
|
const val NOT_PAIRED = 1
|
||||||
|
const val NOT_CONNECTED = 2
|
||||||
|
const val CONNECTED = 3
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,10 +1,15 @@
|
|||||||
package com.example.tvcontroller.ui.views
|
package com.example.tvcontroller.ui.views
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.util.Log
|
||||||
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.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@ -12,6 +17,7 @@ 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.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@ -20,77 +26,124 @@ import com.example.tvcontroller.R
|
|||||||
import com.example.tvcontroller.Settings
|
import com.example.tvcontroller.Settings
|
||||||
import com.example.tvcontroller.SettingsViewModel
|
import com.example.tvcontroller.SettingsViewModel
|
||||||
import com.example.tvcontroller.services.BluetoothService
|
import com.example.tvcontroller.services.BluetoothService
|
||||||
|
import com.example.tvcontroller.services.ControllerService
|
||||||
import com.example.tvcontroller.services.DeviceService
|
import com.example.tvcontroller.services.DeviceService
|
||||||
|
import kotlinx.coroutines.flow.toList
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsView(
|
fun SettingsView(
|
||||||
deviceService: DeviceService,
|
deviceService: DeviceService,
|
||||||
|
controllerService: ControllerService,
|
||||||
bluetoothService: BluetoothService
|
bluetoothService: BluetoothService
|
||||||
) {
|
) {
|
||||||
val viewModel =
|
val viewModel = viewModel<SettingsViewModel>(
|
||||||
viewModel<SettingsViewModel>(factory = SettingsViewModel.provideFactory(deviceService, bluetoothService))
|
factory = SettingsViewModel.provideFactory(
|
||||||
|
deviceService, controllerService, bluetoothService
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
Column(
|
@Composable
|
||||||
modifier = Modifier
|
fun MainSettingsView() {
|
||||||
.padding(16.dp, 16.dp)
|
Column(
|
||||||
.verticalScroll(rememberScrollState()),
|
modifier = Modifier
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
.padding(16.dp, 16.dp)
|
||||||
) {
|
.verticalScroll(rememberScrollState()),
|
||||||
Text(
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
text = stringResource(id = R.string.settings_title),
|
) {
|
||||||
style = MaterialTheme.typography.displaySmall
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.padding(8.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(id = R.string.server_settings_heading),
|
|
||||||
style = MaterialTheme.typography.headlineSmall
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = stringResource(id = R.string.server_connection_label) + ": " + getConnectionStateString(
|
|
||||||
viewModel.connectionState
|
|
||||||
),
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
OutlinedTextField(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
value = viewModel.serverAddress,
|
|
||||||
onValueChange = viewModel::onServerAddressChanged,
|
|
||||||
label = { Text(stringResource(id = R.string.server_address_label)) }
|
|
||||||
)
|
|
||||||
OutlinedTextField(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
value = viewModel.deviceName,
|
|
||||||
onValueChange = viewModel::onDeviceNameChanged,
|
|
||||||
label = { Text(stringResource(id = R.string.device_name_label)) }
|
|
||||||
)
|
|
||||||
OutlinedTextField(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
value = viewModel.registrationCode,
|
|
||||||
onValueChange = viewModel::onRegistrationCodeChanged,
|
|
||||||
label = { Text(stringResource(id = R.string.registration_code_label)) }
|
|
||||||
)
|
|
||||||
OutlinedButton(onClick = { viewModel.connect() }, modifier = Modifier.fillMaxWidth()) {
|
|
||||||
Text(
|
Text(
|
||||||
stringResource(id = R.string.connect_button_label)
|
text = stringResource(id = R.string.settings_title),
|
||||||
|
style = MaterialTheme.typography.displaySmall
|
||||||
)
|
)
|
||||||
}
|
Spacer(modifier = Modifier.padding(8.dp))
|
||||||
Spacer(modifier = Modifier.padding(8.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(id = R.string.controller_settings_heading),
|
|
||||||
style = MaterialTheme.typography.headlineSmall
|
|
||||||
)
|
|
||||||
if (viewModel.bluetoothEnabled) {
|
|
||||||
Text(
|
Text(
|
||||||
text = "Controller settings: Bluetooth is enabled.",
|
text = stringResource(id = R.string.server_settings_heading),
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.headlineSmall
|
||||||
)
|
)
|
||||||
} else {
|
Text(
|
||||||
Text(
|
text = stringResource(id = R.string.server_connection_label) + ": " + getConnectionStateString(
|
||||||
text = "Bluetooth is disabled. Please enable it in settings.",
|
viewModel.connectionState
|
||||||
|
), style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = viewModel.serverAddress,
|
||||||
|
onValueChange = viewModel::onServerAddressChanged,
|
||||||
|
label = { Text(stringResource(id = R.string.server_address_label)) })
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = viewModel.deviceName,
|
||||||
|
onValueChange = viewModel::onDeviceNameChanged,
|
||||||
|
label = { Text(stringResource(id = R.string.device_name_label)) })
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = viewModel.registrationCode,
|
||||||
|
onValueChange = viewModel::onRegistrationCodeChanged,
|
||||||
|
label = { Text(stringResource(id = R.string.registration_code_label)) })
|
||||||
|
OutlinedButton(onClick = { viewModel.connect() }, modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Text(
|
||||||
|
stringResource(id = R.string.connect_button_label)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.padding(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.controller_settings_heading),
|
||||||
|
style = MaterialTheme.typography.headlineSmall
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Controller status: " + getControllerConnectionStateString(viewModel.controllerConnectionState) + ".",
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium
|
||||||
)
|
)
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = { viewModel.activeView = SettingsViewModel.CONNECT_CONTROLLER_VIEW },
|
||||||
|
enabled = viewModel.controllerConnectionState != ControllerService.BLUETOOTH_DISABLED && viewModel.controllerConnectionState != ControllerService.CONNECTED,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(id = R.string.connect_button_label)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
@Composable
|
||||||
|
fun ConnectControllerView() {
|
||||||
|
viewModel.startBluetoothScan()
|
||||||
|
val pairedDevices = viewModel.pairedDevices.collectAsState()
|
||||||
|
val scannedDevices = viewModel.scannedDevices.collectAsState()
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(16.dp, 16.dp)
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.connect_controller_title),
|
||||||
|
style = MaterialTheme.typography.displaySmall
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.padding(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.paired_devices_label),
|
||||||
|
style = MaterialTheme.typography.headlineSmall
|
||||||
|
)
|
||||||
|
pairedDevices.value.forEach { device ->
|
||||||
|
Text(text = device.getName())
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.padding(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.scanned_devices_label),
|
||||||
|
style = MaterialTheme.typography.headlineSmall
|
||||||
|
)
|
||||||
|
scannedDevices.value.forEach { device ->
|
||||||
|
Text(text = device.getName())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
when (viewModel.activeView) {
|
||||||
|
SettingsViewModel.MAIN_SETTINGS_VIEW -> MainSettingsView()
|
||||||
|
SettingsViewModel.CONNECT_CONTROLLER_VIEW -> ConnectControllerView()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -100,3 +153,14 @@ fun getConnectionStateString(state: Settings.ConnectionState): String {
|
|||||||
Settings.ConnectionState.Registered -> stringResource(id = R.string.connection_state_registered)
|
Settings.ConnectionState.Registered -> stringResource(id = R.string.connection_state_registered)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun getControllerConnectionStateString(state: Int): 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)
|
||||||
|
else -> "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -13,4 +13,11 @@
|
|||||||
<string name="server_address_label">Server address</string>
|
<string name="server_address_label">Server address</string>
|
||||||
<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_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>
|
||||||
</resources>
|
</resources>
|
||||||
Loading…
Reference in New Issue
Block a user