Compare commits

...

2 Commits

6 changed files with 107 additions and 15 deletions

View File

@ -31,11 +31,13 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.tvcontroller.services.BluetoothService import com.example.tvcontroller.services.BluetoothService
import com.example.tvcontroller.services.DeviceService
import com.example.tvcontroller.ui.AppViewModel import com.example.tvcontroller.ui.AppViewModel
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private lateinit var bluetoothService: BluetoothService private lateinit var bluetoothService: BluetoothService
private lateinit var deviceService: DeviceService
private val appViewModel by viewModels<AppViewModel>() private val appViewModel by viewModels<AppViewModel>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -43,10 +45,15 @@ class MainActivity : ComponentActivity() {
bluetoothService = BluetoothService(this.applicationContext) bluetoothService = BluetoothService(this.applicationContext)
bluetoothService.onBluetoothStateChanged { state -> appViewModel.setBluetoothEnabled(state == BluetoothAdapter.STATE_ON) } bluetoothService.onBluetoothStateChanged { state -> appViewModel.setBluetoothEnabled(state == BluetoothAdapter.STATE_ON) }
appViewModel.setBluetoothEnabled(bluetoothService.isBluetoothEnabled()) appViewModel.setBluetoothEnabled(bluetoothService.isBluetoothEnabled())
deviceService = DeviceService(this.applicationContext)
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
TVControllerTheme { TVControllerTheme {
TvControllerApp(appViewModel = appViewModel, bluetoothService = bluetoothService) TvControllerApp(
appViewModel = appViewModel,
bluetoothService = bluetoothService,
deviceService = deviceService
)
} }
} }
} }
@ -56,7 +63,8 @@ class MainActivity : ComponentActivity() {
fun TvControllerApp( fun TvControllerApp(
navController: NavHostController = rememberNavController(), navController: NavHostController = rememberNavController(),
appViewModel: AppViewModel = viewModel(), appViewModel: AppViewModel = viewModel(),
bluetoothService: BluetoothService? = null bluetoothService: BluetoothService? = null,
deviceService: DeviceService
) { ) {
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)
@ -113,7 +121,7 @@ fun TvControllerApp(
RemoteScreen() RemoteScreen()
} }
composable(route = Screen.Settings.name) { composable(route = Screen.Settings.name) {
SettingsScreen(appViewModel = appViewModel) SettingsScreen(appViewModel = appViewModel, deviceService = deviceService)
} }
} }
} }

View File

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

View File

@ -16,11 +16,13 @@ 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
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.tvcontroller.services.DeviceService
import com.example.tvcontroller.ui.AppViewModel import com.example.tvcontroller.ui.AppViewModel
@Composable @Composable
fun SettingsScreen(appViewModel: AppViewModel = viewModel()) { fun SettingsScreen(deviceService: DeviceService, appViewModel: AppViewModel) {
val viewModel = viewModel<SettingsViewModel>() val viewModel =
viewModel<SettingsViewModel>(factory = SettingsViewModel.provideFactory(deviceService))
Column( Column(
modifier = Modifier modifier = Modifier
@ -38,7 +40,9 @@ fun SettingsScreen(appViewModel: AppViewModel = viewModel()) {
style = MaterialTheme.typography.headlineSmall style = MaterialTheme.typography.headlineSmall
) )
Text( Text(
text = stringResource(id = R.string.server_connection_label) + ": " + stringResource(id = R.string.connection_state_disconnected), text = stringResource(id = R.string.server_connection_label) + ": " + getConnectionStateString(
viewModel.connectionState
),
style = MaterialTheme.typography.bodyMedium style = MaterialTheme.typography.bodyMedium
) )
OutlinedTextField( OutlinedTextField(
@ -82,3 +86,11 @@ fun SettingsScreen(appViewModel: AppViewModel = viewModel()) {
} }
} }
} }
@Composable
fun getConnectionStateString(state: Settings.ConnectionState): String {
return when (state) {
Settings.ConnectionState.Unregistered -> stringResource(id = R.string.connection_state_unregistered)
Settings.ConnectionState.Registered -> stringResource(id = R.string.connection_state_registered)
}
}

View File

@ -4,23 +4,40 @@ import androidx.compose.runtime.getValue
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.viewModelScope import androidx.lifecycle.viewModelScope
import com.example.tvcontroller.services.DeviceService import com.example.tvcontroller.services.DeviceService
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class SettingsViewModel : ViewModel() { class SettingsViewModel(private val deviceService: DeviceService) : ViewModel() {
var serverAddress by mutableStateOf(DeviceService.getServerAddress()) var serverAddress by mutableStateOf(deviceService.getServerAddress())
private set private set
var deviceName by mutableStateOf(android.os.Build.MANUFACTURER + " " + android.os.Build.MODEL) var deviceName by mutableStateOf(android.os.Build.MANUFACTURER + " " + android.os.Build.MODEL)
private set private set
var registrationCode by mutableStateOf("") var registrationCode by mutableStateOf("")
private set private set
var connectionState by mutableStateOf(Settings.ConnectionState.Unregistered)
private set
init {
checkConnectionState()
}
fun connect() { fun connect() {
//Log.i("SettingsScreen", "Save settings: $serverUrl, $deviceName, $registrationCode") //Log.i("SettingsScreen", "Save settings: $serverUrl, $deviceName, $registrationCode")
viewModelScope.launch { viewModelScope.launch {
DeviceService.setServerAddress(serverAddress) deviceService.setServerAddress(serverAddress)
DeviceService.createIntegration(deviceName, registrationCode) deviceService.createIntegration(deviceName, registrationCode)
checkConnectionState()
}
}
private fun checkConnectionState() {
connectionState = if (deviceService.getToken().isEmpty()) {
Settings.ConnectionState.Unregistered
return
} else {
Settings.ConnectionState.Registered
} }
} }
@ -35,4 +52,15 @@ class SettingsViewModel : ViewModel() {
fun onRegistrationCodeChanged(code: String) { fun onRegistrationCodeChanged(code: String) {
registrationCode = code registrationCode = code
} }
companion object {
fun provideFactory(
deviceService: DeviceService,
) = object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return SettingsViewModel(deviceService) as T
}
}
}
} }

View File

@ -1,5 +1,7 @@
package com.example.tvcontroller.services package com.example.tvcontroller.services
import android.content.Context
import android.content.Context.MODE_PRIVATE
import android.util.Log import android.util.Log
import io.ktor.client.engine.cio.* import io.ktor.client.engine.cio.*
import io.ktor.client.* import io.ktor.client.*
@ -11,31 +13,59 @@ import io.ktor.client.statement.HttpResponse
import io.ktor.http.HttpMethod import io.ktor.http.HttpMethod
import org.json.JSONObject import org.json.JSONObject
object DeviceService { private const val SHARED_PREFERENCES_NAME = "devices";
class DeviceService(private val context: Context) {
private var client = HttpClient(CIO) private var client = HttpClient(CIO)
private var serverAddress: String = "" private var serverAddress: String = ""
private var token: String = ""
init {
loadPreferences()
}
suspend fun createIntegration(name: String, code: String) { suspend fun createIntegration(name: String, code: String) {
Log.i("DeviceService", "Creating integration for $name with code $code at $serverAddress") Log.i("DeviceService", "Creating integration for $name with code $code at $serverAddress")
val json = JSONObject() val requestJson = JSONObject()
json.put("name", name) requestJson.put("name", name)
json.put("code", code) requestJson.put("code", code)
try { try {
val response: HttpResponse = client.request("http://$serverAddress/api/integrations") { val response: HttpResponse = client.request("http://$serverAddress/api/integrations") {
method = HttpMethod.Post method = HttpMethod.Post
setBody(json.toString()) setBody(requestJson.toString())
headers { headers {
append("Content-Type", "application/json") append("Content-Type", "application/json")
} }
} }
val body: String = response.body() val body: String = response.body()
val responseJson = JSONObject(body)
token = responseJson.getString("token")
savePreferences()
Log.i("DeviceService", "Response: ${response.status.value} $body") Log.i("DeviceService", "Response: ${response.status.value} $body")
} catch (e: Exception) { } catch (e: Exception) {
Log.e("DeviceService", "Error creating integration", e) Log.e("DeviceService", "Error creating integration", e)
} }
} }
private fun loadPreferences() {
val sharedPreferences = context.getSharedPreferences(SHARED_PREFERENCES_NAME, MODE_PRIVATE)
serverAddress = sharedPreferences.getString("server_address", "")!!
token = sharedPreferences.getString("token", "")!!
Log.i("DeviceService", "Loaded preferences: $serverAddress $token")
}
private fun savePreferences() {
val sharedPreferences = context.getSharedPreferences(SHARED_PREFERENCES_NAME, MODE_PRIVATE)
val editor = sharedPreferences.edit()
editor.apply {
putString("server_address", serverAddress)
putString("token", token)
apply()
}
}
fun setServerAddress(url: String) { fun setServerAddress(url: String) {
serverAddress = url serverAddress = url
} }
@ -43,4 +73,8 @@ object DeviceService {
fun getServerAddress(): String { fun getServerAddress(): String {
return serverAddress return serverAddress
} }
fun getToken(): String {
return token
}
} }

View File

@ -11,4 +11,6 @@
<string name="save_button_label">Save</string> <string name="save_button_label">Save</string>
<string name="connect_button_label">Connect</string> <string name="connect_button_label">Connect</string>
<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_registered">registered</string>
</resources> </resources>