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.extensions)
implementation(libs.material3)
implementation(libs.opencsv)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)

View File

@ -5,11 +5,8 @@ import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
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.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
@ -24,7 +21,6 @@ import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.example.tvcontroller.ui.theme.TVControllerTheme
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.res.painterResource
import androidx.core.app.ActivityCompat
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.DeviceService
import com.example.tvcontroller.ui.views.CameraView
import com.example.tvcontroller.ui.views.RemoteView
import com.example.tvcontroller.ui.views.SettingsView
@ -48,7 +45,7 @@ class MainActivity : ComponentActivity() {
bluetoothService = BluetoothService(this.applicationContext)
deviceService = DeviceService(this.applicationContext)
cameraService = CameraService(this.applicationContext)
controllerService = ControllerService(bluetoothService)
controllerService = ControllerService(this.applicationContext, bluetoothService)
checkPermissions()
enableEdgeToEdge()
setContent {
@ -125,19 +122,20 @@ fun TvControllerApp(
}) { innerPadding ->
NavHost(
navController = navController,
startDestination = Screen.Settings.name,
startDestination = Screen.Remote.name,
modifier = Modifier.padding(innerPadding)
) {
composable(route = Screen.Camera.name) {
CameraView()
}
composable(route = Screen.Remote.name) {
RemoteScreen()
RemoteView(
controllerService = controllerService
)
}
composable(route = Screen.Settings.name) {
SettingsView(
deviceService = deviceService,
controllerService = controllerService,
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
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
@ -9,14 +8,12 @@ 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.Dispatchers
import kotlinx.coroutines.launch
class SettingsViewModel(
private val deviceService: DeviceService,
controllerService: ControllerService,
private val bluetoothService: BluetoothService
) : ViewModel() {
var serverAddress by mutableStateOf(deviceService.getServerAddress())
@ -103,12 +100,11 @@ class SettingsViewModel(
fun provideFactory(
deviceService: DeviceService,
controllerService: ControllerService,
bluetoothService: BluetoothService
) = object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
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) {
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
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
private set
class ControllerService(
private val context: Context,
private val bluetoothService: BluetoothService
) {
private val samsungCommands = mutableMapOf<String, RemoteCommand>()
init {
handleBluetoothStateChanged(bluetoothService.state)
bluetoothService.onBluetoothStateChanged {
handleBluetoothStateChanged(it)
}
}
fun onConnectionStateChanged(callback: (Int) -> Unit) {
connectionStateChangedListener.add(callback)
}
private fun setConnectionState(state: Int) {
connectionState = state
connectionStateChangedListener.forEach { it(state) }
}
private fun handleBluetoothStateChanged(state: String) {
when (state) {
BluetoothService.STATE_OFF -> setConnectionState(BLUETOOTH_DISABLED)
else -> setConnectionState(NOT_CONNECTED)
loadCommands()
}
fun sendCommand(command: String) {
if (samsungCommands[command] == null) return
Log.i("ControllerService", "Sending command: $command")
val jsonString = remoteCommandToJsonString(samsungCommands[command]!!)
sendData(jsonString)
}
fun remoteCommandToJsonString(command: RemoteCommand): String {
var commandObject = JSONObject()
commandObject.put("protocol", command.protocol)
commandObject.put("address", command.device)
commandObject.put("command", command.function)
return commandObject.toString()
}
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 {
const val BLUETOOTH_DISABLED = 0
const val NOT_CONNECTED = 1
const val CONNECTED = 2
const val POWER = "POWER"
const val CURSOR_UP = "CURSOR UP"
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
fun SettingsView(
deviceService: DeviceService,
controllerService: ControllerService,
bluetoothService: BluetoothService
) {
val viewModel = viewModel<SettingsViewModel>(
factory = SettingsViewModel.provideFactory(
deviceService, controllerService, bluetoothService
deviceService, bluetoothService
)
)
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"
material3 = "1.4.0-alpha10"
navigationCompose = "2.8.4"
opencsv = "4.6"
[libraries]
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-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" }
opencsv = { module = "com.opencsv:opencsv", version.ref = "opencsv" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }