feat: add remote ui to send commands via bt
This commit is contained in:
parent
4f40490fee
commit
cba125738d
@ -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)
|
||||
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
) {}
|
||||
}
|
||||
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
|
||||
5
app/src/main/res/drawable/baseline_arrow_downward_24.xml
Normal file
5
app/src/main/res/drawable/baseline_arrow_downward_24.xml
Normal 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>
|
||||
5
app/src/main/res/drawable/baseline_arrow_forward_24.xml
Normal file
5
app/src/main/res/drawable/baseline_arrow_forward_24.xml
Normal 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>
|
||||
5
app/src/main/res/drawable/baseline_arrow_upward_24.xml
Normal file
5
app/src/main/res/drawable/baseline_arrow_upward_24.xml
Normal 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>
|
||||
5
app/src/main/res/drawable/baseline_fast_forward_24.xml
Normal file
5
app/src/main/res/drawable/baseline_fast_forward_24.xml
Normal 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>
|
||||
5
app/src/main/res/drawable/baseline_fast_rewind_24.xml
Normal file
5
app/src/main/res/drawable/baseline_fast_rewind_24.xml
Normal 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>
|
||||
5
app/src/main/res/drawable/baseline_home_24.xml
Normal file
5
app/src/main/res/drawable/baseline_home_24.xml
Normal 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>
|
||||
5
app/src/main/res/drawable/baseline_info_24.xml
Normal file
5
app/src/main/res/drawable/baseline_info_24.xml
Normal 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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
5
app/src/main/res/drawable/baseline_list_alt_24.xml
Normal file
5
app/src/main/res/drawable/baseline_list_alt_24.xml
Normal 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>
|
||||
5
app/src/main/res/drawable/baseline_login_24.xml
Normal file
5
app/src/main/res/drawable/baseline_login_24.xml
Normal 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>
|
||||
5
app/src/main/res/drawable/baseline_pause_24.xml
Normal file
5
app/src/main/res/drawable/baseline_pause_24.xml
Normal 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>
|
||||
5
app/src/main/res/drawable/baseline_play_arrow_24.xml
Normal file
5
app/src/main/res/drawable/baseline_play_arrow_24.xml
Normal 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>
|
||||
@ -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>
|
||||
5
app/src/main/res/drawable/baseline_volume_down_24.xml
Normal file
5
app/src/main/res/drawable/baseline_volume_down_24.xml
Normal 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>
|
||||
5
app/src/main/res/drawable/baseline_volume_off_24.xml
Normal file
5
app/src/main/res/drawable/baseline_volume_off_24.xml
Normal 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>
|
||||
5
app/src/main/res/drawable/baseline_volume_up_24.xml
Normal file
5
app/src/main/res/drawable/baseline_volume_up_24.xml
Normal 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>
|
||||
39
app/src/main/res/raw/samsung.csv
Normal file
39
app/src/main/res/raw/samsung.csv
Normal 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
|
||||
|
@ -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" }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user