From 82f3a5bd10b1ca07915badfced5953ceb15412f1 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Sat, 14 Feb 2026 18:40:57 +0100 Subject: [PATCH 1/2] Add options to hide system bars and turn screen on --- .../example/ava/settings/DisplaySettings.kt | 59 +++++++++++++++++++ .../example/ava/ui/screens/home/HomeScreen.kt | 20 ++++++- .../ava/ui/screens/home/HomeViewModel.kt | 13 ++++ .../screens/home/components/HideSystemBars.kt | 31 ++++++++++ .../components/WakeScreenOnInteraction.kt | 35 +++++++++++ .../ui/screens/settings/SettingsViewModel.kt | 16 ++++- .../settings/VoiceSatelliteSettings.kt | 32 +++++++++- .../ava/ui/services/ServiceViewModel.kt | 8 ++- app/src/main/res/values/strings.xml | 4 ++ 9 files changed, 210 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/com/example/ava/settings/DisplaySettings.kt create mode 100644 app/src/main/java/com/example/ava/ui/screens/home/HomeViewModel.kt create mode 100644 app/src/main/java/com/example/ava/ui/screens/home/components/HideSystemBars.kt create mode 100644 app/src/main/java/com/example/ava/ui/screens/home/components/WakeScreenOnInteraction.kt diff --git a/app/src/main/java/com/example/ava/settings/DisplaySettings.kt b/app/src/main/java/com/example/ava/settings/DisplaySettings.kt new file mode 100644 index 0000000..c2d57da --- /dev/null +++ b/app/src/main/java/com/example/ava/settings/DisplaySettings.kt @@ -0,0 +1,59 @@ +package com.example.ava.settings + +import android.content.Context +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.flow.map +import kotlinx.serialization.Serializable +import javax.inject.Inject +import javax.inject.Singleton + +@Serializable +data class DisplaySettings( + val wakeScreen: Boolean = false, + val hideSystemBars: Boolean = false +) + +private val DEFAULT = DisplaySettings() + +/** + * Used to inject a concrete implementation of DisplaySettingsStore + */ +@Module +@InstallIn(SingletonComponent::class) +abstract class DisplaySettingsModule() { + @Binds + abstract fun bindDisplaySettingsStore(displaySettingsStoreImpl: DisplaySettingsStoreImpl): DisplaySettingsStore +} + +interface DisplaySettingsStore : SettingsStore { + /** + * Whether to wake the screen when an event occurs. + */ + val wakeScreen: SettingState + + /** + * Whether to hide the system bars. + */ + val hideSystemBars: SettingState +} + +@Singleton +class DisplaySettingsStoreImpl @Inject constructor(@param:ApplicationContext private val context: Context) : + DisplaySettingsStore, SettingsStoreImpl( + context = context, + default = DEFAULT, + fileName = "display_settings.json", + serializer = DisplaySettings.serializer() +) { + override val wakeScreen = SettingState(getFlow().map { it.wakeScreen }) { value -> + update { it.copy(wakeScreen = value) } + } + + override val hideSystemBars = SettingState(getFlow().map { it.hideSystemBars }) { value -> + update { it.copy(hideSystemBars = value) } + } +} diff --git a/app/src/main/java/com/example/ava/ui/screens/home/HomeScreen.kt b/app/src/main/java/com/example/ava/ui/screens/home/HomeScreen.kt index 385225c..b699544 100644 --- a/app/src/main/java/com/example/ava/ui/screens/home/HomeScreen.kt +++ b/app/src/main/java/com/example/ava/ui/screens/home/HomeScreen.kt @@ -30,20 +30,34 @@ import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import com.example.ava.R import com.example.ava.ui.Settings +import com.example.ava.ui.screens.home.components.HideSystemBars +import com.example.ava.ui.screens.home.components.WakeScreenOnInteraction import com.example.ava.ui.services.StartStopVoiceSatellite import com.example.ava.ui.services.components.timerListSection import com.example.ava.ui.services.components.timerState @OptIn(ExperimentalMaterial3Api::class) @Composable -fun HomeScreen(navController: NavController) { - val configuration = LocalConfiguration.current - val isLandscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE +fun HomeScreen( + navController: NavController, + viewModel: HomeViewModel = hiltViewModel() +) { + val isLandscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE val timerState = timerState() + val displaySettings by viewModel.displaySettings.collectAsStateWithLifecycle(null) + if (displaySettings?.wakeScreen == true) { + WakeScreenOnInteraction() + } + if (displaySettings?.hideSystemBars == true) { + HideSystemBars() + } + Scaffold( modifier = Modifier.fillMaxSize(), topBar = { diff --git a/app/src/main/java/com/example/ava/ui/screens/home/HomeViewModel.kt b/app/src/main/java/com/example/ava/ui/screens/home/HomeViewModel.kt new file mode 100644 index 0000000..0364cfc --- /dev/null +++ b/app/src/main/java/com/example/ava/ui/screens/home/HomeViewModel.kt @@ -0,0 +1,13 @@ +package com.example.ava.ui.screens.home + +import androidx.lifecycle.ViewModel +import com.example.ava.settings.DisplaySettingsStore +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class HomeViewModel @Inject constructor( + displaySettingsStore: DisplaySettingsStore +) : ViewModel() { + val displaySettings = displaySettingsStore.getFlow() +} diff --git a/app/src/main/java/com/example/ava/ui/screens/home/components/HideSystemBars.kt b/app/src/main/java/com/example/ava/ui/screens/home/components/HideSystemBars.kt new file mode 100644 index 0000000..6e98d69 --- /dev/null +++ b/app/src/main/java/com/example/ava/ui/screens/home/components/HideSystemBars.kt @@ -0,0 +1,31 @@ +package com.example.ava.ui.screens.home.components + +import androidx.activity.compose.LocalActivity +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat + +@Composable +fun HideSystemBars() { + val window = LocalActivity.current?.window + if (window != null) { + DisposableEffect("HideSystemBars") { + val insetsController = WindowCompat.getInsetsController(window, window.decorView) + + insetsController.apply { + hide(WindowInsetsCompat.Type.systemBars()) + systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } + + onDispose { + insetsController.apply { + show(WindowInsetsCompat.Type.systemBars()) + systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_DEFAULT + } + } + } + } +} diff --git a/app/src/main/java/com/example/ava/ui/screens/home/components/WakeScreenOnInteraction.kt b/app/src/main/java/com/example/ava/ui/screens/home/components/WakeScreenOnInteraction.kt new file mode 100644 index 0000000..8a1324f --- /dev/null +++ b/app/src/main/java/com/example/ava/ui/screens/home/components/WakeScreenOnInteraction.kt @@ -0,0 +1,35 @@ +package com.example.ava.ui.screens.home.components + +import android.view.WindowManager +import androidx.activity.compose.LocalActivity +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.example.ava.esphome.voicesatellite.Listening +import com.example.ava.esphome.voicesatellite.Processing +import com.example.ava.esphome.voicesatellite.Responding +import com.example.ava.esphome.voicesatellite.VoiceTimer +import com.example.ava.ui.services.ServiceViewModel + + +@Composable +fun WakeScreenOnInteraction(viewModel: ServiceViewModel = hiltViewModel()) { + val timers by viewModel.voiceTimers.collectAsStateWithLifecycle(emptyList()) + val currentState by viewModel.satelliteState.collectAsStateWithLifecycle(null) + val wakingStates = setOf(Listening, Processing, Responding) + + if (wakingStates.contains(currentState) || timers.any({ it !is VoiceTimer.Paused })) { + val window = LocalActivity.current?.window + if (window != null) { + DisposableEffect("WakeScreenOnInteraction") { + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + + onDispose { + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/ava/ui/screens/settings/SettingsViewModel.kt b/app/src/main/java/com/example/ava/ui/screens/settings/SettingsViewModel.kt index f177ca2..aceb875 100644 --- a/app/src/main/java/com/example/ava/ui/screens/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/example/ava/ui/screens/settings/SettingsViewModel.kt @@ -7,6 +7,7 @@ import androidx.compose.runtime.Immutable import androidx.core.net.toUri import androidx.lifecycle.ViewModel import com.example.ava.R +import com.example.ava.settings.DisplaySettingsStore import com.example.ava.settings.MicrophoneSettingsStore import com.example.ava.settings.PlayerSettingsStore import com.example.ava.settings.VoiceSatelliteSettingsStore @@ -33,7 +34,8 @@ class SettingsViewModel @Inject constructor( @param:ApplicationContext private val context: Context, private val satelliteSettingsStore: VoiceSatelliteSettingsStore, private val playerSettingsStore: PlayerSettingsStore, - private val microphoneSettingsStore: MicrophoneSettingsStore + private val microphoneSettingsStore: MicrophoneSettingsStore, + private val displaySettingsStore: DisplaySettingsStore ) : ViewModel() { val satelliteSettingsState = satelliteSettingsStore.getFlow() @@ -56,6 +58,8 @@ class SettingsViewModel @Inject constructor( val playerSettingsState = playerSettingsStore.getFlow() + val displaySettingsState = displaySettingsStore.getFlow() + suspend fun saveName(name: String) { if (validateName(name).isNullOrBlank()) { satelliteSettingsStore.name.set(name) @@ -148,6 +152,14 @@ class SettingsViewModel @Inject constructor( playerSettingsStore.repeatTimerFinishedSound.set(repeatTimerFinishedSound) } + suspend fun saveWakeScreen(wakeScreen: Boolean) { + displaySettingsStore.wakeScreen.set(wakeScreen) + } + + suspend fun saveHideSystemBars(hideSystemBars: Boolean) { + displaySettingsStore.hideSystemBars.set(hideSystemBars) + } + fun validateName(name: String): String? = if (name.isBlank()) context.getString(R.string.validation_voice_satellite_name_empty) @@ -167,4 +179,4 @@ class SettingsViewModel @Inject constructor( else null } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/example/ava/ui/screens/settings/VoiceSatelliteSettings.kt b/app/src/main/java/com/example/ava/ui/screens/settings/VoiceSatelliteSettings.kt index 8c89bea..d112f1d 100644 --- a/app/src/main/java/com/example/ava/ui/screens/settings/VoiceSatelliteSettings.kt +++ b/app/src/main/java/com/example/ava/ui/screens/settings/VoiceSatelliteSettings.kt @@ -30,6 +30,7 @@ fun VoiceSatelliteSettings( val satelliteState by viewModel.satelliteSettingsState.collectAsStateWithLifecycle(null) val microphoneState by viewModel.microphoneSettingsState.collectAsStateWithLifecycle(null) val playerState by viewModel.playerSettingsState.collectAsStateWithLifecycle(null) + val displayState by viewModel.displaySettingsState.collectAsStateWithLifecycle(null) val disabledLabel = stringResource(R.string.label_disabled) LazyColumn( @@ -202,5 +203,34 @@ fun VoiceSatelliteSettings( } ) } + item { + Divider() + } + item { + SwitchSetting( + name = stringResource(R.string.label_wake_screen), + description = stringResource(R.string.description_wake_screen), + value = displayState?.wakeScreen ?: false, + enabled = enabled, + onCheckedChange = { + coroutineScope.launch { + viewModel.saveWakeScreen(it) + } + } + ) + } + item { + SwitchSetting( + name = stringResource(R.string.label_hide_system_bars), + description = stringResource(R.string.description_hide_system_bars), + value = displayState?.hideSystemBars ?: false, + enabled = enabled, + onCheckedChange = { + coroutineScope.launch { + viewModel.saveHideSystemBars(it) + } + } + ) + } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/example/ava/ui/services/ServiceViewModel.kt b/app/src/main/java/com/example/ava/ui/services/ServiceViewModel.kt index 155e758..f3c9ef7 100644 --- a/app/src/main/java/com/example/ava/ui/services/ServiceViewModel.kt +++ b/app/src/main/java/com/example/ava/ui/services/ServiceViewModel.kt @@ -24,7 +24,7 @@ import javax.inject.Inject @HiltViewModel class ServiceViewModel @Inject constructor( @param:ApplicationContext private val context: Context, - private val settings: VoiceSatelliteSettingsStore + private val settings: VoiceSatelliteSettingsStore, ) : ViewModel() { private var created = false @@ -35,6 +35,10 @@ class ServiceViewModel @Inject constructor( service?.voiceTimers ?: flowOf(emptyList()) } + val satelliteState = _satellite.flatMapLatest { service -> + service?.voiceSatelliteState ?: flowOf(null) + } + private val serviceConnection = bindService(context) { _satellite.value = it } @@ -80,4 +84,4 @@ class ServiceViewModel @Inject constructor( _satellite.dropWhile { it == null }.first()?.startVoiceSatellite() } } -} \ No newline at end of file +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fbc365c..973cf35 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -22,6 +22,10 @@ Specify a different file to play Repeat timer sound If enabled, repeat the sound until you say "stop" + Wake screen on activity + Turn on the screen when a wake word is detected or timers are active + Hide system bars + Run in full screen mode by hiding status and navigation bars Name cannot be empty Port must be between 1 and 65535 From 1af75fead9d21060114b2b63edc2be75694391fb Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Thu, 26 Feb 2026 15:33:18 +0100 Subject: [PATCH 2/2] collect state in parent to limit redraws --- README.md | 2 ++ .../example/ava/ui/screens/home/HomeScreen.kt | 19 ++++++---- .../screens/home/components/HideSystemBars.kt | 29 +++++++-------- .../components/WakeScreenOnInteraction.kt | 35 ++++++++----------- .../settings/VoiceSatelliteSettings.kt | 2 +- 5 files changed, 44 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index c651f50..009e3ed 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,8 @@ Once connected, the satellite is fully configurable from within Home Assistant a - Enable wake sound: Whether to play a sound when the satellite is woken by a wake word - Custom timer sound: Specify an audio file to play instead of the default one - Repeat timer sound: By default, the timer sound is repeated until stopped by the user +- Hide system bars: Run the main screen in full screen mode by hiding status and navigation bars +- Wake screen on activity: Turn on the screen when a wake word is detected or timers are active. Only when the Ava screen is displayed, and the lock screen is disabled # Custom wake word models The app includes a default [set of wake words](https://github.com/brownard/Ava/tree/master/app/src/main/assets/wakeWords), however you can also specify a directory containing custom wake words supported by microWakeWord. diff --git a/app/src/main/java/com/example/ava/ui/screens/home/HomeScreen.kt b/app/src/main/java/com/example/ava/ui/screens/home/HomeScreen.kt index b699544..174deb6 100644 --- a/app/src/main/java/com/example/ava/ui/screens/home/HomeScreen.kt +++ b/app/src/main/java/com/example/ava/ui/screens/home/HomeScreen.kt @@ -1,6 +1,7 @@ package com.example.ava.ui.screens.home import android.content.res.Configuration +import androidx.activity.compose.LocalActivity import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues @@ -37,6 +38,7 @@ import com.example.ava.R import com.example.ava.ui.Settings import com.example.ava.ui.screens.home.components.HideSystemBars import com.example.ava.ui.screens.home.components.WakeScreenOnInteraction +import com.example.ava.ui.services.ServiceViewModel import com.example.ava.ui.services.StartStopVoiceSatellite import com.example.ava.ui.services.components.timerListSection import com.example.ava.ui.services.components.timerState @@ -45,17 +47,22 @@ import com.example.ava.ui.services.components.timerState @Composable fun HomeScreen( navController: NavController, - viewModel: HomeViewModel = hiltViewModel() + viewModel: HomeViewModel = hiltViewModel(), + serviceViewModel: ServiceViewModel = hiltViewModel() ) { + val displaySettings by viewModel.displaySettings.collectAsStateWithLifecycle(null) + val satelliteState by serviceViewModel.satelliteState.collectAsStateWithLifecycle(null) + val timers by serviceViewModel.voiceTimers.collectAsStateWithLifecycle(emptyList()) + + val currentWindow = LocalActivity.current?.window val isLandscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE val timerState = timerState() - val displaySettings by viewModel.displaySettings.collectAsStateWithLifecycle(null) - if (displaySettings?.wakeScreen == true) { - WakeScreenOnInteraction() + if (displaySettings?.hideSystemBars == true && currentWindow != null) { + HideSystemBars(currentWindow) } - if (displaySettings?.hideSystemBars == true) { - HideSystemBars() + if (displaySettings?.wakeScreen == true && currentWindow != null) { + WakeScreenOnInteraction(currentWindow, timers, satelliteState) } Scaffold( diff --git a/app/src/main/java/com/example/ava/ui/screens/home/components/HideSystemBars.kt b/app/src/main/java/com/example/ava/ui/screens/home/components/HideSystemBars.kt index 6e98d69..ab7af4e 100644 --- a/app/src/main/java/com/example/ava/ui/screens/home/components/HideSystemBars.kt +++ b/app/src/main/java/com/example/ava/ui/screens/home/components/HideSystemBars.kt @@ -1,6 +1,6 @@ package com.example.ava.ui.screens.home.components -import androidx.activity.compose.LocalActivity +import android.view.Window import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.core.view.WindowCompat @@ -8,23 +8,20 @@ import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat @Composable -fun HideSystemBars() { - val window = LocalActivity.current?.window - if (window != null) { - DisposableEffect("HideSystemBars") { - val insetsController = WindowCompat.getInsetsController(window, window.decorView) +fun HideSystemBars(window: Window) { + DisposableEffect(Unit) { + val insetsController = WindowCompat.getInsetsController(window, window.decorView) - insetsController.apply { - hide(WindowInsetsCompat.Type.systemBars()) - systemBarsBehavior = - WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE - } + insetsController.apply { + hide(WindowInsetsCompat.Type.systemBars()) + systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } - onDispose { - insetsController.apply { - show(WindowInsetsCompat.Type.systemBars()) - systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_DEFAULT - } + onDispose { + insetsController.apply { + show(WindowInsetsCompat.Type.systemBars()) + systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_DEFAULT } } } diff --git a/app/src/main/java/com/example/ava/ui/screens/home/components/WakeScreenOnInteraction.kt b/app/src/main/java/com/example/ava/ui/screens/home/components/WakeScreenOnInteraction.kt index 8a1324f..b3760c7 100644 --- a/app/src/main/java/com/example/ava/ui/screens/home/components/WakeScreenOnInteraction.kt +++ b/app/src/main/java/com/example/ava/ui/screens/home/components/WakeScreenOnInteraction.kt @@ -1,35 +1,30 @@ package com.example.ava.ui.screens.home.components +import android.view.Window import android.view.WindowManager -import androidx.activity.compose.LocalActivity import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.getValue -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.compose.runtime.remember +import com.example.ava.esphome.EspHomeState import com.example.ava.esphome.voicesatellite.Listening import com.example.ava.esphome.voicesatellite.Processing import com.example.ava.esphome.voicesatellite.Responding import com.example.ava.esphome.voicesatellite.VoiceTimer -import com.example.ava.ui.services.ServiceViewModel +import kotlin.collections.contains +private val wakingStates = setOf(Listening, Processing, Responding) @Composable -fun WakeScreenOnInteraction(viewModel: ServiceViewModel = hiltViewModel()) { - val timers by viewModel.voiceTimers.collectAsStateWithLifecycle(emptyList()) - val currentState by viewModel.satelliteState.collectAsStateWithLifecycle(null) - val wakingStates = setOf(Listening, Processing, Responding) - - if (wakingStates.contains(currentState) || timers.any({ it !is VoiceTimer.Paused })) { - val window = LocalActivity.current?.window - if (window != null) { - DisposableEffect("WakeScreenOnInteraction") { - window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - - onDispose { - window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - } +fun WakeScreenOnInteraction(window: Window, timers: List, satelliteState: EspHomeState?) { + val isInteracting = remember(timers, satelliteState) { + wakingStates.contains(satelliteState) || timers.any { it !is VoiceTimer.Paused } + } + if (isInteracting) { + DisposableEffect(Unit) { + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + onDispose { + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/example/ava/ui/screens/settings/VoiceSatelliteSettings.kt b/app/src/main/java/com/example/ava/ui/screens/settings/VoiceSatelliteSettings.kt index d112f1d..d9a355a 100644 --- a/app/src/main/java/com/example/ava/ui/screens/settings/VoiceSatelliteSettings.kt +++ b/app/src/main/java/com/example/ava/ui/screens/settings/VoiceSatelliteSettings.kt @@ -204,7 +204,7 @@ fun VoiceSatelliteSettings( ) } item { - Divider() + HorizontalDivider() } item { SwitchSetting(