Add profile selection dialog

This commit is contained in:
Tim Kluge 2025-05-29 13:15:24 +02:00
parent e68d4fabca
commit bf500596fb
6 changed files with 155 additions and 34 deletions

View File

@ -7,6 +7,7 @@ import de.timklge.karooreminder.screens.Reminder
import de.timklge.karooreminder.screens.defaultReminders
import de.timklge.karooreminder.screens.preferencesKey
import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.models.ActiveRideProfile
import io.hammerhead.karooext.models.OnStreamState
import io.hammerhead.karooext.models.RideState
import io.hammerhead.karooext.models.StreamState
@ -40,6 +41,17 @@ fun KarooSystemService.streamRideState(): Flow<RideState> {
}
}
fun KarooSystemService.streamActiveRideProfile(): Flow<ActiveRideProfile> {
return callbackFlow {
val listenerId = addConsumer { activeProfile: ActiveRideProfile ->
trySendBlocking(activeProfile)
}
awaitClose {
removeConsumer(listenerId)
}
}
}
fun KarooSystemService.streamUserProfile(): Flow<UserProfile> {
return callbackFlow {
val listenerId = addConsumer { userProfile: UserProfile ->

View File

@ -5,8 +5,10 @@ import android.media.MediaPlayer
import android.util.Log
import de.timklge.karooreminder.screens.Reminder
import de.timklge.karooreminder.screens.ReminderBeepPattern
import de.timklge.karooreminder.screens.reminderIsActive
import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.extension.KarooExtension
import io.hammerhead.karooext.models.ActiveRideProfile
import io.hammerhead.karooext.models.DataType
import io.hammerhead.karooext.models.HardwareType
import io.hammerhead.karooext.models.InRideAlert
@ -231,13 +233,19 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS
val triggerJobs = mutableSetOf<Job>()
data class StreamData(val preferences: MutableList<Reminder>, val activeProfile: ActiveRideProfile)
val streamDataFlow = combine(streamPreferences(), karooSystem.streamActiveRideProfile()) { reminders, activeProfile ->
StreamData(preferences = reminders, activeProfile = activeProfile)
}
triggerStreamJob = CoroutineScope(Dispatchers.IO).launch {
streamPreferences().collect { reminders ->
streamDataFlow.collect { (reminders, activeRideProfile) ->
triggerJobs.forEach { it.cancel() }
triggerJobs.clear()
if (reminders.any { it.trigger == ReminderTrigger.DISTANCE }){
val distanceJob = startIntervalJob(ReminderTrigger.DISTANCE) {
val distanceJob = startIntervalJob(reminders, activeRideProfile, ReminderTrigger.DISTANCE) {
karooSystem.streamDataFlow(DataType.Type.DISTANCE)
.mapNotNull { (it as? StreamState.Streaming)?.dataPoint?.singleValue }
.combine(karooSystem.streamUserProfile()) { distance, profile -> distance to profile }
@ -255,7 +263,7 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS
}
if (reminders.any { it.trigger == ReminderTrigger.ELAPSED_TIME }){
val elapsedTimeJob = startIntervalJob(ReminderTrigger.ELAPSED_TIME) {
val elapsedTimeJob = startIntervalJob(reminders, activeRideProfile, ReminderTrigger.ELAPSED_TIME) {
karooSystem.streamDataFlow(DataType.Type.ELAPSED_TIME)
.mapNotNull { (it as? StreamState.Streaming)?.dataPoint?.singleValue }
.map { (it / 1000 / 60).toInt() }
@ -266,7 +274,7 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS
}
if (reminders.any { it.trigger == ReminderTrigger.ENERGY_OUTPUT }){
val energyOutputJob = startIntervalJob(ReminderTrigger.ENERGY_OUTPUT) {
val energyOutputJob = startIntervalJob(reminders, activeRideProfile, ReminderTrigger.ENERGY_OUTPUT) {
karooSystem.streamDataFlow(DataType.Type.ENERGY_OUTPUT)
.mapNotNull { (it as? StreamState.Streaming)?.dataPoint?.singleValue }
.map { it.toInt() }
@ -301,7 +309,7 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS
intervalTriggers.forEach { trigger ->
SmoothSetting.entries.forEach { smoothSetting ->
if (reminders.any { it.trigger == trigger && it.smoothSetting == smoothSetting }){
val job = startRangeExceededJob(trigger, smoothSetting)
val job = startRangeExceededJob(reminders, activeRideProfile, trigger, smoothSetting)
triggerJobs.add(job)
}
}
@ -310,41 +318,43 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS
}
}
private fun startIntervalJob(trigger: ReminderTrigger, flow: () -> Flow<Int>): Job {
private fun startIntervalJob(preferences: List<Reminder>, activeRideProfile: ActiveRideProfile, trigger: ReminderTrigger, flow: () -> Flow<Int>): Job {
return CoroutineScope(Dispatchers.IO).launch {
val preferences = streamPreferences()
flow()
.filterNot { it == 0 }
.combine(preferences) { elapsedMinutes, reminders -> elapsedMinutes to reminders}
.distinctUntilChanged { old, new -> old.first == new.first }
.collectLatest { (elapsedMinutes, reminders) ->
val rs = reminders
.distinctUntilChanged()
.collectLatest { elapsedMinutes ->
val rs = preferences
.filter { reminder ->
val interval = reminder.interval
reminder.trigger == trigger && reminder.isActive && interval != null && elapsedMinutes % interval == 0
reminder.trigger == trigger && reminderIsActive(reminder, activeRideProfile.profile) && interval != null && elapsedMinutes % interval == 0
}
for (reminder in rs){
for (reminder in rs) {
Log.i(TAG, "$trigger reminder: ${reminder.name}")
reminderChannel.send(DisplayedReminder(reminder.tone, trigger, InRideAlert(
id = "reminder-${reminder.id}-${elapsedMinutes}",
detail = reminder.text,
title = reminder.name,
autoDismissMs = if(reminder.isAutoDismiss) reminder.autoDismissSeconds * 1000L else null,
icon = R.drawable.timer,
textColor = reminder.displayForegroundColor?.getTextColor() ?: R.color.black,
backgroundColor = reminder.displayForegroundColor?.colorRes ?: R.color.hRed
)))
reminderChannel.send(
DisplayedReminder(
reminder.tone, trigger, InRideAlert(
id = "reminder-${reminder.id}-${elapsedMinutes}",
detail = reminder.text,
title = reminder.name,
autoDismissMs = if (reminder.isAutoDismiss) reminder.autoDismissSeconds * 1000L else null,
icon = R.drawable.timer,
textColor = reminder.displayForegroundColor?.getTextColor()
?: R.color.black,
backgroundColor = reminder.displayForegroundColor?.colorRes
?: R.color.hRed
)
)
)
}
}
}
}
private fun startRangeExceededJob(triggerType: ReminderTrigger, smoothSetting: SmoothSetting): Job {
private fun startRangeExceededJob(preferences: MutableList<Reminder>, activeRideProfile: ActiveRideProfile, triggerType: ReminderTrigger, smoothSetting: SmoothSetting): Job {
return CoroutineScope(Dispatchers.IO).launch {
val preferences = streamPreferences()
Log.i(TAG, "Starting range exceeded job for trigger $triggerType with smooth setting $smoothSetting")
val valueStream = karooSystem.streamDataFlow(triggerType.getSmoothedDataType(smoothSetting))
@ -370,9 +380,9 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS
data class StreamData(val value: Double, val reminders: MutableList<Reminder>, val distanceImperial: Boolean, val temperatureImperial: Boolean, val rideState: RideState)
combine(valueStream, preferences, karooSystem.streamUserProfile(), karooSystem.streamRideState()) { value, reminders, profile, rideState ->
combine(valueStream, karooSystem.streamUserProfile(), karooSystem.streamRideState()) { value, profile, rideState ->
StreamData(distanceImperial = profile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL, temperatureImperial = profile.preferredUnit.temperature == UserProfile.PreferredUnit.UnitType.IMPERIAL,
value = value, reminders = reminders, rideState = rideState)
value = value, reminders = preferences, rideState = rideState)
}.filter {
it.rideState is RideState.Recording
}.let {
@ -432,7 +442,7 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS
ReminderTrigger.ELAPSED_TIME, ReminderTrigger.DISTANCE, ReminderTrigger.ENERGY_OUTPUT -> error("Unsupported trigger type: $triggerType")
}
reminder.isActive && reminder.trigger == triggerType && triggerIsMet
reminderIsActive(reminder, activeRideProfile.profile) && reminder.trigger == triggerType && triggerIsMet
}
triggered

View File

@ -103,6 +103,8 @@ fun DetailScreen(isCreating: Boolean, reminder: Reminder, onSubmit: (updatedRemi
var selectedTone by remember { mutableStateOf(reminder.tone) }
var autoDismissSeconds by remember { mutableStateOf(reminder.autoDismissSeconds.toString()) }
var selectedTrigger by remember { mutableStateOf(reminder.trigger) }
var rideProfileDialogVisible by remember { mutableStateOf(false) }
var enabledRideProfiles by remember { mutableStateOf(reminder.enabledRideProfiles.toMutableSet()) }
val profile by karooSystem.streamUserProfile().collectAsStateWithLifecycle(null)
@ -116,7 +118,8 @@ fun DetailScreen(isCreating: Boolean, reminder: Reminder, onSubmit: (updatedRemi
isActive = isActive,
smoothSetting = smoothSetting,
trigger = selectedTrigger,
isAutoDismiss = autoDismiss, tone = selectedTone, autoDismissSeconds = autoDismissSeconds.toIntOrNull() ?: 15)
isAutoDismiss = autoDismiss, tone = selectedTone, autoDismissSeconds = autoDismissSeconds.toIntOrNull() ?: 15,
enabledRideProfiles = enabledRideProfiles.toSet())
}
Column(modifier = Modifier
@ -265,6 +268,16 @@ fun DetailScreen(isCreating: Boolean, reminder: Reminder, onSubmit: (updatedRemi
Text("Is Active")
}
FilledTonalButton(modifier = Modifier
.fillMaxWidth()
.height(60.dp), onClick = {
rideProfileDialogVisible = true
}) {
Icon(Icons.Default.Build, contentDescription = "Change Ride Profiles", modifier = Modifier.size(20.dp))
Spacer(modifier = Modifier.width(5.dp))
Text("Ride Profiles: ${if (enabledRideProfiles.isEmpty()) "All" else enabledRideProfiles.joinToString(", ")}")
}
FilledTonalButton(modifier = Modifier
.fillMaxWidth()
.height(50.dp), onClick = {
@ -310,6 +323,79 @@ fun DetailScreen(isCreating: Boolean, reminder: Reminder, onSubmit: (updatedRemi
title = { Text("Delete reminder") }, text = { Text("Really delete this reminder?") })
}
if (rideProfileDialogVisible) {
var newProfileName by remember { mutableStateOf("") }
Dialog(onDismissRequest = { rideProfileDialogVisible = false }) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp),
shape = RoundedCornerShape(10.dp),
) {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
if (enabledRideProfiles.isEmpty()) {
Text("All profiles enabled")
} else {
Column(modifier = Modifier.fillMaxWidth()) {
enabledRideProfiles.forEach { profileName ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(profileName)
Button(onClick = {
enabledRideProfiles = enabledRideProfiles.toMutableSet().apply { remove(profileName) }
}) {
Icon(Icons.Default.Delete, contentDescription = "Delete profile")
}
}
}
}
}
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
value = newProfileName,
onValueChange = { newProfileName = it },
label = { Text("New profile name") },
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
Button(
onClick = {
if (newProfileName.isNotBlank()) {
enabledRideProfiles = enabledRideProfiles.toMutableSet().apply { add(newProfileName) }
newProfileName = "" // Clear the text field
}
},
modifier = Modifier.fillMaxWidth()
) {
Text("Add Profile")
}
Spacer(modifier = Modifier.height(10.dp))
Button(
onClick = { rideProfileDialogVisible = false },
modifier = Modifier.fillMaxWidth()
) {
Text("Close")
}
}
}
}
}
if (triggerDialogVisible){
Dialog(onDismissRequest = { triggerDialogVisible = false }) {
Card(
@ -429,4 +515,5 @@ fun DetailScreen(isCreating: Boolean, reminder: Reminder, onSubmit: (updatedRemi
}
}
}
}
}

View File

@ -63,6 +63,8 @@ import androidx.navigation.navArgument
import de.timklge.karooreminder.KarooReminderExtension
import de.timklge.karooreminder.R
import de.timklge.karooreminder.dataStore
import de.timklge.karooreminder.streamActiveRideProfile
import de.timklge.karooreminder.streamRideState
import de.timklge.karooreminder.streamUserProfile
import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.models.UserProfile
@ -165,6 +167,7 @@ fun MainScreen(reminders: MutableList<Reminder>, onNavigateToReminder: (r: Remin
var showWarnings by remember { mutableStateOf(false) }
val profile by karooSystem.streamUserProfile().collectAsStateWithLifecycle(null)
val currentRideProfile by karooSystem.streamActiveRideProfile().collectAsStateWithLifecycle(null)
LaunchedEffect(Unit) {
delay(1000L)
@ -197,7 +200,7 @@ fun MainScreen(reminders: MutableList<Reminder>, onNavigateToReminder: (r: Remin
Card(Modifier
.fillMaxWidth()
.height(60.dp)
.alpha(if (reminder.isActive) 1f else 0.6f)
.alpha(if (reminderIsActive(reminder, currentRideProfile?.profile)) 1f else 0.6f)
.clickable { onNavigateToReminder(reminder) }
.padding(5.dp), shape = RoundedCornerShape(corner = CornerSize(10.dp))
) {

View File

@ -7,6 +7,7 @@ import de.timklge.karooreminder.R
import de.timklge.karooreminder.ReminderTrigger
import de.timklge.karooreminder.SmoothSetting
import io.hammerhead.karooext.models.PlayBeepPattern
import io.hammerhead.karooext.models.RideProfile
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@ -141,6 +142,14 @@ class Reminder(val id: Int, var name: String,
val isActive: Boolean = true, val isAutoDismiss: Boolean = true,
val tone: ReminderBeepPattern = ReminderBeepPattern.THREE_TONES_UP,
var trigger: ReminderTrigger = ReminderTrigger.ELAPSED_TIME,
val autoDismissSeconds: Int = 15)
val autoDismissSeconds: Int = 15,
val enabledRideProfiles: Set<String> = emptySet())
val defaultReminders = Json.encodeToString(listOf(Reminder(0, "Drink", 30, text = "Take a sip!")))
val defaultReminders = Json.encodeToString(listOf(Reminder(0, "Drink", 30, text = "Take a sip!")))
fun reminderIsActive(reminder: Reminder, currentRideProfile: RideProfile?): Boolean {
val enabledRideProfiles = reminder.enabledRideProfiles.map { it.lowercase().trim() }
val currentProfileName = currentRideProfile?.name?.lowercase()?.trim()
return reminder.isActive && (currentRideProfile == null || reminder.enabledRideProfiles.isEmpty() || enabledRideProfiles.contains(currentProfileName))
}

View File

@ -22,7 +22,7 @@ compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "
[libraries]
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
color = { module = "com.maxkeppeler.sheets-compose-dialogs:color", version.ref = "color" }
hammerhead-karoo-ext = { group = "io.hammerhead", name = "karoo-ext", version = "1.1.1" }
hammerhead-karoo-ext = { group = "io.hammerhead", name = "karoo-ext", version = "1.1.5" }
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidxCore" }