Compare commits

...

2 Commits

11 changed files with 143 additions and 54 deletions

View File

@ -52,6 +52,13 @@ dependencies {
implementation(libs.androidx.navigation.compose)
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.cio)
implementation(libs.androidx.camera.core)
implementation(libs.androidx.camera.camera2)
implementation(libs.androidx.camera.lifecycle)
implementation(libs.androidx.camera.video)
implementation(libs.androidx.camera.view)
implementation(libs.androidx.camera.mlkit.vision)
implementation(libs.androidx.camera.extensions)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)

View File

@ -1,10 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<application
android:allowBackup="true"

View File

@ -1,13 +1,11 @@
package com.example.tvcontroller
import android.bluetooth.BluetoothAdapter
import android.content.ContentValues.TAG
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
@ -29,41 +27,47 @@ 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.lifecycle.viewmodel.compose.viewModel
import androidx.core.app.ActivityCompat
import com.example.tvcontroller.services.BluetoothService
import com.example.tvcontroller.services.CameraService
import com.example.tvcontroller.services.DeviceService
import com.example.tvcontroller.ui.AppViewModel
import com.example.tvcontroller.ui.views.CameraView
import com.example.tvcontroller.ui.views.SettingsView
class MainActivity : ComponentActivity() {
private lateinit var bluetoothService: BluetoothService
private lateinit var deviceService: DeviceService
private val appViewModel by viewModels<AppViewModel>()
private lateinit var cameraService: CameraService
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bluetoothService = BluetoothService(this.applicationContext)
bluetoothService.onBluetoothStateChanged { state -> appViewModel.setBluetoothEnabled(state == BluetoothAdapter.STATE_ON) }
appViewModel.setBluetoothEnabled(bluetoothService.isBluetoothEnabled())
deviceService = DeviceService(this.applicationContext)
cameraService = CameraService(this.applicationContext)
checkPermissions()
enableEdgeToEdge()
setContent {
TVControllerTheme {
TvControllerApp(
appViewModel = appViewModel,
bluetoothService = bluetoothService,
deviceService = deviceService
)
}
}
}
private fun checkPermissions() {
if (!cameraService.hasRequiredPermissions()) {
ActivityCompat.requestPermissions(this, CameraService.CAMERAX_PERMISSIONS, 0)
}
}
}
@Composable
fun TvControllerApp(
navController: NavHostController = rememberNavController(),
appViewModel: AppViewModel = viewModel(),
bluetoothService: BluetoothService? = null,
bluetoothService: BluetoothService,
deviceService: DeviceService
) {
val backStackEntry by navController.currentBackStackEntryAsState()
@ -115,31 +119,22 @@ fun TvControllerApp(
modifier = Modifier.padding(innerPadding)
) {
composable(route = Screen.Camera.name) {
CameraScreen()
CameraView()
}
composable(route = Screen.Remote.name) {
RemoteScreen()
}
composable(route = Screen.Settings.name) {
SettingsScreen(appViewModel = appViewModel, deviceService = deviceService)
SettingsView(
deviceService = deviceService,
bluetoothService = bluetoothService
)
}
}
}
}
}
@Composable
fun CameraScreen(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(text = "Camera Screen", modifier = modifier)
}
}
@Composable
fun RemoteScreen(modifier: Modifier = Modifier) {
Column(

View File

@ -1,15 +1,20 @@
package com.example.tvcontroller
import android.bluetooth.BluetoothAdapter
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.example.tvcontroller.services.BluetoothService
import com.example.tvcontroller.services.DeviceService
import kotlinx.coroutines.launch
class SettingsViewModel(private val deviceService: DeviceService) : ViewModel() {
class SettingsViewModel(
private val deviceService: DeviceService,
private val bluetoothService: BluetoothService
) : ViewModel() {
var serverAddress by mutableStateOf(deviceService.getServerAddress())
private set
var deviceName by mutableStateOf(android.os.Build.MANUFACTURER + " " + android.os.Build.MODEL)
@ -18,12 +23,17 @@ class SettingsViewModel(private val deviceService: DeviceService) : ViewModel()
private set
var connectionState by mutableStateOf(Settings.ConnectionState.Unregistered)
private set
var bluetoothEnabled by mutableStateOf(bluetoothService.isBluetoothEnabled())
private set
init {
updateConnectionState()
viewModelScope.launch {
updateDeviceInfo()
}
bluetoothService.onBluetoothStateChanged {
bluetoothEnabled = it == BluetoothAdapter.STATE_ON
}
}
fun connect() {
@ -69,10 +79,11 @@ class SettingsViewModel(private val deviceService: DeviceService) : ViewModel()
companion object {
fun provideFactory(
deviceService: DeviceService,
bluetoothService: BluetoothService,
) = object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return SettingsViewModel(deviceService) as T
return SettingsViewModel(deviceService, bluetoothService) as T
}
}
}

View File

@ -0,0 +1,19 @@
package com.example.tvcontroller.services
import android.content.Context
import android.content.pm.PackageManager
class CameraService(private val context: Context) {
fun hasRequiredPermissions(): Boolean {
return CAMERAX_PERMISSIONS.all {
context.checkSelfPermission(it) == PackageManager.PERMISSION_GRANTED
}
}
companion object {
val CAMERAX_PERMISSIONS = arrayOf(
android.Manifest.permission.CAMERA,
android.Manifest.permission.RECORD_AUDIO
)
}
}

View File

@ -1,7 +0,0 @@
package com.example.tvcontroller.ui
import com.example.tvcontroller.Screen
data class AppUiState(
val currentScreen: Screen = Screen.Settings,
)

View File

@ -1,16 +0,0 @@
package com.example.tvcontroller.ui
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
class AppViewModel() : ViewModel() {
private var isBluetoothEnabled = mutableStateOf(false)
fun setBluetoothEnabled(enabled: Boolean) {
isBluetoothEnabled.value = enabled
}
fun isBluetoothEnabled(): Boolean {
return isBluetoothEnabled.value
}
}

View File

@ -0,0 +1,25 @@
package com.example.tvcontroller.ui.components
import androidx.camera.view.LifecycleCameraController
import androidx.camera.view.PreviewView
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.compose.LocalLifecycleOwner
@Composable
fun CameraPreview(
controller: LifecycleCameraController,
modifier: Modifier = Modifier
) {
val lifecycleOwner = LocalLifecycleOwner.current
AndroidView(
modifier = modifier,
factory = {
PreviewView(it).apply {
this.controller = controller
controller.bindToLifecycle(lifecycleOwner)
}
},
)
}

View File

@ -0,0 +1,34 @@
package com.example.tvcontroller.ui.views
import androidx.camera.view.CameraController
import androidx.camera.view.LifecycleCameraController
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.example.tvcontroller.ui.components.CameraPreview
@Composable
fun CameraView() {
val context = LocalContext.current
val controller = remember {
LifecycleCameraController(context).apply {
setEnabledUseCases(CameraController.VIDEO_CAPTURE)
}
}
Box(
modifier = Modifier
.fillMaxSize()
.padding(all = 16.dp),
) {
CameraPreview(controller = controller, modifier = Modifier.fillMaxSize())
}
}

View File

@ -1,4 +1,4 @@
package com.example.tvcontroller
package com.example.tvcontroller.ui.views
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@ -16,13 +16,19 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.tvcontroller.R
import com.example.tvcontroller.Settings
import com.example.tvcontroller.SettingsViewModel
import com.example.tvcontroller.services.BluetoothService
import com.example.tvcontroller.services.DeviceService
import com.example.tvcontroller.ui.AppViewModel
@Composable
fun SettingsScreen(deviceService: DeviceService, appViewModel: AppViewModel) {
fun SettingsView(
deviceService: DeviceService,
bluetoothService: BluetoothService
) {
val viewModel =
viewModel<SettingsViewModel>(factory = SettingsViewModel.provideFactory(deviceService))
viewModel<SettingsViewModel>(factory = SettingsViewModel.provideFactory(deviceService, bluetoothService))
Column(
modifier = Modifier
@ -73,7 +79,7 @@ fun SettingsScreen(deviceService: DeviceService, appViewModel: AppViewModel) {
text = stringResource(id = R.string.controller_settings_heading),
style = MaterialTheme.typography.headlineSmall
)
if (appViewModel.isBluetoothEnabled()) {
if (viewModel.bluetoothEnabled) {
Text(
text = "Controller settings: Bluetooth is enabled.",
style = MaterialTheme.typography.bodyMedium

View File

@ -1,5 +1,6 @@
[versions]
agp = "8.9.0"
cameraCore = "1.4.1"
kotlin = "2.0.0"
coreKtx = "1.10.1"
junit = "4.13.2"
@ -12,6 +13,13 @@ composeBom = "2024.04.01"
navigationCompose = "2.8.4"
[libraries]
androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "cameraCore" }
androidx-camera-core = { module = "androidx.camera:camera-core", version.ref = "cameraCore" }
androidx-camera-extensions = { module = "androidx.camera:camera-extensions", version.ref = "cameraCore" }
androidx-camera-mlkit-vision = { module = "androidx.camera:camera-mlkit-vision", version.ref = "cameraCore" }
androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = "cameraCore" }
androidx-camera-video = { module = "androidx.camera:camera-video", version.ref = "cameraCore" }
androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "cameraCore" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
junit = { group = "junit", name = "junit", version.ref = "junit" }