feat: add remote ui to send commands via bt

This commit is contained in:
Fritz Heiden 2025-03-27 18:43:11 +01:00
parent 4f40490fee
commit cba125738d
31 changed files with 604 additions and 56 deletions

View File

@ -60,6 +60,7 @@ dependencies {
implementation(libs.androidx.camera.mlkit.vision) implementation(libs.androidx.camera.mlkit.vision)
implementation(libs.androidx.camera.extensions) implementation(libs.androidx.camera.extensions)
implementation(libs.material3) implementation(libs.material3)
implementation(libs.opencsv)
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.espresso.core)

View File

@ -5,11 +5,8 @@ import android.util.Log
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationBarItem
@ -24,7 +21,6 @@ import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.example.tvcontroller.ui.theme.TVControllerTheme import com.example.tvcontroller.ui.theme.TVControllerTheme
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.res.painterResource 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
@ -32,6 +28,7 @@ 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.ui.views.CameraView import com.example.tvcontroller.ui.views.CameraView
import com.example.tvcontroller.ui.views.RemoteView
import com.example.tvcontroller.ui.views.SettingsView import com.example.tvcontroller.ui.views.SettingsView
@ -48,7 +45,7 @@ class MainActivity : ComponentActivity() {
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) controllerService = ControllerService(this.applicationContext, bluetoothService)
checkPermissions() checkPermissions()
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
@ -125,19 +122,20 @@ fun TvControllerApp(
}) { innerPadding -> }) { innerPadding ->
NavHost( NavHost(
navController = navController, navController = navController,
startDestination = Screen.Settings.name, startDestination = Screen.Remote.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() RemoteView(
controllerService = controllerService
)
} }
composable(route = Screen.Settings.name) { composable(route = Screen.Settings.name) {
SettingsView( SettingsView(
deviceService = deviceService, deviceService = deviceService,
controllerService = controllerService,
bluetoothService = bluetoothService bluetoothService = bluetoothService
) )
} }
@ -145,17 +143,3 @@ fun TvControllerApp(
} }
} }
@Composable
fun RemoteScreen(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(text = "Remote Screen", modifier = modifier)
Button(onClick = { Log.i(TAG, "RemoteScreen: Button clicked") }) {
Text(text = "Button")
}
}
}

View File

@ -1,7 +1,6 @@
package com.example.tvcontroller package com.example.tvcontroller
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
@ -9,14 +8,12 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
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.ControllerService
import com.example.tvcontroller.services.DeviceService import com.example.tvcontroller.services.DeviceService
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
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())
@ -103,12 +100,11 @@ class SettingsViewModel(
fun provideFactory( fun provideFactory(
deviceService: DeviceService, deviceService: DeviceService,
controllerService: ControllerService,
bluetoothService: BluetoothService 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, controllerService, bluetoothService) as T return SettingsViewModel(deviceService, bluetoothService) as T
} }
} }
} }

View File

@ -0,0 +1,9 @@
package com.example.tvcontroller.data
class RemoteCommand {
var functionName: String? = null
var protocol: String? = null
var device: String? = null
var subdevice: String? = null
var function: String? = null
}

View File

@ -152,6 +152,8 @@ class BluetoothService(private val context: Context) {
} }
} catch (e: IOException) { } catch (e: IOException) {
Log.e(TAG, "Error reading from socket: $e") Log.e(TAG, "Error reading from socket: $e")
} catch (e: Exception) {
Log.e(TAG, "Error receiving data: $e")
} }
} }
} }

View File

@ -1,39 +1,102 @@
package com.example.tvcontroller.services package com.example.tvcontroller.services
import android.bluetooth.BluetoothAdapter import android.content.Context
import android.util.Log
import com.example.tvcontroller.R
import com.example.tvcontroller.data.RemoteCommand
import com.opencsv.CSVReader
import org.json.JSONObject
class ControllerService(bluetoothService: BluetoothService) {
private var connectionStateChangedListener: MutableList<(Int) -> Unit> = mutableListOf()
var connectionState = BLUETOOTH_DISABLED class ControllerService(
private set private val context: Context,
private val bluetoothService: BluetoothService
) {
private val samsungCommands = mutableMapOf<String, RemoteCommand>()
init { init {
handleBluetoothStateChanged(bluetoothService.state) loadCommands()
bluetoothService.onBluetoothStateChanged { }
handleBluetoothStateChanged(it)
} fun sendCommand(command: String) {
} if (samsungCommands[command] == null) return
Log.i("ControllerService", "Sending command: $command")
fun onConnectionStateChanged(callback: (Int) -> Unit) { val jsonString = remoteCommandToJsonString(samsungCommands[command]!!)
connectionStateChangedListener.add(callback) sendData(jsonString)
} }
private fun setConnectionState(state: Int) { fun remoteCommandToJsonString(command: RemoteCommand): String {
connectionState = state var commandObject = JSONObject()
connectionStateChangedListener.forEach { it(state) } commandObject.put("protocol", command.protocol)
} commandObject.put("address", command.device)
commandObject.put("command", command.function)
private fun handleBluetoothStateChanged(state: String) { return commandObject.toString()
when (state) { }
BluetoothService.STATE_OFF -> setConnectionState(BLUETOOTH_DISABLED)
else -> setConnectionState(NOT_CONNECTED) fun sendData(data: String) {
bluetoothService.sendData(data)
}
fun loadCommands() {
Log.i("ControllerService", "Loading commands");
var inputStream = context.resources.openRawResource(R.raw.samsung)
var csvReader = CSVReader(inputStream.reader())
csvReader.forEach { nextLine ->
if (nextLine.size < 5) return@forEach
if (nextLine[0] == "functionname") return@forEach
var remoteCommand = RemoteCommand()
remoteCommand.functionName = nextLine[0]
remoteCommand.protocol = "samsung"
remoteCommand.device = nextLine[2]
remoteCommand.subdevice = nextLine[3]
remoteCommand.function = nextLine[4]
samsungCommands[remoteCommand.functionName!!] = remoteCommand
} }
Log.i("ControllerService", "Commands loaded: ${samsungCommands.size}")
} }
companion object { companion object {
const val BLUETOOTH_DISABLED = 0 const val POWER = "POWER"
const val NOT_CONNECTED = 1 const val CURSOR_UP = "CURSOR UP"
const val CONNECTED = 2 const val CURSOR_DOWN = "CURSOR DOWN"
const val CURSOR_LEFT = "CURSOR LEFT"
const val CURSOR_RIGHT = "CURSOR RIGHT"
const val ENTER = "ENTER"
const val ONE = "1"
const val TWO = "2"
const val THREE = "3"
const val FOUR = "4"
const val FIVE = "5"
const val SIX = "6"
const val SEVEN = "7"
const val EIGHT = "8"
const val NINE = "9"
const val ZERO = "0"
const val CHANNEL_UP = "CHANNEL +"
const val CHANNEL_DOWN = "CHANNEL -"
const val VOLUME_UP = "VOLUME +"
const val VOLUME_DOWN = "VOLUME -"
const val MUTE = "MUTE"
const val GUIDE = "GUIDE"
const val INFO = "INFO"
const val AD_SUBTITLE = "AD/SUBT"
const val E_MANUAL = "E-MANUAL"
const val TOOLS = "TOOLS"
const val RETURN = "RETURN"
const val MENU = "MENU"
const val SMART_HUB = "SMART HUB"
const val EXIT = "EXIT"
const val CHANNEL_LIST = "CH LIST"
const val HOME = "HOME"
const val SETTINGS = "SETTINGS"
const val RED = "RED"
const val GREEN = "GREEN"
const val YELLOW = "YELLOW"
const val BLUE = "BLUE"
const val INPUT_SOURCE = "INPUT SOURCE"
const val REWIND = "REWIND"
const val PLAY = "PLAY"
const val PAUSE = "PAUSE"
const val FORWARD = "FORWARD"
} }
} }

View File

@ -0,0 +1,44 @@
package com.example.tvcontroller.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@Composable
fun RemoteButton(
modifier: Modifier = Modifier,
text: String? = "",
onClick: () -> Unit,
icon: Painter? = null,
width: Dp = 64.dp,
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.width(width)
.clip(RoundedCornerShape(12.dp))
.background(MaterialTheme.colorScheme.surfaceVariant)
.clickable(onClick = onClick)
.then(modifier)
) {
if (!text.isNullOrBlank()) Text(text = text)
if (icon != null) {
Icon(
icon,
contentDescription = "Settings"
)
}
}
}

View File

@ -0,0 +1,18 @@
package com.example.tvcontroller.ui.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun RemoteButtonPlaceholder(
modifier: Modifier = Modifier,
) {
Box(
modifier = Modifier
.width(64.dp)
.then(modifier)
) {}
}

View File

@ -0,0 +1,269 @@
package com.example.tvcontroller.ui.views
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.tvcontroller.R
import com.example.tvcontroller.services.ControllerService
import com.example.tvcontroller.ui.components.RemoteButton
import com.example.tvcontroller.ui.components.RemoteButtonPlaceholder
@Composable
fun RemoteView(modifier: Modifier = Modifier, controllerService: ControllerService) {
val viewModel = viewModel<RemoteViewModel>(
factory = RemoteViewModel.provideFactory(
controllerService
)
)
Column(
modifier = modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
RemoteButton(
onClick = { viewModel.sendCommand(ControllerService.POWER) },
modifier = Modifier.aspectRatio(1.5f),
icon = painterResource(id = R.drawable.baseline_power_settings_new_24)
)
RemoteButtonPlaceholder()
RemoteButton(
onClick = { viewModel.sendCommand(ControllerService.INPUT_SOURCE) },
modifier = Modifier.aspectRatio(1.5f),
icon = painterResource(id = R.drawable.baseline_login_24)
)
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
RemoteButton(
onClick = { viewModel.sendCommand(ControllerService.ONE) },
modifier = Modifier.aspectRatio(1.5f),
text = "1"
)
RemoteButton(
onClick = { viewModel.sendCommand(ControllerService.TWO) },
modifier = Modifier.aspectRatio(1.5f),
text = "2"
)
RemoteButton(
onClick = { viewModel.sendCommand(ControllerService.THREE) },
modifier = Modifier.aspectRatio(1.5f),
text = "3"
)
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
RemoteButton(
onClick = { viewModel.sendCommand(ControllerService.FOUR) },
modifier = Modifier.aspectRatio(1.5f),
text = "4"
)
RemoteButton(
onClick = { viewModel.sendCommand(ControllerService.FIVE) },
modifier = Modifier.aspectRatio(1.5f),
text = "5"
)
RemoteButton(
onClick = { viewModel.sendCommand(ControllerService.SIX) },
modifier = Modifier.aspectRatio(1.5f),
text = "6"
)
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
RemoteButton(
onClick = { viewModel.sendCommand(ControllerService.SEVEN) },
modifier = Modifier.aspectRatio(1.5f),
text = "7"
)
RemoteButton(
onClick = { viewModel.sendCommand(ControllerService.EIGHT) },
modifier = Modifier.aspectRatio(1.5f),
text = "8"
)
RemoteButton(
onClick = { viewModel.sendCommand(ControllerService.NINE) },
modifier = Modifier.aspectRatio(1.5f),
text = "9"
)
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
RemoteButton(
onClick = { viewModel.sendCommand(ControllerService.VOLUME_UP) },
modifier = Modifier.aspectRatio(1.5f),
icon=painterResource(R.drawable.baseline_volume_up_24)
)
RemoteButton(
onClick = { viewModel.sendCommand(ControllerService.ZERO) },
modifier = Modifier.aspectRatio(1.5f),
text = "0"
)
RemoteButton(
onClick = { viewModel.sendCommand(ControllerService.CHANNEL_UP) },
modifier = Modifier.aspectRatio(1.5f),
icon = painterResource(id = R.drawable.baseline_keyboard_arrow_up_24)
)
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
RemoteButton(
onClick = { viewModel.sendCommand(ControllerService.VOLUME_DOWN) },
modifier = Modifier.aspectRatio(1.5f),
icon=painterResource(R.drawable.baseline_volume_down_24)
)
RemoteButton(
onClick = { viewModel.sendCommand(ControllerService.MUTE) },
modifier = Modifier.aspectRatio(1.5f),
icon = painterResource(id = R.drawable.baseline_volume_off_24)
)
RemoteButton(
onClick = { viewModel.sendCommand(ControllerService.CHANNEL_DOWN) },
modifier = Modifier.aspectRatio(1.5f),
icon = painterResource(id = R.drawable.baseline_keyboard_arrow_down_24)
)
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
RemoteButton(
onClick = { viewModel.sendCommand(ControllerService.MENU) },
modifier = Modifier.aspectRatio(1.5f),
text = "MENU"
)
RemoteButton(
onClick = { viewModel.sendCommand(ControllerService.HOME) },
modifier = Modifier.aspectRatio(1.5f),
icon = painterResource(id = R.drawable.baseline_home_24)
)
RemoteButton(
onClick = { viewModel.sendCommand(ControllerService.SETTINGS) },
modifier = Modifier.aspectRatio(1.5f),
icon = painterResource(id = R.drawable.baseline_settings_24)
)
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
RemoteButton(
onClick = { viewModel.sendCommand(ControllerService.CHANNEL_LIST) },
modifier = Modifier.aspectRatio(1.5f),
icon = painterResource(id = R.drawable.baseline_list_alt_24)
)
RemoteButton(
onClick = { viewModel.sendCommand(ControllerService.CURSOR_UP) },
modifier = Modifier.aspectRatio(1.5f),
icon = painterResource(id = R.drawable.baseline_arrow_upward_24)
)
RemoteButton(
onClick = { viewModel.sendCommand(ControllerService.INFO) },
modifier = Modifier.aspectRatio(1.5f),
icon = painterResource(id = R.drawable.baseline_info_24)
)
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
RemoteButton(
onClick = { viewModel.sendCommand(ControllerService.CURSOR_LEFT) },
modifier = Modifier.aspectRatio(1.5f),
icon = painterResource(id = R.drawable.baseline_arrow_back_24)
)
RemoteButton(
text = "OK",
onClick = { viewModel.sendCommand(ControllerService.ENTER) },
modifier = Modifier.aspectRatio(1.5f),
)
RemoteButton(
onClick = { viewModel.sendCommand(ControllerService.CURSOR_RIGHT) },
modifier = Modifier.aspectRatio(1.5f),
icon = painterResource(id = R.drawable.baseline_arrow_forward_24)
)
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
RemoteButton(
onClick = { viewModel.sendCommand(ControllerService.RETURN) },
modifier = Modifier.aspectRatio(1.5f),
icon = painterResource(id = R.drawable.baseline_keyboard_return_24)
)
RemoteButton(
onClick = { viewModel.sendCommand(ControllerService.CURSOR_DOWN) },
modifier = Modifier.aspectRatio(1.5f),
icon = painterResource(id = R.drawable.baseline_arrow_downward_24)
)
RemoteButton(
onClick = { viewModel.sendCommand(ControllerService.EXIT) },
modifier = Modifier.aspectRatio(1.5f),
text = "EXIT"
)
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
val width = 46.dp
RemoteButton(
onClick = { viewModel.sendCommand(ControllerService.RED) },
modifier = Modifier.aspectRatio(1.5f).background(color = Color.Red).clip(
RoundedCornerShape(12.dp)
),
width = width,
)
RemoteButton(
onClick = { viewModel.sendCommand(ControllerService.GREEN) },
modifier = Modifier.aspectRatio(1.5f).background(color = Color.Green).clip(
RoundedCornerShape(12.dp)
),
width = width,
)
RemoteButton(
onClick = { viewModel.sendCommand(ControllerService.YELLOW) },
modifier = Modifier.aspectRatio(1.5f).background(color = Color.Yellow).clip(
RoundedCornerShape(12.dp)
),
width = width,
)
RemoteButton(
onClick = { viewModel.sendCommand(ControllerService.BLUE) },
modifier = Modifier.aspectRatio(1.5f).background(color = Color.Blue).clip(
RoundedCornerShape(12.dp)
),
width = width,
)
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
val width = 46.dp
RemoteButton(
onClick = { viewModel.sendCommand(ControllerService.REWIND) },
modifier = Modifier.aspectRatio(1.5f),
width = width,
icon = painterResource(id = R.drawable.baseline_fast_rewind_24)
)
RemoteButton(
onClick = { viewModel.sendCommand(ControllerService.PLAY) },
modifier = Modifier.aspectRatio(1.5f),
width = width,
icon = painterResource(id = R.drawable.baseline_play_arrow_24)
)
RemoteButton(
onClick = { viewModel.sendCommand(ControllerService.PAUSE) },
modifier = Modifier.aspectRatio(1.5f),
width = width,
icon = painterResource(id = R.drawable.baseline_pause_24)
)
RemoteButton(
onClick = { viewModel.sendCommand(ControllerService.FORWARD) },
modifier = Modifier.aspectRatio(1.5f),
width = width,
icon = painterResource(id = R.drawable.baseline_fast_forward_24)
)
}
}
}
}

View File

@ -0,0 +1,32 @@
package com.example.tvcontroller.ui.views
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.example.tvcontroller.services.BluetoothService
import com.example.tvcontroller.services.ControllerService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.json.JSONObject
class RemoteViewModel(
private val controllerService: ControllerService
) : ViewModel() {
fun sendCommand(command: String) {
viewModelScope.launch(Dispatchers.IO) {
controllerService.sendCommand(command)
}
}
companion object {
fun provideFactory(
controllerService: ControllerService
) = object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return RemoteViewModel(controllerService) as T
}
}
}
}

View File

@ -42,12 +42,11 @@ import com.example.tvcontroller.services.DeviceService
@Composable @Composable
fun SettingsView( fun SettingsView(
deviceService: DeviceService, deviceService: DeviceService,
controllerService: ControllerService,
bluetoothService: BluetoothService bluetoothService: BluetoothService
) { ) {
val viewModel = viewModel<SettingsViewModel>( val viewModel = viewModel<SettingsViewModel>(
factory = SettingsViewModel.provideFactory( factory = SettingsViewModel.provideFactory(
deviceService, controllerService, bluetoothService deviceService, bluetoothService
) )
) )
val navController = rememberNavController() val navController = rememberNavController()

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M20,12l-1.41,-1.41L13,16.17V4h-2v12.17l-5.58,-5.59L4,12l8,8 8,-8z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M12,4l-1.41,1.41L16.17,11H4v2h12.17l-5.58,5.59L12,20l8,-8z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M4,12l1.41,1.41L11,7.83V20h2V7.83l5.58,5.59L20,12l-8,-8 -8,8z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M4,18l8.5,-6L4,6v12zM13,6v12l8.5,-6L13,6z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M11,18L11,6l-8.5,6 8.5,6zM11.5,12l8.5,6L20,6l-8.5,6z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M7.41,8.59L12,13.17l4.59,-4.58L18,10l-6,6 -6,-6 1.41,-1.41z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M7.41,15.41L12,10.83l4.59,4.58L18,14l-6,-6 -6,6z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M19,7v4H5.83l3.58,-3.59L8,6l-6,6 6,6 1.41,-1.41L5.83,13H21V7z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M19,5v14L5,19L5,5h14m1.1,-2L3.9,3c-0.5,0 -0.9,0.4 -0.9,0.9v16.2c0,0.4 0.4,0.9 0.9,0.9h16.2c0.4,0 0.9,-0.5 0.9,-0.9L21,3.9c0,-0.5 -0.5,-0.9 -0.9,-0.9zM11,7h6v2h-6L11,7zM11,11h6v2h-6v-2zM11,15h6v2h-6zM7,7h2v2L7,9zM7,11h2v2L7,13zM7,15h2v2L7,17z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M11,7L9.6,8.4l2.6,2.6H2v2h10.2l-2.6,2.6L11,17l5,-5L11,7zM20,19h-8v2h8c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2h-8v2h8V19z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M8,5v14l11,-7z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M13,3h-2v10h2L13,3zM17.83,5.17l-1.42,1.42C17.99,7.86 19,9.81 19,12c0,3.87 -3.13,7 -7,7s-7,-3.13 -7,-7c0,-2.19 1.01,-4.14 2.58,-5.42L6.17,5.17C4.23,6.82 3,9.26 3,12c0,4.97 4.03,9 9,9s9,-4.03 9,-9c0,-2.74 -1.23,-5.18 -3.17,-6.83z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M18.5,12c0,-1.77 -1.02,-3.29 -2.5,-4.03v8.05c1.48,-0.73 2.5,-2.25 2.5,-4.02zM5,9v6h4l5,5V4L9,9H5z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M16.5,12c0,-1.77 -1.02,-3.29 -2.5,-4.03v2.21l2.45,2.45c0.03,-0.2 0.05,-0.41 0.05,-0.63zM19,12c0,0.94 -0.2,1.82 -0.54,2.64l1.51,1.51C20.63,14.91 21,13.5 21,12c0,-4.28 -2.99,-7.86 -7,-8.77v2.06c2.89,0.86 5,3.54 5,6.71zM4.27,3L3,4.27 7.73,9L3,9v6h4l5,5v-6.73l4.25,4.25c-0.67,0.52 -1.42,0.93 -2.25,1.18v2.06c1.38,-0.31 2.63,-0.95 3.69,-1.81L19.73,21 21,19.73l-9,-9L4.27,3zM12,4L9.91,6.09 12,8.18L12,4z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M3,9v6h4l5,5L12,4L7,9L3,9zM16.5,12c0,-1.77 -1.02,-3.29 -2.5,-4.03v8.05c1.48,-0.73 2.5,-2.25 2.5,-4.02zM14,3.23v2.06c2.89,0.86 5,3.54 5,6.71s-2.11,5.85 -5,6.71v2.06c4.01,-0.91 7,-4.49 7,-8.77s-2.99,-7.86 -7,-8.77z"/>
</vector>

View File

@ -0,0 +1,39 @@
functionname,protocol,device,subdevice,function
INPUT SOURCE,NECx2,7,7,1
POWER,NECx2,7,7,2
1,NECx2,7,7,4
2,NECx2,7,7,5
3,NECx2,7,7,6
VOLUME +,NECx2,7,7,7
4,NECx2,7,7,8
5,NECx2,7,7,9
6,NECx2,7,7,10
VOLUME -,NECx2,7,7,11
7,NECx2,7,7,12
8,NECx2,7,7,13
9,NECx2,7,7,14
MUTE,NECx2,7,7,15
CHANNEL -,NECx2,7,7,16
0,NECx2,7,7,17
CHANNEL +,NECx2,7,7,18
LAST,NECx2,7,7,19
MENU,NECx2,7,7,26
INFO,NECx2,7,7,31
AD/SUBT,NECx2,7,7,37
EXIT,NECx2,7,7,45
E-MANUAL,NECx2,7,7,63
TOOLS,NECx2,7,7,75
GUIDE,NECx2,7,7,79
RETURN,NECx2,7,7,88
CURSOR UP,NECx2,7,7,96
CURSOR DOWN,NECx2,7,7,97
CURSOR RIGHT,NECx2,7,7,98
CURSOR LEFT,NECx2,7,7,101
ENTER,NECx2,7,7,104
CH LIST,NECx2,7,7,107
SMART HUB,NECx2,7,7,121
3D,NECx2,7,7,159
HDMI2,NECx2,7,7,190
HDMI3,NECx2,7,7,194
HDMI4,NECx2,7,7,197
HDMI1,NECx2,7,7,233
1 functionname protocol device subdevice function
2 INPUT SOURCE NECx2 7 7 1
3 POWER NECx2 7 7 2
4 1 NECx2 7 7 4
5 2 NECx2 7 7 5
6 3 NECx2 7 7 6
7 VOLUME + NECx2 7 7 7
8 4 NECx2 7 7 8
9 5 NECx2 7 7 9
10 6 NECx2 7 7 10
11 VOLUME - NECx2 7 7 11
12 7 NECx2 7 7 12
13 8 NECx2 7 7 13
14 9 NECx2 7 7 14
15 MUTE NECx2 7 7 15
16 CHANNEL - NECx2 7 7 16
17 0 NECx2 7 7 17
18 CHANNEL + NECx2 7 7 18
19 LAST NECx2 7 7 19
20 MENU NECx2 7 7 26
21 INFO NECx2 7 7 31
22 AD/SUBT NECx2 7 7 37
23 EXIT NECx2 7 7 45
24 E-MANUAL NECx2 7 7 63
25 TOOLS NECx2 7 7 75
26 GUIDE NECx2 7 7 79
27 RETURN NECx2 7 7 88
28 CURSOR UP NECx2 7 7 96
29 CURSOR DOWN NECx2 7 7 97
30 CURSOR RIGHT NECx2 7 7 98
31 CURSOR LEFT NECx2 7 7 101
32 ENTER NECx2 7 7 104
33 CH LIST NECx2 7 7 107
34 SMART HUB NECx2 7 7 121
35 3D NECx2 7 7 159
36 HDMI2 NECx2 7 7 190
37 HDMI3 NECx2 7 7 194
38 HDMI4 NECx2 7 7 197
39 HDMI1 NECx2 7 7 233

View File

@ -12,6 +12,7 @@ activityCompose = "1.8.0"
composeBom = "2024.04.01" composeBom = "2024.04.01"
material3 = "1.4.0-alpha10" material3 = "1.4.0-alpha10"
navigationCompose = "2.8.4" navigationCompose = "2.8.4"
opencsv = "4.6"
[libraries] [libraries]
androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "cameraCore" } androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "cameraCore" }
@ -39,6 +40,7 @@ androidx-material3 = { group = "androidx.compose.material3", name = "material3"
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" } material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" }
opencsv = { module = "com.opencsv:opencsv", version.ref = "opencsv" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }