Compare commits

..

No commits in common. "1ee564c7b2b1574bebfe79c5c74d887c79b8c621" and "a3eb9879370968faf63ae739c7e3037cc7da343d" have entirely different histories.

13 changed files with 204 additions and 408 deletions

1
.github/FUNDING.yml vendored
View File

@ -1 +0,0 @@
buy_me_a_coffee: timklge

View File

@ -53,6 +53,6 @@ jobs:
name: ${{ github.ref_name }} name: ${{ github.ref_name }}
prerelease: false prerelease: false
generateReleaseNotes: true generateReleaseNotes: true
artifacts: app/build/outputs/apk/release/app-release.apk, app/manifest.json, app/karoo-reminder.png, reminder.png, detail.png, list.png artifacts: app/build/outputs/apk/release/app-release.apk, app/manifest.json, app/karoo-reminder.png
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -8,13 +8,24 @@ Karoo extension that displays in-ride alerts based on custom triggers. Reminders
Compatible with Karoo 2 and Karoo 3 devices. Compatible with Karoo 2 and Karoo 3 devices.
<a href="https://www.buymeacoffee.com/timklge" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174"></a>
![Reminder List](list.png) ![Reminder List](list.png)
![Reminder Detail](detail.png) ![Reminder Detail](detail.png)
![Reminder in ride](reminder.png) ![Reminder in ride](reminder.png)
## Installation ## Installation
This extension is available from the extension library on your Karoo device. Find more information on extensions in the [Hammerhead FAQ](https://support.hammerhead.io/hc/en-us/articles/34676015530907-Karoo-OS-Extensions-Library). If you are using a Karoo 3, you can use [Hammerhead's sideloading procedure](https://support.hammerhead.io/hc/en-us/articles/31576497036827-Companion-App-Sideloading) to install the app:
1. Using the browser on your phone, long-press [this download link](https://github.com/timklge/karoo-reminder/releases/latest/download/app-release.apk) and share it with the Hammerhead Companion app.
2. Your karoo should show an info screen about the app now. Press "Install".
If you are using a Karoo 2, you can use manual sideloading:
1. Download the apk from the [releases page](https://github.com/timklge/karoo-reminder/releases) (or build it from source)
2. Set up your Karoo for sideloading. DC Rainmaker has a great [step-by-step guide](https://www.dcrainmaker.com/2021/02/how-to-sideload-android-apps-on-your-hammerhead-karoo-1-karoo-2.html).
3. Install the app by running `adb install app-release.apk`.
## Credits ## Credits

View File

@ -63,20 +63,16 @@ tasks.register("generateManifest") {
doLast { doLast {
val manifestFile = file("$projectDir/manifest.json") val manifestFile = file("$projectDir/manifest.json")
val manifest = mapOf( val manifest = mapOf(
"label" to "Reminder", "label" to "karoo-reminder",
"packageName" to "de.timklge.karooreminder", "packageName" to "de.timklge.karooreminder",
"iconUrl" to "https://github.com/timklge/karoo-reminder/releases/latest/download/karoo-reminder.png", "iconUrl" to "https://github.com/timklge/karoo-reminder/releases/latest/download/karoo-reminder.png",
"latestApkUrl" to "https://github.com/timklge/karoo-reminder/releases/latest/download/app-release.apk", "latestApkUrl" to "https://github.com/timklge/karoo-reminder/releases/latest/download/app-release.apk",
"latestVersion" to android.defaultConfig.versionName, "latestVersion" to android.defaultConfig.versionName,
"latestVersionCode" to android.defaultConfig.versionCode, "latestVersionCode" to android.defaultConfig.versionCode,
"developer" to "github.com/timklge", "developer" to "timklge",
"description" to "Open-source extension that shows in-ride alerts after a given time interval has passed, distance has been traveled or HR / power / speed / cadence range is exceeded", "description" to "Shows in-ride alerts after a given time interval, distance or HR / power / speed / cadence out of range",
"releaseNotes" to "* Add rolling average setting for power triggers", "releaseNotes" to "* Add additional beep patterns for Karoo 3\n" +
"screenshotUrls" to listOf( "* Add touchable back button",
"https://github.com/timklge/karoo-reminder/releases/latest/download/reminder.png",
"https://github.com/timklge/karoo-reminder/releases/latest/download/list.png",
"https://github.com/timklge/karoo-reminder/releases/latest/download/detail.png",
)
) )
val gson = groovy.json.JsonBuilder(manifest).toPrettyString() val gson = groovy.json.JsonBuilder(manifest).toPrettyString()

View File

@ -1,11 +1,5 @@
package de.timklge.karooreminder package de.timklge.karooreminder
import android.content.Context
import android.util.Log
import de.timklge.karooreminder.KarooReminderExtension.Companion.TAG
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.KarooSystemService
import io.hammerhead.karooext.models.OnStreamState import io.hammerhead.karooext.models.OnStreamState
import io.hammerhead.karooext.models.RideState import io.hammerhead.karooext.models.RideState
@ -15,8 +9,6 @@ import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.map
import kotlinx.serialization.json.Json
fun KarooSystemService.streamDataFlow(dataTypeId: String): Flow<StreamState> { fun KarooSystemService.streamDataFlow(dataTypeId: String): Flow<StreamState> {
return callbackFlow { return callbackFlow {
@ -50,16 +42,3 @@ fun KarooSystemService.streamUserProfile(): Flow<UserProfile> {
} }
} }
} }
fun Context.streamPreferences(): Flow<MutableList<Reminder>> {
return applicationContext.dataStore.data.map { remindersJson ->
try {
Json.decodeFromString<MutableList<Reminder>>(
remindersJson[preferencesKey] ?: defaultReminders
)
} catch(e: Throwable){
Log.e(TAG,"Failed to read preferences", e)
mutableListOf()
}
}
}

View File

@ -5,13 +5,14 @@ import android.media.MediaPlayer
import android.util.Log import android.util.Log
import de.timklge.karooreminder.screens.Reminder import de.timklge.karooreminder.screens.Reminder
import de.timklge.karooreminder.screens.ReminderBeepPattern import de.timklge.karooreminder.screens.ReminderBeepPattern
import de.timklge.karooreminder.screens.defaultReminders
import de.timklge.karooreminder.screens.preferencesKey
import io.hammerhead.karooext.KarooSystemService import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.extension.KarooExtension import io.hammerhead.karooext.extension.KarooExtension
import io.hammerhead.karooext.models.DataType import io.hammerhead.karooext.models.DataType
import io.hammerhead.karooext.models.HardwareType import io.hammerhead.karooext.models.HardwareType
import io.hammerhead.karooext.models.InRideAlert import io.hammerhead.karooext.models.InRideAlert
import io.hammerhead.karooext.models.PlayBeepPattern import io.hammerhead.karooext.models.PlayBeepPattern
import io.hammerhead.karooext.models.RideState
import io.hammerhead.karooext.models.StreamState import io.hammerhead.karooext.models.StreamState
import io.hammerhead.karooext.models.TurnScreenOn import io.hammerhead.karooext.models.TurnScreenOn
import io.hammerhead.karooext.models.UserProfile import io.hammerhead.karooext.models.UserProfile
@ -32,19 +33,8 @@ import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
enum class SmoothSetting(val label: String) {
NONE("None"),
SMOOTH_3S("3 seconds"),
SMOOTH_10S("10 seconds"),
SMOOTH_30S("30 seconds"),
SMOOTH_20M("20 minutes"),
SMOOTH_60M("60 minutes"),
SMOOTH_LAP("Lap"),
SMOOTH_RIDE("Ride");
}
enum class ReminderTrigger(val id: String, val label: String) { enum class ReminderTrigger(val id: String, val label: String) {
ELAPSED_TIME("elapsed_time", "Elapsed Time"), ELAPSED_TIME("elapsed_time", "Elapsed Time"),
@ -57,27 +47,14 @@ enum class ReminderTrigger(val id: String, val label: String) {
SPEED_LIMIT_MAXIMUM_EXCEEDED("speed_limit_max", "Speed above value"), SPEED_LIMIT_MAXIMUM_EXCEEDED("speed_limit_max", "Speed above value"),
SPEED_LIMIT_MINIMUM_EXCEEDED("speed_limit_min", "Speed below value"), SPEED_LIMIT_MINIMUM_EXCEEDED("speed_limit_min", "Speed below value"),
CADENCE_LIMIT_MAXIMUM_EXCEEDED("cadence_limit_max", "Cadence above value"), CADENCE_LIMIT_MAXIMUM_EXCEEDED("cadence_limit_max", "Cadence above value"),
CADENCE_LIMIT_MINIMUM_EXCEEDED("cadence_limit_min", "Cadence below value"), CADENCE_LIMIT_MINIMUM_EXCEEDED("cadence_limit_min", "Cadence below value");
AMBIENT_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED("ambient_temperature_limit_max", "Ambient temperature above value"),
AMBIENT_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED("ambient_temperature_limit_min", "Ambient temperature below value"),
GRADIENT_LIMIT_MAXIMUM_EXCEEDED("gradient_limit_max", "Gradient above value"),
GRADIENT_LIMIT_MINIMUM_EXCEEDED("gradient_limit_min", "Gradient below value"),
CORE_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED("core_temperature_limit_max", "Core temperature above value"),
CORE_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED("core_temperature_limit_min", "Core temperature below value"),
FRONT_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED("front_tire_pressure_limit_max", "Front tire pressure above value"),
FRONT_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED("front_tire_pressure_limit_min", "Front tire pressure below value"),
REAR_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED("rear_tire_pressure_limit_max", "Rear tire pressure above value"),
REAR_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED("rear_tire_pressure_limit_min", "Rear tire pressure below value");
fun getPrefix(): String { fun getPrefix(): String {
return when (this) { return when (this) {
HR_LIMIT_MINIMUM_EXCEEDED, POWER_LIMIT_MINIMUM_EXCEEDED, SPEED_LIMIT_MINIMUM_EXCEEDED, CADENCE_LIMIT_MINIMUM_EXCEEDED, CORE_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED, FRONT_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED, REAR_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED, HR_LIMIT_MINIMUM_EXCEEDED, POWER_LIMIT_MINIMUM_EXCEEDED, SPEED_LIMIT_MINIMUM_EXCEEDED, CADENCE_LIMIT_MINIMUM_EXCEEDED -> "<"
AMBIENT_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED, GRADIENT_LIMIT_MINIMUM_EXCEEDED -> "<" HR_LIMIT_MAXIMUM_EXCEEDED, POWER_LIMIT_MAXIMUM_EXCEEDED, SPEED_LIMIT_MAXIMUM_EXCEEDED, CADENCE_LIMIT_MAXIMUM_EXCEEDED -> ">"
ELAPSED_TIME, DISTANCE -> ""
HR_LIMIT_MAXIMUM_EXCEEDED, POWER_LIMIT_MAXIMUM_EXCEEDED, SPEED_LIMIT_MAXIMUM_EXCEEDED, CADENCE_LIMIT_MAXIMUM_EXCEEDED, CORE_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED, FRONT_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED, REAR_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED, ENERGY_OUTPUT -> ""
AMBIENT_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED, GRADIENT_LIMIT_MAXIMUM_EXCEEDED -> ">"
ELAPSED_TIME, DISTANCE, ENERGY_OUTPUT -> ""
} }
} }
@ -89,55 +66,7 @@ enum class ReminderTrigger(val id: String, val label: String) {
POWER_LIMIT_MAXIMUM_EXCEEDED, POWER_LIMIT_MINIMUM_EXCEEDED -> "W" POWER_LIMIT_MAXIMUM_EXCEEDED, POWER_LIMIT_MINIMUM_EXCEEDED -> "W"
SPEED_LIMIT_MAXIMUM_EXCEEDED, SPEED_LIMIT_MINIMUM_EXCEEDED -> if(imperial) "mph" else "km/h" SPEED_LIMIT_MAXIMUM_EXCEEDED, SPEED_LIMIT_MINIMUM_EXCEEDED -> if(imperial) "mph" else "km/h"
CADENCE_LIMIT_MAXIMUM_EXCEEDED, CADENCE_LIMIT_MINIMUM_EXCEEDED -> "rpm" CADENCE_LIMIT_MAXIMUM_EXCEEDED, CADENCE_LIMIT_MINIMUM_EXCEEDED -> "rpm"
CORE_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED, CORE_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED, AMBIENT_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED, AMBIENT_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED -> if(imperial) "°F" else "°C"
FRONT_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED, FRONT_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED, REAR_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED, REAR_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED -> if(imperial) "psi" else "bar"
ENERGY_OUTPUT -> "kJ" ENERGY_OUTPUT -> "kJ"
GRADIENT_LIMIT_MAXIMUM_EXCEEDED, GRADIENT_LIMIT_MINIMUM_EXCEEDED -> "%"
}
}
fun isDecimalValue() = this == CORE_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED || this == CORE_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED ||
this == FRONT_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED || this == FRONT_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED ||
this == REAR_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED || this == REAR_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED ||
this == AMBIENT_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED || this == AMBIENT_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED ||
this == GRADIENT_LIMIT_MAXIMUM_EXCEEDED || this == GRADIENT_LIMIT_MINIMUM_EXCEEDED
fun getSmoothedDataType(smoothSetting: SmoothSetting): String {
return when(this) {
POWER_LIMIT_MAXIMUM_EXCEEDED, POWER_LIMIT_MINIMUM_EXCEEDED -> {
when (smoothSetting) {
SmoothSetting.NONE -> DataType.Type.POWER
SmoothSetting.SMOOTH_3S -> DataType.Type.SMOOTHED_3S_AVERAGE_POWER
SmoothSetting.SMOOTH_10S -> DataType.Type.SMOOTHED_10S_AVERAGE_POWER
SmoothSetting.SMOOTH_30S -> DataType.Type.SMOOTHED_30S_AVERAGE_POWER
SmoothSetting.SMOOTH_20M -> DataType.Type.SMOOTHED_20M_AVERAGE_POWER
SmoothSetting.SMOOTH_60M -> DataType.Type.SMOOTHED_1HR_AVERAGE_POWER
SmoothSetting.SMOOTH_LAP -> DataType.Type.POWER_LAP
SmoothSetting.SMOOTH_RIDE -> DataType.Type.AVERAGE_POWER
}
}
else -> getDataType()
}
}
fun hasSmoothedDataTypes(): Boolean {
return this == POWER_LIMIT_MAXIMUM_EXCEEDED || this == POWER_LIMIT_MINIMUM_EXCEEDED
}
fun getDataType(): String {
return when (this) {
ReminderTrigger.HR_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.HR_LIMIT_MINIMUM_EXCEEDED -> DataType.Type.HEART_RATE
POWER_LIMIT_MAXIMUM_EXCEEDED, POWER_LIMIT_MINIMUM_EXCEEDED -> DataType.Type.SMOOTHED_3S_AVERAGE_POWER
ReminderTrigger.SPEED_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.SPEED_LIMIT_MINIMUM_EXCEEDED -> DataType.Type.SMOOTHED_3S_AVERAGE_SPEED
ReminderTrigger.CADENCE_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.CADENCE_LIMIT_MINIMUM_EXCEEDED -> DataType.Type.SMOOTHED_3S_AVERAGE_CADENCE
ReminderTrigger.CORE_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.CORE_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED -> DataType.Type.CORE_TEMP
ReminderTrigger.FRONT_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.FRONT_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED -> DataType.Type.TIRE_PRESSURE_FRONT
ReminderTrigger.REAR_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.REAR_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED -> DataType.Type.TIRE_PRESSURE_REAR
ReminderTrigger.GRADIENT_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.GRADIENT_LIMIT_MINIMUM_EXCEEDED -> DataType.Type.ELEVATION_GRADE
ReminderTrigger.AMBIENT_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.AMBIENT_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED -> DataType.Type.TEMPERATURE
ReminderTrigger.DISTANCE, ReminderTrigger.ELAPSED_TIME, ReminderTrigger.ENERGY_OUTPUT -> error("Unsupported trigger type: $this")
} }
} }
} }
@ -167,6 +96,8 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS
private lateinit var karooSystem: KarooSystemService private lateinit var karooSystem: KarooSystemService
private var jobs: MutableSet<Job> = mutableSetOf()
data class DisplayedReminder(val beepPattern: ReminderBeepPattern, val trigger: ReminderTrigger, val alert: InRideAlert) data class DisplayedReminder(val beepPattern: ReminderBeepPattern, val trigger: ReminderTrigger, val alert: InRideAlert)
private var reminderChannel = Channel<DisplayedReminder>(2, BufferOverflow.DROP_OLDEST) private var reminderChannel = Channel<DisplayedReminder>(2, BufferOverflow.DROP_OLDEST)
@ -209,9 +140,6 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS
} }
} }
private lateinit var receiveJob: Job
private lateinit var triggerStreamJob: Job
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@ -219,9 +147,10 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS
karooSystem = KarooSystemService(applicationContext) karooSystem = KarooSystemService(applicationContext)
receiveJob = CoroutineScope(Dispatchers.IO).launch { val receiveJob = CoroutineScope(Dispatchers.IO).launch {
receiverWorker() receiverWorker()
} }
jobs.add(receiveJob)
karooSystem.connect { connected -> karooSystem.connect { connected ->
if (connected) { if (connected) {
@ -229,105 +158,89 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS
} }
} }
val triggerJobs = mutableSetOf<Job>() val distanceJob = CoroutineScope(Dispatchers.IO).launch {
val preferences = applicationContext.dataStore.data.map { remindersJson ->
triggerStreamJob = CoroutineScope(Dispatchers.IO).launch { try {
streamPreferences().collect { reminders -> Json.decodeFromString<MutableList<Reminder>>(
triggerJobs.forEach { it.cancel() } remindersJson[preferencesKey] ?: defaultReminders
triggerJobs.clear() )
} catch(e: Throwable){
if (reminders.any { it.trigger == ReminderTrigger.DISTANCE }){ Log.e(TAG,"Failed to read preferences", e)
val distanceJob = startIntervalJob(ReminderTrigger.DISTANCE) { mutableListOf()
karooSystem.streamDataFlow(DataType.Type.DISTANCE)
.mapNotNull { (it as? StreamState.Streaming)?.dataPoint?.singleValue }
.combine(karooSystem.streamUserProfile()) { distance, profile -> distance to profile }
.map { (distance, profile) ->
if (profile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL){
(distance / 1609.344).toInt()
} else {
(distance / 1000.0).toInt()
}
}
.distinctUntilChanged()
.filterNot { it == 0 }
}
triggerJobs.add(distanceJob)
}
if (reminders.any { it.trigger == ReminderTrigger.ELAPSED_TIME }){
val elapsedTimeJob = startIntervalJob(ReminderTrigger.ELAPSED_TIME) {
karooSystem.streamDataFlow(DataType.Type.ELAPSED_TIME)
.mapNotNull { (it as? StreamState.Streaming)?.dataPoint?.singleValue }
.map { (it / 1000 / 60).toInt() }
.distinctUntilChanged()
.filterNot { it == 0 }
}
triggerJobs.add(elapsedTimeJob)
}
if (reminders.any { it.trigger == ReminderTrigger.ENERGY_OUTPUT }){
val energyOutputJob = startIntervalJob(ReminderTrigger.ENERGY_OUTPUT) {
karooSystem.streamDataFlow(DataType.Type.ENERGY_OUTPUT)
.mapNotNull { (it as? StreamState.Streaming)?.dataPoint?.singleValue }
.map { it.toInt() }
.distinctUntilChanged()
.filterNot { it == 0 }
.allIntermediateInts()
}
triggerJobs.add(energyOutputJob)
}
val intervalTriggers = setOf(
ReminderTrigger.POWER_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.POWER_LIMIT_MINIMUM_EXCEEDED,
ReminderTrigger.HR_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.HR_LIMIT_MINIMUM_EXCEEDED,
ReminderTrigger.SPEED_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.SPEED_LIMIT_MINIMUM_EXCEEDED,
ReminderTrigger.CADENCE_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.CADENCE_LIMIT_MINIMUM_EXCEEDED,
ReminderTrigger.AMBIENT_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.AMBIENT_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED,
ReminderTrigger.GRADIENT_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.GRADIENT_LIMIT_MINIMUM_EXCEEDED,
ReminderTrigger.FRONT_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.FRONT_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED,
ReminderTrigger.REAR_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.REAR_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED,
ReminderTrigger.CORE_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.CORE_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED
)
intervalTriggers.forEach { trigger ->
SmoothSetting.entries.forEach { smoothSetting ->
if (reminders.any { it.trigger == trigger && it.smoothSetting == smoothSetting }){
val job = startRangeExceededJob(trigger, smoothSetting)
triggerJobs.add(job)
}
}
} }
} }
karooSystem.streamDataFlow(DataType.Type.DISTANCE)
.mapNotNull { (it as? StreamState.Streaming)?.dataPoint?.singleValue }
.combine(karooSystem.streamUserProfile()) { distance, profile -> distance to profile }
.map { (distance, profile) ->
if (profile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL){
(distance / 1609.344).toInt()
} else {
(distance / 1000.0).toInt()
}
}
.distinctUntilChanged()
.filterNot { it == 0 }
.combine(preferences) { distance, reminders -> distance to reminders}
.distinctUntilChanged { old, new -> old.first == new.first }
.collectLatest { (distance, reminders) ->
val rs = reminders
.filter { reminder -> reminder.trigger == ReminderTrigger.DISTANCE && reminder.isActive && distance % reminder.interval == 0 }
for (reminder in rs){
Log.i(TAG, "Distance reminder: ${reminder.name}")
reminderChannel.send(DisplayedReminder(reminder.tone, ReminderTrigger.DISTANCE, InRideAlert(
id = "reminder-${reminder.id}-${distance}",
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
)))
}
}
} }
} jobs.add(distanceJob)
private fun startIntervalJob(trigger: ReminderTrigger, flow: () -> Flow<Int>): Job { jobs.addAll(listOf(
return CoroutineScope(Dispatchers.IO).launch { startRangeExceededJob(ReminderTrigger.POWER_LIMIT_MAXIMUM_EXCEEDED),
val preferences = streamPreferences() startRangeExceededJob(ReminderTrigger.HR_LIMIT_MAXIMUM_EXCEEDED),
startRangeExceededJob(ReminderTrigger.POWER_LIMIT_MINIMUM_EXCEEDED),
startRangeExceededJob(ReminderTrigger.HR_LIMIT_MINIMUM_EXCEEDED),
startRangeExceededJob(ReminderTrigger.SPEED_LIMIT_MAXIMUM_EXCEEDED),
startRangeExceededJob(ReminderTrigger.SPEED_LIMIT_MINIMUM_EXCEEDED),
startRangeExceededJob(ReminderTrigger.CADENCE_LIMIT_MAXIMUM_EXCEEDED),
startRangeExceededJob(ReminderTrigger.CADENCE_LIMIT_MINIMUM_EXCEEDED)
))
flow() val elapsedTimeJob = CoroutineScope(Dispatchers.IO).launch {
val preferences = applicationContext.dataStore.data.map { remindersJson ->
try {
Json.decodeFromString<MutableList<Reminder>>(
remindersJson[preferencesKey] ?: defaultReminders
)
} catch(e: Throwable){
Log.e(TAG,"Failed to read preferences", e)
mutableListOf()
}
}
karooSystem.streamDataFlow(DataType.Type.ELAPSED_TIME)
.mapNotNull { (it as? StreamState.Streaming)?.dataPoint?.singleValue }
.map { (it / 1000 / 60).toInt() }
.distinctUntilChanged()
.filterNot { it == 0 } .filterNot { it == 0 }
.combine(preferences) { elapsedMinutes, reminders -> elapsedMinutes to reminders} .combine(preferences) { elapsedMinutes, reminders -> elapsedMinutes to reminders}
.distinctUntilChanged { old, new -> old.first == new.first } .distinctUntilChanged { old, new -> old.first == new.first }
.collectLatest { (elapsedMinutes, reminders) -> .collectLatest { (elapsedMinutes, reminders) ->
val rs = reminders val rs = reminders
.filter { reminder -> .filter { reminder -> reminder.trigger == ReminderTrigger.ELAPSED_TIME && reminder.isActive && elapsedMinutes % reminder.interval == 0 }
val interval = reminder.interval
reminder.trigger == trigger && reminder.isActive && interval != null && elapsedMinutes % interval == 0
}
for (reminder in rs){ for (reminder in rs){
Log.i(TAG, "$trigger reminder: ${reminder.name}") Log.i(TAG, "Elapsed time reminder: ${reminder.name}")
reminderChannel.send(DisplayedReminder(reminder.tone, trigger, InRideAlert( reminderChannel.send(DisplayedReminder(reminder.tone, ReminderTrigger.ELAPSED_TIME, InRideAlert(
id = "reminder-${reminder.id}-${elapsedMinutes}", id = "reminder-${reminder.id}-${elapsedMinutes}",
detail = reminder.text, detail = reminder.text,
title = reminder.name, title = reminder.name,
@ -339,97 +252,96 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS
} }
} }
} }
jobs.add(elapsedTimeJob)
val energyOutputJob = CoroutineScope(Dispatchers.IO).launch {
val preferences = applicationContext.dataStore.data.map { remindersJson ->
try {
Json.decodeFromString<MutableList<Reminder>>(
remindersJson[preferencesKey] ?: defaultReminders
)
} catch(e: Throwable){
Log.e(TAG,"Failed to read preferences", e)
mutableListOf()
}
}
karooSystem.streamDataFlow(DataType.Type.ENERGY_OUTPUT)
.mapNotNull { (it as? StreamState.Streaming)?.dataPoint?.singleValue }
.map { it.toInt() }
.distinctUntilChanged()
.filterNot { it == 0 }
.allIntermediateInts()
.combine(preferences) { energyOutput, reminders -> energyOutput to reminders}
.distinctUntilChanged { old, new -> old.first == new.first }
.collectLatest { (energyOutput, reminders) ->
val rs = reminders
.filter { reminder -> reminder.trigger == ReminderTrigger.ENERGY_OUTPUT && reminder.isActive && energyOutput % reminder.interval == 0 }
for (reminder in rs){
Log.i(TAG, "Energy output reminder: ${reminder.name}")
reminderChannel.send(DisplayedReminder(reminder.tone, ReminderTrigger.ENERGY_OUTPUT, InRideAlert(
id = "reminder-${reminder.id}-${energyOutput}",
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
)))
}
}
}
jobs.add(energyOutputJob)
} }
private fun startRangeExceededJob(triggerType: ReminderTrigger, smoothSetting: SmoothSetting): Job { data class StreamData(val value: Double, val reminders: MutableList<Reminder>? = null, val imperial: Boolean = false)
private fun startRangeExceededJob(triggerType: ReminderTrigger): Job {
return CoroutineScope(Dispatchers.IO).launch { return CoroutineScope(Dispatchers.IO).launch {
val preferences = streamPreferences() val preferences = applicationContext.dataStore.data.map { remindersJson ->
try {
Log.i(TAG, "Starting range exceeded job for trigger $triggerType with smooth setting $smoothSetting") Json.decodeFromString<MutableList<Reminder>>(
remindersJson[preferencesKey] ?: defaultReminders
val valueStream = karooSystem.streamDataFlow(triggerType.getSmoothedDataType(smoothSetting)) )
.mapNotNull { } catch (e: Throwable) {
val dataPoint = (it as? StreamState.Streaming)?.dataPoint Log.e(TAG, "Failed to read preferences", e)
mutableListOf()
@Suppress("KotlinConstantConditions")
when (triggerType) {
ReminderTrigger.CORE_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.CORE_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED -> dataPoint?.values?.get(DataType.Field.CORE_TEMP)
ReminderTrigger.FRONT_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.FRONT_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED,
ReminderTrigger.REAR_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.REAR_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED -> dataPoint?.values?.get(DataType.Field.TIRE_PRESSURE)
ReminderTrigger.ELAPSED_TIME, ReminderTrigger.DISTANCE, ReminderTrigger.ENERGY_OUTPUT,
ReminderTrigger.HR_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.HR_LIMIT_MINIMUM_EXCEEDED,
ReminderTrigger.POWER_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.POWER_LIMIT_MINIMUM_EXCEEDED,
ReminderTrigger.SPEED_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.SPEED_LIMIT_MINIMUM_EXCEEDED,
ReminderTrigger.CADENCE_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.CADENCE_LIMIT_MINIMUM_EXCEEDED,
ReminderTrigger.AMBIENT_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.AMBIENT_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED,
ReminderTrigger.GRADIENT_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.GRADIENT_LIMIT_MINIMUM_EXCEEDED -> dataPoint?.singleValue
}
} }
.filter { it != 0.0 } }
data class StreamData(val value: Double, val reminders: MutableList<Reminder>, val distanceImperial: Boolean, val temperatureImperial: Boolean, val rideState: RideState) val dataType = when (triggerType) {
ReminderTrigger.HR_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.HR_LIMIT_MINIMUM_EXCEEDED -> DataType.Type.HEART_RATE
ReminderTrigger.POWER_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.POWER_LIMIT_MINIMUM_EXCEEDED -> DataType.Type.SMOOTHED_3S_AVERAGE_POWER
ReminderTrigger.SPEED_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.SPEED_LIMIT_MINIMUM_EXCEEDED -> DataType.Type.SMOOTHED_3S_AVERAGE_SPEED
ReminderTrigger.CADENCE_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.CADENCE_LIMIT_MINIMUM_EXCEEDED -> DataType.Type.SMOOTHED_3S_AVERAGE_CADENCE
ReminderTrigger.DISTANCE, ReminderTrigger.ELAPSED_TIME, ReminderTrigger.ENERGY_OUTPUT -> error("Unsupported trigger type: $triggerType")
}
combine(valueStream, preferences, karooSystem.streamUserProfile(), karooSystem.streamRideState()) { value, reminders, profile, rideState -> karooSystem.streamDataFlow(dataType)
StreamData(distanceImperial = profile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL, temperatureImperial = profile.preferredUnit.temperature == UserProfile.PreferredUnit.UnitType.IMPERIAL, .mapNotNull { (it as? StreamState.Streaming)?.dataPoint?.singleValue }
value = value, reminders = reminders, rideState = rideState) .filter { it > 0.0 }
}.filter { .combine(preferences) { value, reminders -> StreamData(value, reminders) }
it.rideState is RideState.Recording .combine(karooSystem.streamUserProfile()) { streamData, profile -> streamData.copy(imperial = profile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL) }
}.let { .onlyIfNValuesReceivedWithinTimeframe(5, 1000 * 10) // At least 5 values have been received over the last 10 seconds
@Suppress("KotlinConstantConditions") .map { (value, reminders, imperial) ->
when (triggerType){ val triggered = reminders?.filter { reminder ->
// Tire pressure, gradient and temperature triggers do not require ongoing measurements, as measurement rate is unknown val isSpeedTrigger = triggerType == ReminderTrigger.SPEED_LIMIT_MAXIMUM_EXCEEDED || triggerType == ReminderTrigger.SPEED_LIMIT_MINIMUM_EXCEEDED
ReminderTrigger.REAR_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.REAR_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED, val reminderValue = if (isSpeedTrigger){
ReminderTrigger.FRONT_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.FRONT_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED, // Convert m/s speed to km/h or mph
ReminderTrigger.CORE_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.CORE_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED, if (imperial) reminder.interval * 0.44704 else reminder.interval * 0.277778
ReminderTrigger.GRADIENT_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.GRADIENT_LIMIT_MAXIMUM_EXCEEDED, } else {
ReminderTrigger.AMBIENT_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.AMBIENT_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED -> it reminder.interval.toDouble()
ReminderTrigger.HR_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.HR_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.POWER_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.POWER_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.SPEED_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.SPEED_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.CADENCE_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.CADENCE_LIMIT_MAXIMUM_EXCEEDED -> it.onlyIfNValuesReceivedWithinTimeframe(5, 1000 * 10) // At least 5 values have been received over the last 10 seconds
ReminderTrigger.ELAPSED_TIME, ReminderTrigger.DISTANCE, ReminderTrigger.ENERGY_OUTPUT -> error("Unsupported trigger type: $triggerType")
}
}
.map { (actualValue, reminders, distanceImperial, temperatureImperial) ->
val triggered = reminders.filter { reminder ->
val triggerThreshold = when(triggerType) {
ReminderTrigger.SPEED_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.SPEED_LIMIT_MAXIMUM_EXCEEDED -> {
if (distanceImperial) reminder.interval?.times(0.44704) else reminder.interval?.times(0.277778)
}
ReminderTrigger.CORE_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.CORE_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.AMBIENT_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.AMBIENT_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED -> {
if (temperatureImperial) (reminder.intervalFloat?.minus(32))?.div(1.8) else reminder.intervalFloat
}
ReminderTrigger.FRONT_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.FRONT_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.REAR_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.REAR_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED -> {
if(distanceImperial) reminder.intervalFloat?.times(68.9476) /* psi to mbar */ else reminder.intervalFloat?.times(1000.0) /* bar to mbar */
}
ReminderTrigger.ELAPSED_TIME, ReminderTrigger.DISTANCE,
ReminderTrigger.ENERGY_OUTPUT, ReminderTrigger.HR_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.HR_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.POWER_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.POWER_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.CADENCE_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.CADENCE_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.GRADIENT_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.GRADIENT_LIMIT_MINIMUM_EXCEEDED -> reminder.interval?.toDouble()
} }
val triggerIsMet = when (triggerType){ val triggerIsMet = when (triggerType){
ReminderTrigger.HR_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.POWER_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.HR_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.POWER_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.CADENCE_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.SPEED_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.CADENCE_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.SPEED_LIMIT_MAXIMUM_EXCEEDED -> value > reminderValue
ReminderTrigger.CORE_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.FRONT_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.REAR_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.AMBIENT_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.GRADIENT_LIMIT_MAXIMUM_EXCEEDED -> triggerThreshold != null && actualValue > triggerThreshold
ReminderTrigger.HR_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.POWER_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.HR_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.POWER_LIMIT_MINIMUM_EXCEEDED,
ReminderTrigger.CADENCE_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.SPEED_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.CADENCE_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.SPEED_LIMIT_MINIMUM_EXCEEDED -> value < reminderValue
ReminderTrigger.CORE_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.FRONT_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED,
ReminderTrigger.REAR_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.AMBIENT_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED,
ReminderTrigger.GRADIENT_LIMIT_MINIMUM_EXCEEDED -> triggerThreshold != null && actualValue < triggerThreshold
ReminderTrigger.ELAPSED_TIME, ReminderTrigger.DISTANCE, ReminderTrigger.ENERGY_OUTPUT -> error("Unsupported trigger type: $triggerType") else -> error("Unsupported trigger type: $triggerType")
} }
reminder.isActive && reminder.trigger == triggerType && triggerIsMet reminder.isActive && reminder.trigger == triggerType && triggerIsMet
@ -440,12 +352,10 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS
.filterNotNull() .filterNotNull()
.filter { it.isNotEmpty() } .filter { it.isNotEmpty() }
.throttle(1_000 * 60) // At most once every minute .throttle(1_000 * 60) // At most once every minute
.onCompletion {
Log.i(TAG, "Range exceeded job for trigger $triggerType with smooth setting $smoothSetting completed")
}
.collectLatest { reminders -> .collectLatest { reminders ->
Log.i(TAG, "Triggered range reminder: ${reminders.size} reminders")
reminders.forEach { reminder -> reminders.forEach { reminder ->
Log.d(TAG, "Dispatching reminder: ${reminder.name}")
reminderChannel.send( reminderChannel.send(
DisplayedReminder( DisplayedReminder(
reminder.tone, triggerType, InRideAlert( reminder.tone, triggerType, InRideAlert(
@ -465,8 +375,8 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS
} }
override fun onDestroy() { override fun onDestroy() {
receiveJob.cancel() jobs.forEach { job -> job.cancel() }
triggerStreamJob.cancel() jobs.clear()
karooSystem.disconnect() karooSystem.disconnect()
super.onDestroy() super.onDestroy()

View File

@ -2,21 +2,15 @@ package de.timklge.karooreminder
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.preferencesDataStore import androidx.datastore.preferences.preferencesDataStore
import de.timklge.karooreminder.screens.ReminderAppNavHost import de.timklge.karooreminder.screens.ReminderAppNavHost
import de.timklge.karooreminder.theme.AppTheme import de.timklge.karooreminder.theme.AppTheme
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings", corruptionHandler = ReplaceFileCorruptionHandler { val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
Log.w(KarooReminderExtension.TAG, "Error reading settings, using default values")
emptyPreferences()
})
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {

View File

@ -36,7 +36,6 @@ import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -62,7 +61,6 @@ import com.maxkeppeler.sheets.color.models.MultipleColors
import com.maxkeppeler.sheets.color.models.SingleColor import com.maxkeppeler.sheets.color.models.SingleColor
import de.timklge.karooreminder.R import de.timklge.karooreminder.R
import de.timklge.karooreminder.ReminderTrigger import de.timklge.karooreminder.ReminderTrigger
import de.timklge.karooreminder.SmoothSetting
import de.timklge.karooreminder.streamUserProfile import de.timklge.karooreminder.streamUserProfile
import io.hammerhead.karooext.KarooSystemService import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.models.HardwareType import io.hammerhead.karooext.models.HardwareType
@ -77,47 +75,28 @@ fun DetailScreen(isCreating: Boolean, reminder: Reminder, onSubmit: (updatedRemi
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
karooSystem.connect{} karooSystem.connect{}
} }
DisposableEffect(Unit) {
onDispose {
karooSystem.disconnect()
}
}
var title by remember { mutableStateOf(reminder.name) } var title by remember { mutableStateOf(reminder.name) }
var text by remember { mutableStateOf(reminder.text) } var text by remember { mutableStateOf(reminder.text) }
var selectedColor by remember { mutableStateOf(reminder.displayForegroundColor) } var selectedColor by remember { mutableStateOf(reminder.displayForegroundColor) }
val colorDialogState by remember { mutableStateOf(UseCaseState()) } val colorDialogState by remember { mutableStateOf(UseCaseState()) }
var duration by remember { var duration by remember { mutableStateOf(reminder.interval.toString()) }
mutableStateOf(if (reminder.intervalFloat != null){
java.text.DecimalFormat("#.##").format(reminder.intervalFloat)
} else {
reminder.interval.toString()
})
}
var smoothSetting by remember { mutableStateOf(reminder.smoothSetting) }
var isActive by remember { mutableStateOf(reminder.isActive) } var isActive by remember { mutableStateOf(reminder.isActive) }
var autoDismiss by remember { mutableStateOf(reminder.isAutoDismiss) } var autoDismiss by remember { mutableStateOf(reminder.isAutoDismiss) }
var deleteDialogVisible by remember { mutableStateOf(false) } var deleteDialogVisible by remember { mutableStateOf(false) }
var toneDialogVisible by remember { mutableStateOf(false) } var toneDialogVisible by remember { mutableStateOf(false) }
var triggerDialogVisible by remember { mutableStateOf(false) } var triggerDialogVisible by remember { mutableStateOf(false) }
var smoothSettingDialogVisible by remember { mutableStateOf(false) }
var selectedTone by remember { mutableStateOf(reminder.tone) } var selectedTone by remember { mutableStateOf(reminder.tone) }
var autoDismissSeconds by remember { mutableStateOf(reminder.autoDismissSeconds.toString()) } var autoDismissSeconds by remember { mutableStateOf(reminder.autoDismissSeconds.toString()) }
var selectedTrigger by remember { mutableStateOf(reminder.trigger) } var selectedTrigger by remember { mutableStateOf(reminder.trigger) }
val profile by karooSystem.streamUserProfile().collectAsStateWithLifecycle(null) val profile by karooSystem.streamUserProfile().collectAsStateWithLifecycle(null)
fun getUpdatedReminder(): Reminder { fun getUpdatedReminder(): Reminder = Reminder(reminder.id, title, duration.toIntOrNull() ?: 1,
val durationString = duration.replace(",", ".") text = text,
displayForegroundColor = selectedColor,
return Reminder(reminder.id, title, interval = durationString.toDoubleOrNull()?.toInt() ?: 1, isActive = isActive,
intervalFloat = if (selectedTrigger.isDecimalValue()) durationString.toDoubleOrNull() else null, trigger = selectedTrigger,
text = text, isAutoDismiss = autoDismiss, tone = selectedTone, autoDismissSeconds = autoDismissSeconds.toIntOrNull() ?: 15)
displayForegroundColor = selectedColor,
isActive = isActive,
smoothSetting = smoothSetting,
trigger = selectedTrigger,
isAutoDismiss = autoDismiss, tone = selectedTone, autoDismissSeconds = autoDismissSeconds.toIntOrNull() ?: 15)
}
Column(modifier = Modifier Column(modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -157,37 +136,15 @@ fun DetailScreen(isCreating: Boolean, reminder: Reminder, onSubmit: (updatedRemi
ReminderTrigger.CADENCE_LIMIT_MAXIMUM_EXCEEDED -> Text("Maximum cadence") ReminderTrigger.CADENCE_LIMIT_MAXIMUM_EXCEEDED -> Text("Maximum cadence")
ReminderTrigger.CADENCE_LIMIT_MINIMUM_EXCEEDED -> Text("Minimum cadence") ReminderTrigger.CADENCE_LIMIT_MINIMUM_EXCEEDED -> Text("Minimum cadence")
ReminderTrigger.ENERGY_OUTPUT -> Text("Energy Output") ReminderTrigger.ENERGY_OUTPUT -> Text("Energy Output")
ReminderTrigger.CORE_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED -> Text("Maximum core temp")
ReminderTrigger.CORE_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED -> Text("Minimum core temp")
ReminderTrigger.FRONT_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED -> Text("Max front tire pressure")
ReminderTrigger.FRONT_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED -> Text("Min front tire pressure")
ReminderTrigger.REAR_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED -> Text("Max rear tire pressure")
ReminderTrigger.REAR_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED -> Text("Min rear tire pressure")
ReminderTrigger.AMBIENT_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED -> Text("Maximum temp")
ReminderTrigger.AMBIENT_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED -> Text("Minimum temp")
ReminderTrigger.GRADIENT_LIMIT_MAXIMUM_EXCEEDED -> Text("Maximum gradient")
ReminderTrigger.GRADIENT_LIMIT_MINIMUM_EXCEEDED -> Text("Minimum gradient")
} }
}, },
suffix = { suffix = {
Text(selectedTrigger.getSuffix(profile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL)) Text(selectedTrigger.getSuffix(profile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL))
}, },
keyboardOptions = KeyboardOptions(keyboardType = if (selectedTrigger.isDecimalValue()) KeyboardType.Decimal else KeyboardType.Number), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
singleLine = true singleLine = true
) )
if (selectedTrigger.hasSmoothedDataTypes()){
FilledTonalButton(modifier = Modifier
.fillMaxWidth()
.height(60.dp), onClick = {
smoothSettingDialogVisible = true
}) {
Icon(Icons.Default.Build, contentDescription = "Change Smooth Setting", modifier = Modifier.size(20.dp))
Spacer(modifier = Modifier.width(5.dp))
Text("Average: ${smoothSetting.label}")
}
}
ColorDialog( ColorDialog(
state = colorDialogState, state = colorDialogState,
selection = ColorSelection( selection = ColorSelection(
@ -354,41 +311,6 @@ fun DetailScreen(isCreating: Boolean, reminder: Reminder, onSubmit: (updatedRemi
} }
} }
if (smoothSettingDialogVisible) {
Dialog(onDismissRequest = { smoothSettingDialogVisible = false }) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp),
shape = RoundedCornerShape(10.dp),
) {
Column(modifier = Modifier
.padding(5.dp)
.verticalScroll(rememberScrollState())
.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(10.dp)) {
SmoothSetting.entries.forEach { setting ->
Row(modifier = Modifier
.fillMaxWidth()
.clickable {
smoothSetting = setting
smoothSettingDialogVisible = false
}, verticalAlignment = Alignment.CenterVertically) {
RadioButton(selected = smoothSetting == setting, onClick = {
smoothSetting = setting
smoothSettingDialogVisible = false
})
Text(
text = setting.label,
modifier = Modifier.padding(start = 10.dp)
)
}
}
}
}
}
}
if (toneDialogVisible){ if (toneDialogVisible){
Dialog(onDismissRequest = { toneDialogVisible = false }) { Dialog(onDismissRequest = { toneDialogVisible = false }) {
Card( Card(

View File

@ -33,7 +33,6 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
@ -48,7 +47,6 @@ import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.edit
@ -136,7 +134,7 @@ fun ReminderAppNavHost(modifier: Modifier = Modifier, navController: NavHostCont
} }
composable(route = "create") { composable(route = "create") {
val nextReminderId = reminders.maxOfOrNull { it.id + 1 } ?: 0 val nextReminderId = reminders.maxOfOrNull { it.id + 1 } ?: 0
val newReminder = Reminder(nextReminderId, "", 30, text = "") val newReminder = Reminder(nextReminderId, "", 30, "")
DetailScreen(true, newReminder, { updatedReminder -> DetailScreen(true, newReminder, { updatedReminder ->
updatedReminder?.let { r -> updatedReminder?.let { r ->
@ -171,18 +169,6 @@ fun MainScreen(reminders: MutableList<Reminder>, onNavigateToReminder: (r: Remin
showWarnings = true showWarnings = true
} }
LaunchedEffect(Unit) {
karooSystem.connect { connected ->
karooConnected = connected
}
}
DisposableEffect(Unit) {
onDispose {
karooSystem.disconnect()
}
}
Scaffold( Scaffold(
topBar = { TopAppBar(title = {Text("Reminder")}) }, topBar = { TopAppBar(title = {Text("Reminder")}) },
content = { content = {
@ -213,14 +199,21 @@ fun MainScreen(reminders: MutableList<Reminder>, onNavigateToReminder: (r: Remin
Spacer(modifier = Modifier.width(10.dp)) Spacer(modifier = Modifier.width(10.dp))
Text(modifier = Modifier.weight(1.0f), text = reminder.name, maxLines = 1, overflow = TextOverflow.Ellipsis) Text(reminder.name)
val value = if (reminder.trigger.isDecimalValue()) java.text.DecimalFormat("#.##").format(reminder.intervalFloat) else reminder.interval Spacer(Modifier.weight(1.0f))
Text("${reminder.trigger.getPrefix()}${value}${reminder.trigger.getSuffix(profile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL)}")
Text("${reminder.trigger.getPrefix()}${reminder.interval}${reminder.trigger.getSuffix(profile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL)}")
} }
} }
} }
LaunchedEffect(Unit) {
karooSystem.connect { connected ->
karooConnected = connected
}
}
if (showWarnings){ if (showWarnings){
if (reminders.isEmpty()) Text(modifier = Modifier.padding(5.dp), text = "No reminders added.") if (reminders.isEmpty()) Text(modifier = Modifier.padding(5.dp), text = "No reminders added.")

View File

@ -5,7 +5,6 @@ import androidx.annotation.ColorRes
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import de.timklge.karooreminder.R import de.timklge.karooreminder.R
import de.timklge.karooreminder.ReminderTrigger import de.timklge.karooreminder.ReminderTrigger
import de.timklge.karooreminder.SmoothSetting
import io.hammerhead.karooext.models.PlayBeepPattern import io.hammerhead.karooext.models.PlayBeepPattern
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
@ -127,14 +126,7 @@ enum class ReminderColor(@ColorRes val colorRes: Int, val whiteFont: Boolean, va
} }
@Serializable @Serializable
class Reminder(val id: Int, var name: String, class Reminder(val id: Int, var name: String, var interval: Int, var text: String,
/** Trigger value used by all triggers except temperature, gradient, tire pressure */
var interval: Int? = null,
/** Trigger value used by temperature, gradient, tire pressure triggers */
var intervalFloat: Double? = null,
/** Smooth interval used by power, speed triggers */
var smoothSetting: SmoothSetting = SmoothSetting.SMOOTH_3S,
var text: String,
var displayForegroundColor: ReminderColor? = null, var displayForegroundColor: ReminderColor? = null,
@Deprecated("Use displayForegroundColor instead") @Deprecated("Use displayForegroundColor instead")
var foregroundColor: Int = android.graphics.Color.parseColor("#FF6060"), var foregroundColor: Int = android.graphics.Color.parseColor("#FF6060"),
@ -143,4 +135,4 @@ class Reminder(val id: Int, var name: String,
var trigger: ReminderTrigger = ReminderTrigger.ELAPSED_TIME, var trigger: ReminderTrigger = ReminderTrigger.ELAPSED_TIME,
val autoDismissSeconds: Int = 15) val autoDismissSeconds: Int = 15)
val defaultReminders = Json.encodeToString(listOf(Reminder(0, "Drink", 30, text = "Take a sip!"))) val defaultReminders = Json.encodeToString(listOf(Reminder(0, "Drink", 30, "Take a sip!")))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 26 KiB

BIN
list.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 28 KiB