Compare commits
10 Commits
a3eb987937
...
1ee564c7b2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ee564c7b2 | ||
|
|
70d902ed2c | ||
|
|
810ffad69d | ||
|
|
04820cf120 | ||
|
|
24cada2cad | ||
| dcde90981d | |||
|
|
f6085b09fd | ||
|
|
c8d77009fb | ||
|
|
a085d97ead | ||
|
|
349cabca05 |
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
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 }}
|
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
|
artifacts: app/build/outputs/apk/release/app-release.apk, app/manifest.json, app/karoo-reminder.png, reminder.png, detail.png, list.png
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
13
README.md
13
README.md
@ -8,24 +8,13 @@ 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>
|
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
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:
|
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).
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|||||||
@ -63,16 +63,20 @@ 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 "karoo-reminder",
|
"label" to "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 "timklge",
|
"developer" to "github.com/timklge",
|
||||||
"description" to "Shows in-ride alerts after a given time interval, distance or HR / power / speed / cadence out of range",
|
"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 additional beep patterns for Karoo 3\n" +
|
"releaseNotes" to "* Add rolling average setting for power triggers",
|
||||||
"* Add touchable back button",
|
"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",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
val gson = groovy.json.JsonBuilder(manifest).toPrettyString()
|
val gson = groovy.json.JsonBuilder(manifest).toPrettyString()
|
||||||
|
|||||||
@ -1,5 +1,11 @@
|
|||||||
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
|
||||||
@ -9,6 +15,8 @@ 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 {
|
||||||
@ -41,4 +49,17 @@ fun KarooSystemService.streamUserProfile(): Flow<UserProfile> {
|
|||||||
removeConsumer(listenerId)
|
removeConsumer(listenerId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,14 +5,13 @@ 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
|
||||||
@ -33,8 +32,19 @@ 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"),
|
||||||
@ -47,14 +57,27 @@ 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 -> "<"
|
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_MAXIMUM_EXCEEDED, POWER_LIMIT_MAXIMUM_EXCEEDED, SPEED_LIMIT_MAXIMUM_EXCEEDED, CADENCE_LIMIT_MAXIMUM_EXCEEDED -> ">"
|
AMBIENT_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED, GRADIENT_LIMIT_MINIMUM_EXCEEDED -> "<"
|
||||||
ELAPSED_TIME, DISTANCE -> ""
|
|
||||||
ENERGY_OUTPUT -> ""
|
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 -> ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,7 +89,55 @@ 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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -96,8 +167,6 @@ 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)
|
||||||
@ -140,6 +209,9 @@ 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()
|
||||||
|
|
||||||
@ -147,10 +219,9 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS
|
|||||||
|
|
||||||
karooSystem = KarooSystemService(applicationContext)
|
karooSystem = KarooSystemService(applicationContext)
|
||||||
|
|
||||||
val receiveJob = CoroutineScope(Dispatchers.IO).launch {
|
receiveJob = CoroutineScope(Dispatchers.IO).launch {
|
||||||
receiverWorker()
|
receiverWorker()
|
||||||
}
|
}
|
||||||
jobs.add(receiveJob)
|
|
||||||
|
|
||||||
karooSystem.connect { connected ->
|
karooSystem.connect { connected ->
|
||||||
if (connected) {
|
if (connected) {
|
||||||
@ -158,89 +229,105 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val distanceJob = CoroutineScope(Dispatchers.IO).launch {
|
val triggerJobs = mutableSetOf<Job>()
|
||||||
val preferences = applicationContext.dataStore.data.map { remindersJson ->
|
|
||||||
try {
|
triggerStreamJob = CoroutineScope(Dispatchers.IO).launch {
|
||||||
Json.decodeFromString<MutableList<Reminder>>(
|
streamPreferences().collect { reminders ->
|
||||||
remindersJson[preferencesKey] ?: defaultReminders
|
triggerJobs.forEach { it.cancel() }
|
||||||
)
|
triggerJobs.clear()
|
||||||
} catch(e: Throwable){
|
|
||||||
Log.e(TAG,"Failed to read preferences", e)
|
if (reminders.any { it.trigger == ReminderTrigger.DISTANCE }){
|
||||||
mutableListOf()
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
}
|
||||||
|
|
||||||
jobs.addAll(listOf(
|
private fun startIntervalJob(trigger: ReminderTrigger, flow: () -> Flow<Int>): Job {
|
||||||
startRangeExceededJob(ReminderTrigger.POWER_LIMIT_MAXIMUM_EXCEEDED),
|
return CoroutineScope(Dispatchers.IO).launch {
|
||||||
startRangeExceededJob(ReminderTrigger.HR_LIMIT_MAXIMUM_EXCEEDED),
|
val preferences = streamPreferences()
|
||||||
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 {
|
flow()
|
||||||
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 -> reminder.trigger == ReminderTrigger.ELAPSED_TIME && reminder.isActive && elapsedMinutes % reminder.interval == 0 }
|
.filter { reminder ->
|
||||||
|
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, "Elapsed time reminder: ${reminder.name}")
|
Log.i(TAG, "$trigger reminder: ${reminder.name}")
|
||||||
reminderChannel.send(DisplayedReminder(reminder.tone, ReminderTrigger.ELAPSED_TIME, InRideAlert(
|
reminderChannel.send(DisplayedReminder(reminder.tone, trigger, InRideAlert(
|
||||||
id = "reminder-${reminder.id}-${elapsedMinutes}",
|
id = "reminder-${reminder.id}-${elapsedMinutes}",
|
||||||
detail = reminder.text,
|
detail = reminder.text,
|
||||||
title = reminder.name,
|
title = reminder.name,
|
||||||
@ -252,96 +339,97 @@ 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class StreamData(val value: Double, val reminders: MutableList<Reminder>? = null, val imperial: Boolean = false)
|
private fun startRangeExceededJob(triggerType: ReminderTrigger, smoothSetting: SmoothSetting): Job {
|
||||||
|
|
||||||
private fun startRangeExceededJob(triggerType: ReminderTrigger): Job {
|
|
||||||
return CoroutineScope(Dispatchers.IO).launch {
|
return CoroutineScope(Dispatchers.IO).launch {
|
||||||
val preferences = applicationContext.dataStore.data.map { remindersJson ->
|
val preferences = streamPreferences()
|
||||||
try {
|
|
||||||
Json.decodeFromString<MutableList<Reminder>>(
|
Log.i(TAG, "Starting range exceeded job for trigger $triggerType with smooth setting $smoothSetting")
|
||||||
remindersJson[preferencesKey] ?: defaultReminders
|
|
||||||
)
|
val valueStream = karooSystem.streamDataFlow(triggerType.getSmoothedDataType(smoothSetting))
|
||||||
} catch (e: Throwable) {
|
.mapNotNull {
|
||||||
Log.e(TAG, "Failed to read preferences", e)
|
val dataPoint = (it as? StreamState.Streaming)?.dataPoint
|
||||||
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 }
|
||||||
|
|
||||||
val dataType = when (triggerType) {
|
data class StreamData(val value: Double, val reminders: MutableList<Reminder>, val distanceImperial: Boolean, val temperatureImperial: Boolean, val rideState: RideState)
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
karooSystem.streamDataFlow(dataType)
|
combine(valueStream, preferences, karooSystem.streamUserProfile(), karooSystem.streamRideState()) { value, reminders, profile, rideState ->
|
||||||
.mapNotNull { (it as? StreamState.Streaming)?.dataPoint?.singleValue }
|
StreamData(distanceImperial = profile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL, temperatureImperial = profile.preferredUnit.temperature == UserProfile.PreferredUnit.UnitType.IMPERIAL,
|
||||||
.filter { it > 0.0 }
|
value = value, reminders = reminders, rideState = rideState)
|
||||||
.combine(preferences) { value, reminders -> StreamData(value, reminders) }
|
}.filter {
|
||||||
.combine(karooSystem.streamUserProfile()) { streamData, profile -> streamData.copy(imperial = profile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL) }
|
it.rideState is RideState.Recording
|
||||||
.onlyIfNValuesReceivedWithinTimeframe(5, 1000 * 10) // At least 5 values have been received over the last 10 seconds
|
}.let {
|
||||||
.map { (value, reminders, imperial) ->
|
@Suppress("KotlinConstantConditions")
|
||||||
val triggered = reminders?.filter { reminder ->
|
when (triggerType){
|
||||||
val isSpeedTrigger = triggerType == ReminderTrigger.SPEED_LIMIT_MAXIMUM_EXCEEDED || triggerType == ReminderTrigger.SPEED_LIMIT_MINIMUM_EXCEEDED
|
// Tire pressure, gradient and temperature triggers do not require ongoing measurements, as measurement rate is unknown
|
||||||
val reminderValue = if (isSpeedTrigger){
|
ReminderTrigger.REAR_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.REAR_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED,
|
||||||
// Convert m/s speed to km/h or mph
|
ReminderTrigger.FRONT_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.FRONT_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED,
|
||||||
if (imperial) reminder.interval * 0.44704 else reminder.interval * 0.277778
|
ReminderTrigger.CORE_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.CORE_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED,
|
||||||
} else {
|
ReminderTrigger.GRADIENT_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.GRADIENT_LIMIT_MAXIMUM_EXCEEDED,
|
||||||
reminder.interval.toDouble()
|
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
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 -> value > reminderValue
|
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.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 -> value < reminderValue
|
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
|
||||||
|
|
||||||
else -> error("Unsupported trigger type: $triggerType")
|
ReminderTrigger.ELAPSED_TIME, ReminderTrigger.DISTANCE, ReminderTrigger.ENERGY_OUTPUT -> error("Unsupported trigger type: $triggerType")
|
||||||
}
|
}
|
||||||
|
|
||||||
reminder.isActive && reminder.trigger == triggerType && triggerIsMet
|
reminder.isActive && reminder.trigger == triggerType && triggerIsMet
|
||||||
@ -352,10 +440,12 @@ 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(
|
||||||
@ -375,8 +465,8 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
jobs.forEach { job -> job.cancel() }
|
receiveJob.cancel()
|
||||||
jobs.clear()
|
triggerStreamJob.cancel()
|
||||||
|
|
||||||
karooSystem.disconnect()
|
karooSystem.disconnect()
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
|||||||
@ -2,15 +2,21 @@ 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")
|
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings", corruptionHandler = ReplaceFileCorruptionHandler {
|
||||||
|
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?) {
|
||||||
|
|||||||
@ -36,6 +36,7 @@ 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
|
||||||
@ -61,6 +62,7 @@ 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
|
||||||
@ -75,28 +77,47 @@ 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 { mutableStateOf(reminder.interval.toString()) }
|
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 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 = Reminder(reminder.id, title, duration.toIntOrNull() ?: 1,
|
fun getUpdatedReminder(): Reminder {
|
||||||
text = text,
|
val durationString = duration.replace(",", ".")
|
||||||
displayForegroundColor = selectedColor,
|
|
||||||
isActive = isActive,
|
return Reminder(reminder.id, title, interval = durationString.toDoubleOrNull()?.toInt() ?: 1,
|
||||||
trigger = selectedTrigger,
|
intervalFloat = if (selectedTrigger.isDecimalValue()) durationString.toDoubleOrNull() else null,
|
||||||
isAutoDismiss = autoDismiss, tone = selectedTone, autoDismissSeconds = autoDismissSeconds.toIntOrNull() ?: 15)
|
text = text,
|
||||||
|
displayForegroundColor = selectedColor,
|
||||||
|
isActive = isActive,
|
||||||
|
smoothSetting = smoothSetting,
|
||||||
|
trigger = selectedTrigger,
|
||||||
|
isAutoDismiss = autoDismiss, tone = selectedTone, autoDismissSeconds = autoDismissSeconds.toIntOrNull() ?: 15)
|
||||||
|
}
|
||||||
|
|
||||||
Column(modifier = Modifier
|
Column(modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@ -136,15 +157,37 @@ 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 = KeyboardType.Number),
|
keyboardOptions = KeyboardOptions(keyboardType = if (selectedTrigger.isDecimalValue()) KeyboardType.Decimal else 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(
|
||||||
@ -311,6 +354,41 @@ 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(
|
||||||
|
|||||||
@ -33,6 +33,7 @@ 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
|
||||||
@ -47,6 +48,7 @@ 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
|
||||||
@ -134,7 +136,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, "")
|
val newReminder = Reminder(nextReminderId, "", 30, text = "")
|
||||||
|
|
||||||
DetailScreen(true, newReminder, { updatedReminder ->
|
DetailScreen(true, newReminder, { updatedReminder ->
|
||||||
updatedReminder?.let { r ->
|
updatedReminder?.let { r ->
|
||||||
@ -169,6 +171,18 @@ 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 = {
|
||||||
@ -199,21 +213,14 @@ fun MainScreen(reminders: MutableList<Reminder>, onNavigateToReminder: (r: Remin
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.width(10.dp))
|
Spacer(modifier = Modifier.width(10.dp))
|
||||||
|
|
||||||
Text(reminder.name)
|
Text(modifier = Modifier.weight(1.0f), text = reminder.name, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||||
|
|
||||||
Spacer(Modifier.weight(1.0f))
|
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)}")
|
||||||
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.")
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@ 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
|
||||||
@ -126,7 +127,14 @@ enum class ReminderColor(@ColorRes val colorRes: Int, val whiteFont: Boolean, va
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class Reminder(val id: Int, var name: String, var interval: Int, var text: String,
|
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,
|
||||||
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"),
|
||||||
@ -135,4 +143,4 @@ class Reminder(val id: Int, var name: String, var interval: Int, var text: Strin
|
|||||||
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, "Take a sip!")))
|
val defaultReminders = Json.encodeToString(listOf(Reminder(0, "Drink", 30, text = "Take a sip!")))
|
||||||
BIN
detail.png
BIN
detail.png
Binary file not shown.
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 22 KiB |
BIN
list.png
BIN
list.png
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 20 KiB |
BIN
reminder.png
BIN
reminder.png
Binary file not shown.
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 26 KiB |
Loading…
x
Reference in New Issue
Block a user