feat: add proper connection and status handling

This commit is contained in:
Fritz Heiden 2025-04-06 15:17:22 +02:00
parent 0b5c2a303c
commit c6f3a0b0f9
11 changed files with 196 additions and 102 deletions

View File

@ -4,7 +4,7 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-03-18T16:06:30.698647383Z"> <DropdownSelection timestamp="2025-04-05T11:04:12.656433726Z">
<Target type="DEFAULT_BOOT"> <Target type="DEFAULT_BOOT">
<handle> <handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=d3e11beb" /> <DeviceId pluginId="PhysicalDevice" identifier="serial=d3e11beb" />

View File

@ -26,6 +26,7 @@ import androidx.navigation.compose.rememberNavController
import com.example.tvcontroller.client.WebClient import com.example.tvcontroller.client.WebClient
import com.example.tvcontroller.client.WebsocketClient import com.example.tvcontroller.client.WebsocketClient
import com.example.tvcontroller.services.BluetoothService import com.example.tvcontroller.services.BluetoothService
import com.example.tvcontroller.services.CameraService
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 com.example.tvcontroller.services.webrtc.WebRtcService import com.example.tvcontroller.services.webrtc.WebRtcService
@ -41,9 +42,16 @@ const val TAG = "MainActivity"
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private val webClient by lazy { WebClient() } private val webClient by lazy { WebClient() }
private val websocketClient by lazy { WebsocketClient(webClient.client) } private val websocketClient by lazy { WebsocketClient(webClient.client) }
private val webRtcService by lazy { WebRtcService(applicationContext, websocketClient) } private val cameraService by lazy { CameraService(applicationContext) }
private val webRtcService by lazy {
WebRtcService(
applicationContext,
websocketClient,
cameraService
)
}
private val bluetoothService by lazy { BluetoothService(applicationContext) } private val bluetoothService by lazy { BluetoothService(applicationContext) }
private val deviceService by lazy { DeviceService(applicationContext, webClient) } private val deviceService by lazy { DeviceService(applicationContext, webClient, websocketClient) }
private val controllerService by lazy { ControllerService(bluetoothService) } private val controllerService by lazy { ControllerService(bluetoothService) }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -51,12 +59,20 @@ class MainActivity : ComponentActivity() {
checkPermissions() checkPermissions()
lifecycleScope.launch { lifecycleScope.launch {
deviceService.initialize() deviceService.onStatusChanged {
if (deviceService.serverAddress.isEmpty() || deviceService.token.isEmpty()) return@launch when (it) {
lifecycleScope.launch(Dispatchers.IO) { DeviceService.STATUS_REGISTERED -> {
websocketClient.connect(deviceService.serverAddress, deviceService.token) lifecycleScope.launch(Dispatchers.IO) {
websocketClient.connect(deviceService.serverAddress, deviceService.token)
}
webRtcService.connect()
}
DeviceService.STATUS_UNREGISTERED -> {
Log.i(TAG, "Device unregistered")
}
}
} }
webRtcService.connect() deviceService.initialize()
} }
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
@ -65,7 +81,8 @@ class MainActivity : ComponentActivity() {
deviceService = deviceService, deviceService = deviceService,
controllerService = controllerService, controllerService = controllerService,
bluetoothService = bluetoothService, bluetoothService = bluetoothService,
webRtcService = webRtcService webRtcService = webRtcService,
cameraService = cameraService
) )
} }
} }
@ -77,6 +94,10 @@ class MainActivity : ComponentActivity() {
Log.i(TAG, "Requesting Bluetooth permissions") Log.i(TAG, "Requesting Bluetooth permissions")
ActivityCompat.requestPermissions(this, BluetoothService.BLUETOOTH_PERMISSIONS, 0) ActivityCompat.requestPermissions(this, BluetoothService.BLUETOOTH_PERMISSIONS, 0)
} }
if (!cameraService.hasRequiredPermissions()) {
Log.i(TAG, "Requesting Camera permissions")
ActivityCompat.requestPermissions(this, CameraService.CAMERA_PERMISSIONS, 0)
}
} }
} }
@ -86,7 +107,8 @@ fun TvControllerApp(
deviceService: DeviceService, deviceService: DeviceService,
controllerService: ControllerService, controllerService: ControllerService,
bluetoothService: BluetoothService, bluetoothService: BluetoothService,
webRtcService: WebRtcService webRtcService: WebRtcService,
cameraService: CameraService
) { ) {
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)
@ -125,7 +147,7 @@ fun TvControllerApp(
) { ) {
composable(route = Screen.Camera.name) { composable(route = Screen.Camera.name) {
CameraView( CameraView(
eglBaseContext = webRtcService.eglBaseContext, eglBaseContext = cameraService.eglBaseContext,
videoTrack = webRtcService.videoTrack videoTrack = webRtcService.videoTrack
) )
} }

View File

@ -1,8 +0,0 @@
package com.example.tvcontroller
object Settings {
enum class ConnectionState {
Unregistered,
Registered,
}
}

View File

@ -15,11 +15,14 @@ private const val TAG = "WebsocketClient"
class WebsocketClient(private val client: HttpClient) { class WebsocketClient(private val client: HttpClient) {
private var websocket: DefaultWebSocketSession? = null private var websocket: DefaultWebSocketSession? = null
private val dataHandlers = mutableListOf<(String) -> Unit>() private val dataHandlers = mutableListOf<(String) -> Unit>()
private val connectedHandlers = mutableListOf<() -> Unit>()
private val disconnectedHandlers = mutableListOf<() -> Unit>()
suspend fun connect(serverAddress: String, token: String) { suspend fun connect(serverAddress: String, token: String) {
Log.i(TAG, "Connecting to websocket at $serverAddress") Log.i(TAG, "Connecting to websocket at $serverAddress")
val (host, port) = serverAddress.split(":") val (host, port) = serverAddress.split(":")
val portInt = if (port.isEmpty()) 80 else port.toInt() val portInt = if (port.isEmpty()) 80 else port.toInt()
connectedHandlers.forEach { it() }
client.webSocket( client.webSocket(
method = HttpMethod.Get, method = HttpMethod.Get,
host = host, host = host,
@ -40,6 +43,16 @@ class WebsocketClient(private val client: HttpClient) {
} }
} }
} }
Log.i(TAG, "Websocket connection closed")
disconnectedHandlers.forEach { it() }
}
fun onConnected(handler: () -> Unit) {
connectedHandlers.add(handler)
}
fun onDisconnected(handler: () -> Unit) {
disconnectedHandlers.add(handler)
} }
fun onData(handler: (String) -> Unit) { fun onData(handler: (String) -> Unit) {

View File

@ -2,16 +2,54 @@ package com.example.tvcontroller.services
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraManager
import org.webrtc.Camera2Capturer
import org.webrtc.DefaultVideoDecoderFactory
import org.webrtc.EglBase
import org.webrtc.HardwareVideoEncoderFactory
import org.webrtc.SimulcastVideoEncoderFactory
import org.webrtc.SoftwareVideoEncoderFactory
class CameraService(private val context: Context) { class CameraService(private val context: Context) {
var eglBaseContext: EglBase.Context = EglBase.create().eglBaseContext
val cameraManager by lazy { context.getSystemService(Context.CAMERA_SERVICE) as CameraManager }
val videoDecoderFactory by lazy { DefaultVideoDecoderFactory(eglBaseContext) }
val videoEncoderFactory by lazy {
SimulcastVideoEncoderFactory(
HardwareVideoEncoderFactory(eglBaseContext, true, true),
SoftwareVideoEncoderFactory()
)
}
fun createCameraCapturer(): Camera2Capturer {
val ids = cameraManager.cameraIdList
var foundCamera = false;
var cameraId = ""
for (id in ids) {
val characteristics = cameraManager.getCameraCharacteristics(id)
val facing = characteristics.get(CameraCharacteristics.LENS_FACING)
if (facing == CameraCharacteristics.LENS_FACING_BACK) {
cameraId = id
foundCamera = true
break
}
}
if (!foundCamera) {
cameraId = ids.first()
}
val cameraCapturer = Camera2Capturer(context, cameraId, null)
return cameraCapturer
}
fun hasRequiredPermissions(): Boolean { fun hasRequiredPermissions(): Boolean {
return CAMERAX_PERMISSIONS.all { return CAMERA_PERMISSIONS.all {
context.checkSelfPermission(it) == PackageManager.PERMISSION_GRANTED context.checkSelfPermission(it) == PackageManager.PERMISSION_GRANTED
} }
} }
companion object { companion object {
val CAMERAX_PERMISSIONS = arrayOf( val CAMERA_PERMISSIONS = arrayOf(
android.Manifest.permission.CAMERA, android.Manifest.permission.CAMERA,
android.Manifest.permission.RECORD_AUDIO android.Manifest.permission.RECORD_AUDIO
) )

View File

@ -3,28 +3,62 @@ package com.example.tvcontroller.services
import android.content.Context import android.content.Context
import android.content.Context.MODE_PRIVATE import android.content.Context.MODE_PRIVATE
import android.util.Log import android.util.Log
import androidx.lifecycle.lifecycleScope
import com.example.tvcontroller.client.WebClient import com.example.tvcontroller.client.WebClient
import com.example.tvcontroller.client.WebsocketClient
import com.example.tvcontroller.data.Integration import com.example.tvcontroller.data.Integration
import io.ktor.client.call.body import io.ktor.client.call.body
import io.ktor.http.HttpMethod import io.ktor.http.HttpMethod
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.json.JSONObject import org.json.JSONObject
private const val SHARED_PREFERENCES_NAME = "devices";
private const val TAG = "DeviceService"
class DeviceService(private val context: Context, private val client: WebClient) { class DeviceService(
private val context: Context,
private val client: WebClient,
private val websocketClient: WebsocketClient
) {
var serverAddress: String = "" var serverAddress: String = ""
var token: String = "" var token: String = ""
private set(value) { var status: String = STATUS_UNREGISTERED
field = value private set(status) {
updateDefaultCookies() field = status
statusChangedListeners.forEach { it(status) }
} }
private var deviceId: String = "" private var deviceId: String = ""
private val statusChangedListeners = mutableListOf<(String) -> Unit>()
suspend fun initialize() { suspend fun initialize() {
websocketClient.onConnected { status = STATUS_CONNECTED }
websocketClient.onDisconnected { status = STATUS_REGISTERED }
onStatusChanged {
when(it) {
STATUS_UNREGISTERED -> {
token = ""
deviceId = ""
updateDefaultCookies()
savePreferences()
}
STATUS_REGISTERED -> {
Log.i(TAG, "Device registered with id $deviceId")
savePreferences()
connectWebsocket()
}
}
}
loadPreferences() loadPreferences()
if (token.isEmpty()) return if (token.isEmpty()) return
getIntegration() updateDefaultCookies()
val integration = getIntegration()
Log.i(TAG, "Integration: $integration")
status = if (integration != null) {
STATUS_REGISTERED
} else {
STATUS_UNREGISTERED
}
} }
private fun updateDefaultCookies() { private fun updateDefaultCookies() {
@ -34,14 +68,15 @@ class DeviceService(private val context: Context, private val client: WebClient)
} }
suspend fun registerIntegration(name: String, code: String) { suspend fun registerIntegration(name: String, code: String) {
Log.i(TAG, "Creating integration for $name with code $code at $serverAddress") Log.i(TAG, "Registering integration for $name with code $code at $serverAddress")
savePreferences()
val requestJson = JSONObject().apply { val requestJson = JSONObject().apply {
put("name", name) put("name", name)
put("code", code) put("code", code)
} }
val response = val response =
client.sendJsonRequest( client.sendJsonRequest(
"http://$serverAddress/api/integrations", "http://$serverAddress/api/integrations/register",
HttpMethod.Post, HttpMethod.Post,
requestJson requestJson
) ?: return ) ?: return
@ -49,12 +84,23 @@ class DeviceService(private val context: Context, private val client: WebClient)
val responseJson = JSONObject(response.body<String>()) val responseJson = JSONObject(response.body<String>())
if (response.status.value != 200) { if (response.status.value != 200) {
val error = responseJson.getString("error") val error = responseJson.getString("error")
Log.e(TAG, "Error getting integration: ${response.status.value} $error") Log.e(TAG, "Error registering integration: ${response.status.value} $error")
return return
} }
token = responseJson.getString("token") token = responseJson.getString("token")
deviceId = responseJson.getString("id") deviceId = responseJson.getString("id")
savePreferences() status = STATUS_REGISTERED
}
fun onStatusChanged(listener: (String) -> Unit) {
statusChangedListeners.add(listener)
}
@OptIn(DelicateCoroutinesApi::class)
fun connectWebsocket() {
GlobalScope.launch() {
websocketClient.connect(serverAddress, token)
}
} }
suspend fun getIntegration(): Integration? { suspend fun getIntegration(): Integration? {
@ -77,24 +123,33 @@ class DeviceService(private val context: Context, private val client: WebClient)
private fun loadPreferences() { private fun loadPreferences() {
val sharedPreferences = context.getSharedPreferences(SHARED_PREFERENCES_NAME, MODE_PRIVATE) val sharedPreferences = context.getSharedPreferences(SHARED_PREFERENCES_NAME, MODE_PRIVATE)
serverAddress = sharedPreferences.getString("server_address", "")!! serverAddress = sharedPreferences.getString(SERVER_ADDRESS_KEY, "")!!
token = sharedPreferences.getString("token", "")!! token = sharedPreferences.getString(TOKEN_KEY, "")!!
deviceId = sharedPreferences.getString("device_id", "")!! deviceId = sharedPreferences.getString(DEVICE_ID_KEY, "")!!
Log.i(TAG, "Loaded preferences: $serverAddress $token") Log.i(TAG, "Loaded preferences: $serverAddress $token")
} }
private fun savePreferences() { private fun savePreferences() {
Log.i(TAG, "Saving preferences")
val sharedPreferences = context.getSharedPreferences(SHARED_PREFERENCES_NAME, MODE_PRIVATE) val sharedPreferences = context.getSharedPreferences(SHARED_PREFERENCES_NAME, MODE_PRIVATE)
val editor = sharedPreferences.edit() val editor = sharedPreferences.edit()
editor.apply { editor.apply {
putString("server_address", serverAddress) putString(SERVER_ADDRESS_KEY, serverAddress)
putString("token", token) putString(TOKEN_KEY, token)
putString("device_id", deviceId) putString(DEVICE_ID_KEY, deviceId)
apply() apply()
} }
} }
companion object { companion object {
const val TYPE_SIGNALING = "signaling" const val STATUS_UNREGISTERED = "unregistered"
const val STATUS_REGISTERED = "registered"
const val STATUS_CONNECTED = "connected"
private const val SHARED_PREFERENCES_NAME = "devices";
private const val SERVER_ADDRESS_KEY = "server_address"
private const val TOKEN_KEY = "token"
private const val DEVICE_ID_KEY = "device_id"
private const val TAG = "DeviceService"
} }
} }

View File

@ -4,6 +4,7 @@ import android.content.Context
import android.hardware.camera2.CameraCharacteristics import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraManager import android.hardware.camera2.CameraManager
import android.util.Log import android.util.Log
import com.example.tvcontroller.services.CameraService
import org.webrtc.AudioTrack import org.webrtc.AudioTrack
import org.webrtc.Camera2Capturer import org.webrtc.Camera2Capturer
import org.webrtc.DataChannel import org.webrtc.DataChannel
@ -27,21 +28,13 @@ import org.webrtc.VideoTrack
private const val TAG = "RtcPeerConnection" private const val TAG = "RtcPeerConnection"
class RtcPeerConnection(private val context: Context) { class RtcPeerConnection(private val context: Context, private val cameraService: CameraService) {
private val peerConnectionFactory by lazy { initializeFactory() } private val peerConnectionFactory by lazy { initializeFactory() }
private var iceServers = ArrayList<PeerConnection.IceServer>() private var iceServers = ArrayList<PeerConnection.IceServer>()
var eglBaseContext: EglBase.Context = EglBase.create().eglBaseContext var eglBaseContext: EglBase.Context = cameraService.eglBaseContext
private val cameraManager by lazy { context.getSystemService(Context.CAMERA_SERVICE) as CameraManager } private val videoCapturer by lazy { cameraService.createCameraCapturer() }
private val videoCapturer by lazy { createCameraCapturer() }
private val surfaceTextureHelper by lazy { createSurfaceTextureHelper() } private val surfaceTextureHelper by lazy { createSurfaceTextureHelper() }
private val videoSource by lazy { createVideoSource() } private val videoSource by lazy { createVideoSource() }
private val videoDecoderFactory by lazy { DefaultVideoDecoderFactory(eglBaseContext) }
private val videoEncoderFactory by lazy {
SimulcastVideoEncoderFactory(
HardwareVideoEncoderFactory(eglBaseContext, true, true),
SoftwareVideoEncoderFactory()
)
}
private var peerConnection: PeerConnection? = null private var peerConnection: PeerConnection? = null
private val iceCandidateHandlers = ArrayList<((IceCandidate) -> Unit)>() private val iceCandidateHandlers = ArrayList<((IceCandidate) -> Unit)>()
@ -55,6 +48,7 @@ class RtcPeerConnection(private val context: Context) {
override fun onIceCandidate(iceCandidate: IceCandidate?) { override fun onIceCandidate(iceCandidate: IceCandidate?) {
iceCandidateHandlers.forEach { it(iceCandidate!!) } iceCandidateHandlers.forEach { it(iceCandidate!!) }
} }
override fun onIceCandidatesRemoved(p0: Array<out IceCandidate?>?) {} override fun onIceCandidatesRemoved(p0: Array<out IceCandidate?>?) {}
override fun onAddStream(p0: MediaStream?) {} override fun onAddStream(p0: MediaStream?) {}
override fun onRemoveStream(p0: MediaStream?) {} override fun onRemoveStream(p0: MediaStream?) {}
@ -144,32 +138,13 @@ class RtcPeerConnection(private val context: Context) {
val options = PeerConnectionFactory.Options() val options = PeerConnectionFactory.Options()
val peerConnectionFactory = val peerConnectionFactory =
PeerConnectionFactory.builder().setVideoDecoderFactory(videoDecoderFactory) PeerConnectionFactory.builder()
.setVideoEncoderFactory(videoEncoderFactory).setOptions(options) .setVideoDecoderFactory(cameraService.videoDecoderFactory)
.setVideoEncoderFactory(cameraService.videoEncoderFactory).setOptions(options)
.createPeerConnectionFactory() .createPeerConnectionFactory()
return peerConnectionFactory return peerConnectionFactory
} }
private fun createCameraCapturer(): Camera2Capturer {
val ids = cameraManager.cameraIdList
var foundCamera = false;
var cameraId = ""
for (id in ids) {
val characteristics = cameraManager.getCameraCharacteristics(id)
val facing = characteristics.get(CameraCharacteristics.LENS_FACING)
if (facing == CameraCharacteristics.LENS_FACING_BACK) {
cameraId = id
foundCamera = true
break
}
}
if (!foundCamera) {
cameraId = ids.first()
}
val cameraCapturer = Camera2Capturer(context, cameraId, null)
return cameraCapturer
}
private fun createSurfaceTextureHelper(): SurfaceTextureHelper { private fun createSurfaceTextureHelper(): SurfaceTextureHelper {
val surfaceTextureHelper = val surfaceTextureHelper =
SurfaceTextureHelper.create("SurfaceTextureHelperThread", eglBaseContext) SurfaceTextureHelper.create("SurfaceTextureHelperThread", eglBaseContext)

View File

@ -3,22 +3,21 @@ package com.example.tvcontroller.services.webrtc
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import com.example.tvcontroller.client.WebsocketClient import com.example.tvcontroller.client.WebsocketClient
import com.example.tvcontroller.services.DeviceService import com.example.tvcontroller.services.CameraService
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.json.JSONObject import org.json.JSONObject
import org.webrtc.EglBase
import org.webrtc.IceCandidate import org.webrtc.IceCandidate
import org.webrtc.MediaConstraints import org.webrtc.MediaConstraints
import org.webrtc.SessionDescription import org.webrtc.SessionDescription
private const val TAG = "WebRtcService" class WebRtcService(
private val context: Context,
class WebRtcService(private val context: Context, private val websocketClient: WebsocketClient) { private val websocketClient: WebsocketClient,
private val cameraService: CameraService
) {
private val rtcPeerConnection by lazy { createRtcPeerConnection() } private val rtcPeerConnection by lazy { createRtcPeerConnection() }
val videoTrack by lazy { rtcPeerConnection.createVideoTrack() } val videoTrack by lazy { rtcPeerConnection.createVideoTrack() }
val audioTrack by lazy { rtcPeerConnection.createAudioTrack() } val audioTrack by lazy { rtcPeerConnection.createAudioTrack() }
val eglBaseContext: EglBase.Context
get() = rtcPeerConnection.eglBaseContext
private var peerId: String = "" private var peerId: String = ""
fun connect() { fun connect() {
@ -29,7 +28,7 @@ class WebRtcService(private val context: Context, private val websocketClient: W
private fun createRtcPeerConnection(): RtcPeerConnection { private fun createRtcPeerConnection(): RtcPeerConnection {
val iceServers = arrayOf("stun:stun.l.google.com:19302") val iceServers = arrayOf("stun:stun.l.google.com:19302")
val webRtcService = this val webRtcService = this
val rtcPeerConnection = RtcPeerConnection(context).apply { val rtcPeerConnection = RtcPeerConnection(context, cameraService).apply {
setIceServers(iceServers) setIceServers(iceServers)
onIceCandidate(webRtcService::sendIceCandidate) onIceCandidate(webRtcService::sendIceCandidate)
initialize() initialize()
@ -89,7 +88,7 @@ class WebRtcService(private val context: Context, private val websocketClient: W
private fun sendIceCandidate(iceCandidate: IceCandidate) { private fun sendIceCandidate(iceCandidate: IceCandidate) {
val messageJson = JSONObject() val messageJson = JSONObject()
messageJson.put("type", DeviceService.Companion.TYPE_SIGNALING) messageJson.put("type", TYPE_SIGNALING)
messageJson.put("target", peerId) messageJson.put("target", peerId)
messageJson.put("message", JSONObject().apply { messageJson.put("message", JSONObject().apply {
put("candidate", JSONObject().apply { put("candidate", JSONObject().apply {
@ -108,7 +107,7 @@ class WebRtcService(private val context: Context, private val websocketClient: W
private fun sendAnswer(targetId: String, sdp: String) { private fun sendAnswer(targetId: String, sdp: String) {
val messageJson = JSONObject() val messageJson = JSONObject()
messageJson.put("type", DeviceService.Companion.TYPE_SIGNALING) messageJson.put("type", TYPE_SIGNALING)
messageJson.put("target", targetId) messageJson.put("target", targetId)
messageJson.put("message", JSONObject().apply { messageJson.put("message", JSONObject().apply {
put("sdp", sdp) put("sdp", sdp)
@ -120,4 +119,10 @@ class WebRtcService(private val context: Context, private val websocketClient: W
websocketClient.sendJson(messageJson) websocketClient.sendJson(messageJson)
} }
} }
companion object {
const val TYPE_SIGNALING = "signaling"
private const val TAG = "WebRtcService"
}
} }

View File

@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width 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.Button
import androidx.compose.material3.CircularProgressIndicator 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
@ -31,11 +30,10 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.example.tvcontroller.R import com.example.tvcontroller.R
import com.example.tvcontroller.Settings
import com.example.tvcontroller.ui.views.SettingsViewModel.Companion.CONNECT_CONTROLLER_VIEW
import com.example.tvcontroller.ui.views.SettingsViewModel.Companion.MAIN_SETTINGS_VIEW
import com.example.tvcontroller.services.BluetoothService import com.example.tvcontroller.services.BluetoothService
import com.example.tvcontroller.services.DeviceService import com.example.tvcontroller.services.DeviceService
import com.example.tvcontroller.ui.views.SettingsViewModel.Companion.CONNECT_CONTROLLER_VIEW
import com.example.tvcontroller.ui.views.SettingsViewModel.Companion.MAIN_SETTINGS_VIEW
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -178,10 +176,12 @@ fun SettingsView(
} }
@Composable @Composable
fun getConnectionStateString(state: Settings.ConnectionState): String { fun getConnectionStateString(state: String): String {
return when (state) { return when (state) {
Settings.ConnectionState.Unregistered -> stringResource(id = R.string.connection_state_unregistered) DeviceService.STATUS_UNREGISTERED -> stringResource(id = R.string.connection_state_unregistered)
Settings.ConnectionState.Registered -> stringResource(id = R.string.connection_state_registered) DeviceService.STATUS_REGISTERED -> stringResource(id = R.string.connection_state_registered)
DeviceService.STATUS_CONNECTED -> stringResource(id = R.string.connection_state_connected)
else -> "Unknown"
} }
} }

View File

@ -8,7 +8,6 @@ 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.Settings
import com.example.tvcontroller.data.BluetoothDevice import com.example.tvcontroller.data.BluetoothDevice
import com.example.tvcontroller.services.BluetoothService import com.example.tvcontroller.services.BluetoothService
import com.example.tvcontroller.services.DeviceService import com.example.tvcontroller.services.DeviceService
@ -27,7 +26,7 @@ class SettingsViewModel(
private set private set
var registrationCode by mutableStateOf("") var registrationCode by mutableStateOf("")
private set private set
var connectionState by mutableStateOf(Settings.ConnectionState.Unregistered) var connectionState by mutableStateOf(deviceService.status)
private set private set
var bluetoothConnectionState by mutableStateOf(bluetoothService.state) var bluetoothConnectionState by mutableStateOf(bluetoothService.state)
private set private set
@ -45,10 +44,10 @@ class SettingsViewModel(
currentBluetoothDevice = bluetoothService.currentDevice currentBluetoothDevice = bluetoothService.currentDevice
bluetoothConnectionState = it bluetoothConnectionState = it
} }
deviceService.onStatusChanged { connectionState = it }
} }
fun connect() { fun connect() {
//Log.i("SettingsScreen", "Save settings: $serverUrl, $deviceName, $registrationCode")
viewModelScope.launch { viewModelScope.launch {
deviceService.serverAddress = serverAddress deviceService.serverAddress = serverAddress
deviceService.registerIntegration(deviceName, registrationCode) deviceService.registerIntegration(deviceName, registrationCode)
@ -59,21 +58,15 @@ class SettingsViewModel(
private fun updateConnectionState() { private fun updateConnectionState() {
Log.i(TAG, "Device token: ${deviceService.token}") Log.i(TAG, "Device token: ${deviceService.token}")
connectionState = if (deviceService.token.isEmpty()) { connectionState = deviceService.status
Settings.ConnectionState.Unregistered
} else {
Settings.ConnectionState.Registered
}
} }
private suspend fun updateDeviceInfo() { private suspend fun updateDeviceInfo() {
if (connectionState == Settings.ConnectionState.Unregistered) return if (connectionState == DeviceService.STATUS_UNREGISTERED) return
val integration = deviceService.getIntegration() val integration = deviceService.getIntegration()
if (integration == null) { integration?.let {
connectionState = Settings.ConnectionState.Unregistered deviceName = it.name
return
} }
deviceName = integration.name
} }
fun connectBluetoothDevice(device: BluetoothDevice) { fun connectBluetoothDevice(device: BluetoothDevice) {

View File

@ -21,4 +21,5 @@
<string name="bluetooth_state_connected">connected</string> <string name="bluetooth_state_connected">connected</string>
<string name="bluetooth_state_disconnected">disconnected</string> <string name="bluetooth_state_disconnected">disconnected</string>
<string name="disconnect_button_label">Disconnect</string> <string name="disconnect_button_label">Disconnect</string>
<string name="connection_state_connected">connected</string>
</resources> </resources>