diff --git a/app/src/main/java/com/example/tvcontroller/MainActivity.kt b/app/src/main/java/com/example/tvcontroller/MainActivity.kt index 06ba3e0..ff3770a 100644 --- a/app/src/main/java/com/example/tvcontroller/MainActivity.kt +++ b/app/src/main/java/com/example/tvcontroller/MainActivity.kt @@ -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) { diff --git a/app/src/main/java/com/example/tvcontroller/SettingsViewModel.kt b/app/src/main/java/com/example/tvcontroller/SettingsViewModel.kt index e67f7a5..ff6d44b 100644 --- a/app/src/main/java/com/example/tvcontroller/SettingsViewModel.kt +++ b/app/src/main/java/com/example/tvcontroller/SettingsViewModel.kt @@ -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 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 094db04..12d6fa1 100644 --- a/app/src/main/java/com/example/tvcontroller/services/BluetoothService.kt +++ b/app/src/main/java/com/example/tvcontroller/services/BluetoothService.kt @@ -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>(emptyList()) - var scannedDevices: StateFlow> = _scannedDevices.asStateFlow() + private val _pairedDevices = MutableStateFlow>(emptyList()) var pairedDevices: StateFlow> = _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 ) } } 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 a3c45d1..accb8e6 100644 --- a/app/src/main/java/com/example/tvcontroller/services/ControllerService.kt +++ b/app/src/main/java/com/example/tvcontroller/services/ControllerService.kt @@ -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 } } \ No newline at end of file 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 6d9e589..177e4e2 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,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() } } }