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 }}
prerelease: false
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:
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.
<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 Detail](detail.png)
![Reminder in ride](reminder.png)
## 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

View File

@ -63,20 +63,16 @@ tasks.register("generateManifest") {
doLast {
val manifestFile = file("$projectDir/manifest.json")
val manifest = mapOf(
"label" to "Reminder",
"label" to "karoo-reminder",
"packageName" to "de.timklge.karooreminder",
"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",
"latestVersion" to android.defaultConfig.versionName,
"latestVersionCode" to android.defaultConfig.versionCode,
"developer" to "github.com/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",
"releaseNotes" to "* Add rolling average setting for power triggers",
"screenshotUrls" to listOf(
"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",
)
"developer" to "timklge",
"description" to "Shows in-ride alerts after a given time interval, distance or HR / power / speed / cadence out of range",
"releaseNotes" to "* Add additional beep patterns for Karoo 3\n" +
"* Add touchable back button",
)
val gson = groovy.json.JsonBuilder(manifest).toPrettyString()

View File

@ -1,11 +1,5 @@
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.models.OnStreamState
import io.hammerhead.karooext.models.RideState
@ -15,8 +9,6 @@ import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.map
import kotlinx.serialization.json.Json
fun KarooSystemService.streamDataFlow(dataTypeId: String): Flow<StreamState> {
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 de.timklge.karooreminder.screens.Reminder
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.extension.KarooExtension
import io.hammerhead.karooext.models.DataType
import io.hammerhead.karooext.models.HardwareType
import io.hammerhead.karooext.models.InRideAlert
import io.hammerhead.karooext.models.PlayBeepPattern
import io.hammerhead.karooext.models.RideState
import io.hammerhead.karooext.models.StreamState
import io.hammerhead.karooext.models.TurnScreenOn
import io.hammerhead.karooext.models.UserProfile
@ -32,19 +33,8 @@ import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.launch
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");
}
import kotlinx.serialization.json.Json
enum class ReminderTrigger(val id: String, val label: String) {
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_MINIMUM_EXCEEDED("speed_limit_min", "Speed below value"),
CADENCE_LIMIT_MAXIMUM_EXCEEDED("cadence_limit_max", "Cadence above 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");
CADENCE_LIMIT_MINIMUM_EXCEEDED("cadence_limit_min", "Cadence below value");
fun getPrefix(): String {
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,
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, CORE_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED, FRONT_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED, REAR_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED,
AMBIENT_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED, GRADIENT_LIMIT_MAXIMUM_EXCEEDED -> ">"
ELAPSED_TIME, DISTANCE, ENERGY_OUTPUT -> ""
HR_LIMIT_MINIMUM_EXCEEDED, POWER_LIMIT_MINIMUM_EXCEEDED, SPEED_LIMIT_MINIMUM_EXCEEDED, CADENCE_LIMIT_MINIMUM_EXCEEDED -> "<"
HR_LIMIT_MAXIMUM_EXCEEDED, POWER_LIMIT_MAXIMUM_EXCEEDED, SPEED_LIMIT_MAXIMUM_EXCEEDED, CADENCE_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"
SPEED_LIMIT_MAXIMUM_EXCEEDED, SPEED_LIMIT_MINIMUM_EXCEEDED -> if(imperial) "mph" else "km/h"
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"
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 var jobs: MutableSet<Job> = mutableSetOf()
data class DisplayedReminder(val beepPattern: ReminderBeepPattern, val trigger: ReminderTrigger, val alert: InRideAlert)
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() {
super.onCreate()
@ -219,9 +147,10 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS
karooSystem = KarooSystemService(applicationContext)
receiveJob = CoroutineScope(Dispatchers.IO).launch {
val receiveJob = CoroutineScope(Dispatchers.IO).launch {
receiverWorker()
}
jobs.add(receiveJob)
karooSystem.connect { connected ->
if (connected) {
@ -229,15 +158,18 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS
}
}
val triggerJobs = mutableSetOf<Job>()
val distanceJob = 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()
}
}
triggerStreamJob = CoroutineScope(Dispatchers.IO).launch {
streamPreferences().collect { reminders ->
triggerJobs.forEach { it.cancel() }
triggerJobs.clear()
if (reminders.any { it.trigger == ReminderTrigger.DISTANCE }){
val distanceJob = startIntervalJob(ReminderTrigger.DISTANCE) {
karooSystem.streamDataFlow(DataType.Type.DISTANCE)
.mapNotNull { (it as? StreamState.Streaming)?.dataPoint?.singleValue }
.combine(karooSystem.streamUserProfile()) { distance, profile -> distance to profile }
@ -250,84 +182,65 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS
}
.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)
jobs.addAll(listOf(
startRangeExceededJob(ReminderTrigger.POWER_LIMIT_MAXIMUM_EXCEEDED),
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)
))
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()
}
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)
}
}
}
}
}
}
private fun startIntervalJob(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
.filter { reminder ->
val interval = reminder.interval
reminder.trigger == trigger && reminder.isActive && interval != null && elapsedMinutes % interval == 0
}
.filter { reminder -> reminder.trigger == ReminderTrigger.ELAPSED_TIME && reminder.isActive && elapsedMinutes % reminder.interval == 0 }
for (reminder in rs){
Log.i(TAG, "$trigger reminder: ${reminder.name}")
reminderChannel.send(DisplayedReminder(reminder.tone, trigger, InRideAlert(
Log.i(TAG, "Elapsed time reminder: ${reminder.name}")
reminderChannel.send(DisplayedReminder(reminder.tone, ReminderTrigger.ELAPSED_TIME, InRideAlert(
id = "reminder-${reminder.id}-${elapsedMinutes}",
detail = reminder.text,
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()
}
}
private fun startRangeExceededJob(triggerType: ReminderTrigger, smoothSetting: SmoothSetting): Job {
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)
}
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 {
val preferences = streamPreferences()
Log.i(TAG, "Starting range exceeded job for trigger $triggerType with smooth setting $smoothSetting")
val valueStream = karooSystem.streamDataFlow(triggerType.getSmoothedDataType(smoothSetting))
.mapNotNull {
val dataPoint = (it as? StreamState.Streaming)?.dataPoint
@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
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()
}
}
.filter { it != 0.0 }
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 ->
StreamData(distanceImperial = profile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL, temperatureImperial = profile.preferredUnit.temperature == UserProfile.PreferredUnit.UnitType.IMPERIAL,
value = value, reminders = reminders, rideState = rideState)
}.filter {
it.rideState is RideState.Recording
}.let {
@Suppress("KotlinConstantConditions")
when (triggerType){
// Tire pressure, gradient and temperature triggers do not require ongoing measurements, as measurement rate is unknown
ReminderTrigger.REAR_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.REAR_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.FRONT_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.FRONT_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.CORE_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.CORE_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.GRADIENT_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.GRADIENT_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.AMBIENT_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.AMBIENT_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED -> it
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 */
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")
}
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()
karooSystem.streamDataFlow(dataType)
.mapNotNull { (it as? StreamState.Streaming)?.dataPoint?.singleValue }
.filter { it > 0.0 }
.combine(preferences) { value, reminders -> StreamData(value, reminders) }
.combine(karooSystem.streamUserProfile()) { streamData, profile -> streamData.copy(imperial = profile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL) }
.onlyIfNValuesReceivedWithinTimeframe(5, 1000 * 10) // At least 5 values have been received over the last 10 seconds
.map { (value, reminders, imperial) ->
val triggered = reminders?.filter { reminder ->
val isSpeedTrigger = triggerType == ReminderTrigger.SPEED_LIMIT_MAXIMUM_EXCEEDED || triggerType == ReminderTrigger.SPEED_LIMIT_MINIMUM_EXCEEDED
val reminderValue = if (isSpeedTrigger){
// Convert m/s speed to km/h or mph
if (imperial) reminder.interval * 0.44704 else reminder.interval * 0.277778
} else {
reminder.interval.toDouble()
}
val triggerIsMet = when (triggerType){
ReminderTrigger.HR_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.POWER_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.CADENCE_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.SPEED_LIMIT_MAXIMUM_EXCEEDED,
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.CADENCE_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.SPEED_LIMIT_MAXIMUM_EXCEEDED -> value > reminderValue
ReminderTrigger.HR_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.POWER_LIMIT_MINIMUM_EXCEEDED,
ReminderTrigger.CADENCE_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.SPEED_LIMIT_MINIMUM_EXCEEDED,
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.CADENCE_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.SPEED_LIMIT_MINIMUM_EXCEEDED -> value < reminderValue
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
@ -440,12 +352,10 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS
.filterNotNull()
.filter { it.isNotEmpty() }
.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 ->
Log.i(TAG, "Triggered range reminder: ${reminders.size} reminders")
reminders.forEach { reminder ->
Log.d(TAG, "Dispatching reminder: ${reminder.name}")
reminderChannel.send(
DisplayedReminder(
reminder.tone, triggerType, InRideAlert(
@ -465,8 +375,8 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS
}
override fun onDestroy() {
receiveJob.cancel()
triggerStreamJob.cancel()
jobs.forEach { job -> job.cancel() }
jobs.clear()
karooSystem.disconnect()
super.onDestroy()

View File

@ -2,21 +2,15 @@ package de.timklge.karooreminder
import android.content.Context
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.datastore.core.DataStore
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.preferencesDataStore
import de.timklge.karooreminder.screens.ReminderAppNavHost
import de.timklge.karooreminder.theme.AppTheme
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings", corruptionHandler = ReplaceFileCorruptionHandler {
Log.w(KarooReminderExtension.TAG, "Error reading settings, using default values")
emptyPreferences()
})
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
class MainActivity : ComponentActivity() {
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.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -62,7 +61,6 @@ import com.maxkeppeler.sheets.color.models.MultipleColors
import com.maxkeppeler.sheets.color.models.SingleColor
import de.timklge.karooreminder.R
import de.timklge.karooreminder.ReminderTrigger
import de.timklge.karooreminder.SmoothSetting
import de.timklge.karooreminder.streamUserProfile
import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.models.HardwareType
@ -77,47 +75,28 @@ fun DetailScreen(isCreating: Boolean, reminder: Reminder, onSubmit: (updatedRemi
LaunchedEffect(Unit) {
karooSystem.connect{}
}
DisposableEffect(Unit) {
onDispose {
karooSystem.disconnect()
}
}
var title by remember { mutableStateOf(reminder.name) }
var text by remember { mutableStateOf(reminder.text) }
var selectedColor by remember { mutableStateOf(reminder.displayForegroundColor) }
val colorDialogState by remember { mutableStateOf(UseCaseState()) }
var duration by remember {
mutableStateOf(if (reminder.intervalFloat != null){
java.text.DecimalFormat("#.##").format(reminder.intervalFloat)
} else {
reminder.interval.toString()
})
}
var smoothSetting by remember { mutableStateOf(reminder.smoothSetting) }
var duration by remember { mutableStateOf(reminder.interval.toString()) }
var isActive by remember { mutableStateOf(reminder.isActive) }
var autoDismiss by remember { mutableStateOf(reminder.isAutoDismiss) }
var deleteDialogVisible by remember { mutableStateOf(false) }
var toneDialogVisible by remember { mutableStateOf(false) }
var triggerDialogVisible by remember { mutableStateOf(false) }
var smoothSettingDialogVisible by remember { mutableStateOf(false) }
var selectedTone by remember { mutableStateOf(reminder.tone) }
var autoDismissSeconds by remember { mutableStateOf(reminder.autoDismissSeconds.toString()) }
var selectedTrigger by remember { mutableStateOf(reminder.trigger) }
val profile by karooSystem.streamUserProfile().collectAsStateWithLifecycle(null)
fun getUpdatedReminder(): Reminder {
val durationString = duration.replace(",", ".")
return Reminder(reminder.id, title, interval = durationString.toDoubleOrNull()?.toInt() ?: 1,
intervalFloat = if (selectedTrigger.isDecimalValue()) durationString.toDoubleOrNull() else null,
fun getUpdatedReminder(): Reminder = Reminder(reminder.id, title, duration.toIntOrNull() ?: 1,
text = text,
displayForegroundColor = selectedColor,
isActive = isActive,
smoothSetting = smoothSetting,
trigger = selectedTrigger,
isAutoDismiss = autoDismiss, tone = selectedTone, autoDismissSeconds = autoDismissSeconds.toIntOrNull() ?: 15)
}
Column(modifier = Modifier
.fillMaxSize()
@ -157,37 +136,15 @@ fun DetailScreen(isCreating: Boolean, reminder: Reminder, onSubmit: (updatedRemi
ReminderTrigger.CADENCE_LIMIT_MAXIMUM_EXCEEDED -> Text("Maximum cadence")
ReminderTrigger.CADENCE_LIMIT_MINIMUM_EXCEEDED -> Text("Minimum cadence")
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 = {
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
)
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(
state = colorDialogState,
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){
Dialog(onDismissRequest = { toneDialogVisible = false }) {
Card(

View File

@ -33,7 +33,6 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
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.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.datastore.preferences.core.edit
@ -136,7 +134,7 @@ fun ReminderAppNavHost(modifier: Modifier = Modifier, navController: NavHostCont
}
composable(route = "create") {
val nextReminderId = reminders.maxOfOrNull { it.id + 1 } ?: 0
val newReminder = Reminder(nextReminderId, "", 30, text = "")
val newReminder = Reminder(nextReminderId, "", 30, "")
DetailScreen(true, newReminder, { updatedReminder ->
updatedReminder?.let { r ->
@ -171,18 +169,6 @@ fun MainScreen(reminders: MutableList<Reminder>, onNavigateToReminder: (r: Remin
showWarnings = true
}
LaunchedEffect(Unit) {
karooSystem.connect { connected ->
karooConnected = connected
}
}
DisposableEffect(Unit) {
onDispose {
karooSystem.disconnect()
}
}
Scaffold(
topBar = { TopAppBar(title = {Text("Reminder")}) },
content = {
@ -213,14 +199,21 @@ fun MainScreen(reminders: MutableList<Reminder>, onNavigateToReminder: (r: Remin
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
Text("${reminder.trigger.getPrefix()}${value}${reminder.trigger.getSuffix(profile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL)}")
Spacer(Modifier.weight(1.0f))
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 (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 de.timklge.karooreminder.R
import de.timklge.karooreminder.ReminderTrigger
import de.timklge.karooreminder.SmoothSetting
import io.hammerhead.karooext.models.PlayBeepPattern
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
@ -127,14 +126,7 @@ enum class ReminderColor(@ColorRes val colorRes: Int, val whiteFont: Boolean, va
}
@Serializable
class Reminder(val id: Int, var name: 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,
class Reminder(val id: Int, var name: String, var interval: Int, var text: String,
var displayForegroundColor: ReminderColor? = null,
@Deprecated("Use displayForegroundColor instead")
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,
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