feat: implement serial bt connection to device
This commit is contained in:
parent
07c3ef066b
commit
afc3378828
@ -1,6 +1,5 @@
|
||||
package com.example.tvcontroller
|
||||
|
||||
import android.content.ContentValues.TAG
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.activity.ComponentActivity
|
||||
@ -36,6 +35,8 @@ import com.example.tvcontroller.ui.views.CameraView
|
||||
import com.example.tvcontroller.ui.views.SettingsView
|
||||
|
||||
|
||||
const val TAG = "MainActivity"
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private lateinit var bluetoothService: BluetoothService
|
||||
private lateinit var deviceService: DeviceService
|
||||
@ -62,10 +63,12 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
|
||||
private fun checkPermissions() {
|
||||
Log.i(TAG, "Checking permissions")
|
||||
if (!cameraService.hasRequiredPermissions()) {
|
||||
ActivityCompat.requestPermissions(this, CameraService.CAMERAX_PERMISSIONS, 0)
|
||||
}
|
||||
if (!bluetoothService.hasRequiredPermissions()) {
|
||||
Log.i(TAG, "Requesting Bluetooth permissions")
|
||||
ActivityCompat.requestPermissions(this, BluetoothService.BLUETOOTH_PERMISSIONS, 0)
|
||||
}
|
||||
}
|
||||
@ -122,7 +125,7 @@ fun TvControllerApp(
|
||||
}) { innerPadding ->
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = Screen.Camera.name,
|
||||
startDestination = Screen.Settings.name,
|
||||
modifier = Modifier.padding(innerPadding)
|
||||
) {
|
||||
composable(route = Screen.Camera.name) {
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
package com.example.tvcontroller
|
||||
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@ -8,10 +7,11 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.example.tvcontroller.data.BluetoothDevice
|
||||
import com.example.tvcontroller.services.BluetoothService
|
||||
import com.example.tvcontroller.services.ControllerService
|
||||
import com.example.tvcontroller.services.DeviceService
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class SettingsViewModel(
|
||||
@ -29,9 +29,7 @@ class SettingsViewModel(
|
||||
private set
|
||||
var controllerConnectionState by mutableIntStateOf(controllerService.connectionState)
|
||||
private set
|
||||
var activeView by mutableStateOf(MAIN_SETTINGS_VIEW)
|
||||
|
||||
var scannedDevices = bluetoothService.scannedDevices
|
||||
var pairedDevices = bluetoothService.pairedDevices
|
||||
|
||||
init {
|
||||
@ -53,11 +51,6 @@ class SettingsViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun connectController() {
|
||||
//Log.i("SettingsScreen", "Connect controller")
|
||||
//controllerService.connect()
|
||||
}
|
||||
|
||||
private fun updateConnectionState() {
|
||||
connectionState = if (deviceService.getToken().isEmpty()) {
|
||||
Settings.ConnectionState.Unregistered
|
||||
@ -77,6 +70,12 @@ class SettingsViewModel(
|
||||
deviceName = integration.name
|
||||
}
|
||||
|
||||
fun connectBluetoothDevice(device: BluetoothDevice) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
bluetoothService.connectToDevice(device)
|
||||
}
|
||||
}
|
||||
|
||||
fun onServerAddressChanged(url: String) {
|
||||
serverAddress = url
|
||||
}
|
||||
@ -89,14 +88,6 @@ class SettingsViewModel(
|
||||
registrationCode = code
|
||||
}
|
||||
|
||||
fun startBluetoothScan() {
|
||||
bluetoothService.startDiscovery()
|
||||
}
|
||||
|
||||
fun stopBluetoothScan() {
|
||||
bluetoothService.stopDiscovery()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MAIN_SETTINGS_VIEW = 0
|
||||
const val CONNECT_CONTROLLER_VIEW = 1
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
package com.example.tvcontroller.services
|
||||
|
||||
import android.Manifest
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothManager
|
||||
import android.bluetooth.BluetoothSocket
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.BroadcastReceiver
|
||||
@ -15,54 +17,38 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.io.IOException
|
||||
import java.util.UUID
|
||||
|
||||
private const val SERIAL_PORT_SERVICE_UUID = "00001101-0000-1000-8000-00805F9B34FB"
|
||||
private const val TAG = "BluetoothService"
|
||||
|
||||
class BluetoothService(private val context: Context) {
|
||||
private var bluetoothManager: BluetoothManager =
|
||||
getSystemService(context, BluetoothManager::class.java)!!;
|
||||
getSystemService(context, BluetoothManager::class.java)!!
|
||||
private var bluetoothAdapter: BluetoothAdapter = bluetoothManager.adapter
|
||||
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 clientSocket: BluetoothSocket? = null
|
||||
|
||||
private var bluetoothStateChangedCallbacks: MutableList<(Int) -> 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) }
|
||||
}
|
||||
}
|
||||
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 {
|
||||
registerBluetoothStateReceiver()
|
||||
context.registerReceiver(
|
||||
bluetoothStateReceiver, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)
|
||||
)
|
||||
updatePairedDevices()
|
||||
}
|
||||
|
||||
fun isBluetoothEnabled(): Boolean {
|
||||
return bluetoothAdapter.isEnabled ?: false
|
||||
}
|
||||
|
||||
private fun registerBluetoothStateReceiver() {
|
||||
context.registerReceiver(
|
||||
bluetoothStateReceiver, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)
|
||||
)
|
||||
return bluetoothAdapter.isEnabled
|
||||
}
|
||||
|
||||
fun onBluetoothStateChanged(callback: (Int) -> Unit) {
|
||||
@ -75,7 +61,8 @@ class BluetoothService(private val context: Context) {
|
||||
|
||||
fun cleanUp() {
|
||||
context.unregisterReceiver(bluetoothStateReceiver)
|
||||
context.unregisterReceiver(foundDeviceReceiver)
|
||||
bluetoothStateChangedCallbacks.clear()
|
||||
closeBluetoothConnection()
|
||||
}
|
||||
|
||||
fun updatePairedDevices() {
|
||||
@ -89,29 +76,24 @@ class BluetoothService(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
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 connectToDevice(device: BluetoothDevice) {
|
||||
Log.i(TAG, "Initiating connection process")
|
||||
if (context.checkSelfPermission(Manifest.permission.BLUETOOTH_ADMIN) != PackageManager.PERMISSION_GRANTED) {
|
||||
Log.e(TAG, "Bluetooth permission not granted")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fun stopDiscovery() {
|
||||
context.unregisterReceiver(foundDeviceReceiver)
|
||||
Log.i(TAG, "Connecting to device: $device")
|
||||
try {
|
||||
bluetoothAdapter.cancelDiscovery()
|
||||
} catch (e: SecurityException) {
|
||||
println("Error stopping discovery: $e")
|
||||
var androidBluetoothDevice = bluetoothAdapter.getRemoteDevice(device.getAddress())
|
||||
Log.i(TAG, "Creating socket to device: $device")
|
||||
clientSocket = androidBluetoothDevice.createRfcommSocketToServiceRecord(
|
||||
UUID.fromString(SERIAL_PORT_SERVICE_UUID)
|
||||
)
|
||||
Log.i(TAG, "Connecting to socket")
|
||||
clientSocket?.connect()
|
||||
Log.i(TAG, "Connected to device: $device")
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Error connecting to device: $e")
|
||||
}
|
||||
}
|
||||
|
||||
@ -125,18 +107,22 @@ class BluetoothService(private val context: Context) {
|
||||
return bluetoothAdapter.state
|
||||
}
|
||||
|
||||
fun closeBluetoothConnection() {
|
||||
clientSocket?.close()
|
||||
clientSocket = null
|
||||
}
|
||||
|
||||
companion object {
|
||||
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(
|
||||
android.Manifest.permission.BLUETOOTH,
|
||||
android.Manifest.permission.BLUETOOTH_CONNECT,
|
||||
android.Manifest.permission.BLUETOOTH_SCAN,
|
||||
android.Manifest.permission.BLUETOOTH_ADMIN
|
||||
Manifest.permission.BLUETOOTH, Manifest.permission.BLUETOOTH_ADMIN
|
||||
)
|
||||
} else {
|
||||
arrayOf(
|
||||
android.Manifest.permission.BLUETOOTH,
|
||||
android.Manifest.permission.BLUETOOTH_ADMIN
|
||||
Manifest.permission.BLUETOOTH,
|
||||
Manifest.permission.BLUETOOTH_CONNECT,
|
||||
Manifest.permission.BLUETOOTH_SCAN,
|
||||
Manifest.permission.BLUETOOTH_ADMIN
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,14 +27,13 @@ class ControllerService(bluetoothService: BluetoothService) {
|
||||
private fun handleBluetoothStateChanged(state: Int) {
|
||||
when (state) {
|
||||
BluetoothAdapter.STATE_OFF -> setConnectionState(BLUETOOTH_DISABLED)
|
||||
else -> setConnectionState(NOT_PAIRED)
|
||||
else -> setConnectionState(NOT_CONNECTED)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val BLUETOOTH_DISABLED = 0
|
||||
const val NOT_PAIRED = 1
|
||||
const val NOT_CONNECTED = 2
|
||||
const val CONNECTED = 3
|
||||
const val NOT_CONNECTED = 1
|
||||
const val CONNECTED = 2
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
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
|
||||
@ -18,16 +19,24 @@ 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
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.example.tvcontroller.R
|
||||
import com.example.tvcontroller.Settings
|
||||
import com.example.tvcontroller.SettingsViewModel
|
||||
import com.example.tvcontroller.SettingsViewModel.Companion.CONNECT_CONTROLLER_VIEW
|
||||
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
|
||||
@ -45,6 +54,7 @@ fun SettingsView(
|
||||
deviceService, controllerService, bluetoothService
|
||||
)
|
||||
)
|
||||
val navController = rememberNavController()
|
||||
|
||||
@Composable
|
||||
fun MainSettingsView() {
|
||||
@ -98,7 +108,7 @@ fun SettingsView(
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
OutlinedButton(
|
||||
onClick = { viewModel.activeView = SettingsViewModel.CONNECT_CONTROLLER_VIEW },
|
||||
onClick = { navController.navigate(CONNECT_CONTROLLER_VIEW.toString()) },
|
||||
enabled = viewModel.controllerConnectionState != ControllerService.BLUETOOTH_DISABLED && viewModel.controllerConnectionState != ControllerService.CONNECTED,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
@ -109,17 +119,16 @@ fun SettingsView(
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
@Composable
|
||||
fun ConnectControllerView() {
|
||||
viewModel.startBluetoothScan()
|
||||
val pairedDevices = viewModel.pairedDevices.collectAsState()
|
||||
val scannedDevices = viewModel.scannedDevices.collectAsState()
|
||||
Column {
|
||||
TopAppBar(
|
||||
title = { Text(text = stringResource(id = R.string.connect_controller_title)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = {}) {
|
||||
IconButton(onClick = {
|
||||
navController.navigate(MAIN_SETTINGS_VIEW.toString())
|
||||
}) {
|
||||
Icon(
|
||||
painterResource(id = R.drawable.baseline_arrow_back_24),
|
||||
contentDescription = "Back"
|
||||
@ -143,32 +152,20 @@ fun SettingsView(
|
||||
device.getAddress()
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.padding(8.dp))
|
||||
Text(
|
||||
text = stringResource(id = R.string.scanned_devices_label),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
modifier = Modifier.padding(start = 16.dp)
|
||||
)
|
||||
scannedDevices.value.forEach { device ->
|
||||
if (device != null) ListItem(
|
||||
headlineContent = { Text(device.getName()) },
|
||||
supportingContent = {
|
||||
if (device.getName() != device.getAddress()) Text(
|
||||
device.getAddress()
|
||||
modifier = Modifier.clickable(onClick = {
|
||||
viewModel.connectBluetoothDevice(
|
||||
device
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clickable(onClick = {})
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (viewModel.activeView) {
|
||||
SettingsViewModel.MAIN_SETTINGS_VIEW -> MainSettingsView()
|
||||
SettingsViewModel.CONNECT_CONTROLLER_VIEW -> ConnectControllerView()
|
||||
NavHost(navController = navController, startDestination = MAIN_SETTINGS_VIEW.toString()) {
|
||||
composable(route = MAIN_SETTINGS_VIEW.toString()) { MainSettingsView() }
|
||||
composable(route = CONNECT_CONTROLLER_VIEW.toString()) { ConnectControllerView() }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user