Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
59 changes: 59 additions & 0 deletions app/src/main/java/com/example/ava/settings/DisplaySettings.kt
Original file line number Diff line number Diff line change
@@ -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<DisplaySettings> {
/**
* Whether to wake the screen when an event occurs.
*/
val wakeScreen: SettingState<Boolean>

/**
* Whether to hide the system bars.
*/
val hideSystemBars: SettingState<Boolean>
}

@Singleton
class DisplaySettingsStoreImpl @Inject constructor(@param:ApplicationContext private val context: Context) :
DisplaySettingsStore, SettingsStoreImpl<DisplaySettings>(
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) }
}
}
27 changes: 24 additions & 3 deletions app/src/main/java/com/example/ava/ui/screens/home/HomeScreen.kt
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -30,20 +31,40 @@ 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.ServiceViewModel
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(),
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()

if (displaySettings?.hideSystemBars == true && currentWindow != null) {
HideSystemBars(currentWindow)
}
if (displaySettings?.wakeScreen == true && currentWindow != null) {
WakeScreenOnInteraction(currentWindow, timers, satelliteState)
}

Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
Expand Down
13 changes: 13 additions & 0 deletions app/src/main/java/com/example/ava/ui/screens/home/HomeViewModel.kt
Original file line number Diff line number Diff line change
@@ -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()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.example.ava.ui.screens.home.components

import android.view.Window
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(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
}

onDispose {
insetsController.apply {
show(WindowInsetsCompat.Type.systemBars())
systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_DEFAULT
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.example.ava.ui.screens.home.components

import android.view.Window
import android.view.WindowManager
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
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 kotlin.collections.contains

private val wakingStates = setOf(Listening, Processing, Responding)

@Composable
fun WakeScreenOnInteraction(window: Window, timers: List<VoiceTimer>, 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)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()

Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -167,4 +179,4 @@ class SettingsViewModel @Inject constructor(
else
null
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -202,5 +203,34 @@ fun VoiceSatelliteSettings(
}
)
}
item {
HorizontalDivider()
}
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)
}
}
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
}
Expand Down Expand Up @@ -80,4 +84,4 @@ class ServiceViewModel @Inject constructor(
_satellite.dropWhile { it == null }.first()?.startVoiceSatellite()
}
}
}
}
4 changes: 4 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
<string name="description_custom_timer_sound_location">Specify a different file to play</string>
<string name="label_timer_sound_repeat">Repeat timer sound</string>
<string name="description_timer_sound_repeat">If enabled, repeat the sound until you say "stop"</string>
<string name="label_wake_screen">Wake screen on activity</string>
<string name="description_wake_screen">Turn on the screen when a wake word is detected or timers are active</string>
<string name="label_hide_system_bars">Hide system bars</string>
<string name="description_hide_system_bars">Run in full screen mode by hiding status and navigation bars</string>

<string name="validation_voice_satellite_name_empty">Name cannot be empty</string>
<string name="validation_voice_satellite_port_invalid">Port must be between 1 and 65535</string>
Expand Down