Compare commits
No commits in common. "1ee564c7b2b1574bebfe79c5c74d887c79b8c621" and "a3eb9879370968faf63ae739c7e3037cc7da343d" have entirely different histories.
1ee564c7b2
...
a3eb987937
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@ -1 +0,0 @@
|
||||
buy_me_a_coffee: timklge
|
||||
2
.github/workflows/android.yml
vendored
2
.github/workflows/android.yml
vendored
@ -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 }}
|
||||
13
README.md
13
README.md
@ -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>
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
## 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
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,105 +158,89 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS
|
||||
}
|
||||
}
|
||||
|
||||
val triggerJobs = mutableSetOf<Job>()
|
||||
|
||||
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 }
|
||||
.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)
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
return CoroutineScope(Dispatchers.IO).launch {
|
||||
val preferences = streamPreferences()
|
||||
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)
|
||||
))
|
||||
|
||||
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 }
|
||||
.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()
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
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)
|
||||
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 ->
|
||||
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 */
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
@ -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?) {
|
||||
|
||||
@ -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,
|
||||
text = text,
|
||||
displayForegroundColor = selectedColor,
|
||||
isActive = isActive,
|
||||
smoothSetting = smoothSetting,
|
||||
trigger = selectedTrigger,
|
||||
isAutoDismiss = autoDismiss, tone = selectedTone, autoDismissSeconds = autoDismissSeconds.toIntOrNull() ?: 15)
|
||||
}
|
||||
fun getUpdatedReminder(): Reminder = Reminder(reminder.id, title, duration.toIntOrNull() ?: 1,
|
||||
text = text,
|
||||
displayForegroundColor = selectedColor,
|
||||
isActive = isActive,
|
||||
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(
|
||||
|
||||
@ -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.")
|
||||
|
||||
|
||||
@ -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!")))
|
||||
BIN
detail.png
BIN
detail.png
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 26 KiB |
BIN
list.png
BIN
list.png
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 22 KiB |
BIN
reminder.png
BIN
reminder.png
Binary file not shown.
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 28 KiB |
Loading…
x
Reference in New Issue
Block a user