diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 333e53b..c039496 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -15,8 +15,8 @@ android { applicationId = "de.timklge.karoopowerbar" minSdk = 26 targetSdk = 33 - versionCode = 10 - versionName = "1.3.1" + versionCode = 11 + versionName = "1.3.2" } signingConfigs { diff --git a/app/manifest.json b/app/manifest.json index 95b255d..6614bf7 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -3,9 +3,9 @@ "packageName": "de.timklge.karoopowerbar", "iconUrl": "https://github.com/timklge/karoo-powerbar/releases/latest/download/karoo-powerbar.png", "latestApkUrl": "https://github.com/timklge/karoo-powerbar/releases/latest/download/app-release.apk", - "latestVersion": "1.3.1", - "latestVersionCode": 10, + "latestVersion": "1.3.2", + "latestVersionCode": 11, "developer": "timklge", "description": "Adds a colored power bar to the bottom of the screen", - "releaseNotes": "Add size setting, cadence and speed data sources" + "releaseNotes": "Add size setting, cadence and speed data sources with custom ranges" } \ No newline at end of file diff --git a/app/src/main/kotlin/de/timklge/karoopowerbar/CustomProgressBar.kt b/app/src/main/kotlin/de/timklge/karoopowerbar/CustomProgressBar.kt index 44d473c..718a16a 100644 --- a/app/src/main/kotlin/de/timklge/karoopowerbar/CustomProgressBar.kt +++ b/app/src/main/kotlin/de/timklge/karoopowerbar/CustomProgressBar.kt @@ -24,6 +24,7 @@ class CustomProgressBar @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null ) : View(context, attrs) { var progress: Double = 0.5 + var showValueIfNull: Boolean = false var location: PowerbarLocation = PowerbarLocation.BOTTOM var label: String = "" var showLabel: Boolean = true @@ -104,7 +105,7 @@ class CustomProgressBar @JvmOverloads constructor( canvas.drawRect(0f, 15f, canvas.width.toFloat(), 15f + size.barHeight, backgroundPaint) - if (progress > 0.0) { + if (progress > 0.0 || showValueIfNull) { canvas.drawRoundRect(rect, 2f, 2f, blurPaint) canvas.drawRoundRect(rect, 2f, 2f, linePaint) @@ -140,7 +141,7 @@ class CustomProgressBar @JvmOverloads constructor( canvas.drawRect(0f, canvas.height.toFloat() - size.barHeight, canvas.width.toFloat(), canvas.height.toFloat(), backgroundPaint) - if (progress > 0.0) { + if (progress > 0.0 || showValueIfNull) { canvas.drawRoundRect(rect, 2f, 2f, blurPaint) canvas.drawRoundRect(rect, 2f, 2f, linePaint) diff --git a/app/src/main/kotlin/de/timklge/karoopowerbar/Extensions.kt b/app/src/main/kotlin/de/timklge/karoopowerbar/Extensions.kt index f010141..b5eb8c0 100644 --- a/app/src/main/kotlin/de/timklge/karoopowerbar/Extensions.kt +++ b/app/src/main/kotlin/de/timklge/karoopowerbar/Extensions.kt @@ -1,10 +1,5 @@ package de.timklge.karoopowerbar -import android.content.Context -import android.util.Log -import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.core.stringPreferencesKey -import de.timklge.karoopowerbar.screens.SelectedSource import io.hammerhead.karooext.KarooSystemService import io.hammerhead.karooext.models.OnStreamState import io.hammerhead.karooext.models.RideState @@ -14,49 +9,10 @@ import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map -import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json val jsonWithUnknownKeys = Json { ignoreUnknownKeys = true } -val settingsKey = stringPreferencesKey("settings") - -@Serializable -data class PowerbarSettings( - val source: SelectedSource = SelectedSource.POWER, - val topBarSource: SelectedSource = SelectedSource.NONE, - val onlyShowWhileRiding: Boolean = true, - val showLabelOnBars: Boolean = true, - val useZoneColors: Boolean = true, - val barSize: CustomProgressBarSize = CustomProgressBarSize.MEDIUM -){ - companion object { - val defaultSettings = Json.encodeToString(PowerbarSettings()) - } -} - -suspend fun saveSettings(context: Context, settings: PowerbarSettings) { - context.dataStore.edit { t -> - t[settingsKey] = Json.encodeToString(settings) - } -} - -fun Context.streamSettings(): Flow { - return dataStore.data.map { settingsJson -> - try { - jsonWithUnknownKeys.decodeFromString( - settingsJson[settingsKey] ?: PowerbarSettings.defaultSettings - ) - } catch(e: Throwable){ - Log.e(KarooPowerbarExtension.TAG, "Failed to read preferences", e) - jsonWithUnknownKeys.decodeFromString(PowerbarSettings.defaultSettings) - } - }.distinctUntilChanged() -} - fun KarooSystemService.streamDataFlow(dataTypeId: String): Flow { return callbackFlow { val listenerId = addConsumer(OnStreamState.StartStreaming(dataTypeId)) { event: OnStreamState -> diff --git a/app/src/main/kotlin/de/timklge/karoopowerbar/KarooPowerbarExtension.kt b/app/src/main/kotlin/de/timklge/karoopowerbar/KarooPowerbarExtension.kt index d00518f..bb33283 100644 --- a/app/src/main/kotlin/de/timklge/karoopowerbar/KarooPowerbarExtension.kt +++ b/app/src/main/kotlin/de/timklge/karoopowerbar/KarooPowerbarExtension.kt @@ -10,7 +10,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch -class KarooPowerbarExtension : KarooExtension("karoo-powerbar", "1.3.1") { +class KarooPowerbarExtension : KarooExtension("karoo-powerbar", "1.3.2") { companion object { const val TAG = "karoo-powerbar" diff --git a/app/src/main/kotlin/de/timklge/karoopowerbar/MainActivity.kt b/app/src/main/kotlin/de/timklge/karoopowerbar/MainActivity.kt index 624fd61..6924903 100644 --- a/app/src/main/kotlin/de/timklge/karoopowerbar/MainActivity.kt +++ b/app/src/main/kotlin/de/timklge/karoopowerbar/MainActivity.kt @@ -1,14 +1,11 @@ package de.timklge.karoopowerbar -import android.R import android.content.Context import android.content.Intent -import android.os.Build import android.os.Bundle import android.provider.Settings import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.compose.material3.Text import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.preferencesDataStore diff --git a/app/src/main/kotlin/de/timklge/karoopowerbar/Settings.kt b/app/src/main/kotlin/de/timklge/karoopowerbar/Settings.kt new file mode 100644 index 0000000..b10f359 --- /dev/null +++ b/app/src/main/kotlin/de/timklge/karoopowerbar/Settings.kt @@ -0,0 +1,55 @@ +package de.timklge.karoopowerbar + +import android.content.Context +import android.util.Log +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import de.timklge.karoopowerbar.screens.SelectedSource +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +val settingsKey = stringPreferencesKey("settings") + +@Serializable +data class PowerbarSettings( + val source: SelectedSource = SelectedSource.POWER, + val topBarSource: SelectedSource = SelectedSource.NONE, + val onlyShowWhileRiding: Boolean = true, + val showLabelOnBars: Boolean = true, + val useZoneColors: Boolean = true, + val barSize: CustomProgressBarSize = CustomProgressBarSize.MEDIUM, + + val minCadence: Int = defaultMinCadence, val maxCadence: Int = defaultMaxCadence, + val minSpeed: Float = defaultMinSpeedMs, val maxSpeed: Float = defaultMaxSpeedMs, // 50 km/h in m/s +){ + companion object { + val defaultSettings = Json.encodeToString(PowerbarSettings()) + val defaultMinSpeedMs = 0f + val defaultMaxSpeedMs = 13.89f + val defaultMinCadence = 50 + val defaultMaxCadence = 120 + } +} + +suspend fun saveSettings(context: Context, settings: PowerbarSettings) { + context.dataStore.edit { t -> + t[settingsKey] = Json.encodeToString(settings) + } +} + +fun Context.streamSettings(): Flow { + return dataStore.data.map { settingsJson -> + try { + jsonWithUnknownKeys.decodeFromString( + settingsJson[settingsKey] ?: PowerbarSettings.defaultSettings + ) + } catch(e: Throwable){ + Log.e(KarooPowerbarExtension.TAG, "Failed to read preferences", e) + jsonWithUnknownKeys.decodeFromString(PowerbarSettings.defaultSettings) + } + }.distinctUntilChanged() +} \ No newline at end of file diff --git a/app/src/main/kotlin/de/timklge/karoopowerbar/Window.kt b/app/src/main/kotlin/de/timklge/karoopowerbar/Window.kt index 7dd6bd7..961e40e 100644 --- a/app/src/main/kotlin/de/timklge/karoopowerbar/Window.kt +++ b/app/src/main/kotlin/de/timklge/karoopowerbar/Window.kt @@ -16,6 +16,7 @@ import android.view.View import android.view.ViewGroup import android.view.WindowInsets import android.view.WindowManager +import androidx.annotation.ColorRes import de.timklge.karoopowerbar.KarooPowerbarExtension.Companion.TAG import de.timklge.karoopowerbar.screens.SelectedSource import io.hammerhead.karooext.KarooSystemService @@ -150,28 +151,6 @@ class Window( } } - companion object { - val speedZones = listOf( - UserProfile.Zone(0, 9), - UserProfile.Zone(10, 19), - UserProfile.Zone(20, 24), - UserProfile.Zone(25, 29), - UserProfile.Zone(30, 34), - UserProfile.Zone(34, 39), - UserProfile.Zone(40, 44), - ) - - val cadenceZones = listOf( - UserProfile.Zone(0, 59), - UserProfile.Zone(60, 79), - UserProfile.Zone(80, 89), - UserProfile.Zone(90, 99), - UserProfile.Zone(100, 109), - UserProfile.Zone(110, 119), - UserProfile.Zone(120, 129), - ) - } - private suspend fun streamSpeed(smoothed: Boolean) { val speedFlow = karooSystem.streamDataFlow(if(smoothed) DataType.Type.SMOOTHED_3S_AVERAGE_SPEED else DataType.Type.SPEED) .map { (it as? StreamState.Streaming)?.dataPoint?.singleValue } @@ -185,20 +164,23 @@ class Window( .combine(settingsFlow) { streamData, settings -> streamData.copy(settings = settings) } .distinctUntilChanged() .collect { streamData -> - val valueMetersPerSecond = streamData.value?.roundToInt() + val valueMetersPerSecond = streamData.value val value = when (streamData.userProfile.preferredUnit.distance){ UserProfile.PreferredUnit.UnitType.IMPERIAL -> valueMetersPerSecond?.times(2.23694) else -> valueMetersPerSecond?.times(3.6) }?.roundToInt() - if (value != null) { - val minSpeed = speedZones.first().min - val maxSpeed = speedZones.last().min + 5 + if (value != null && valueMetersPerSecond != null) { + val minSpeed = streamData.settings?.minSpeed ?: PowerbarSettings.defaultMinSpeedMs + val maxSpeed = streamData.settings?.maxSpeed ?: PowerbarSettings.defaultMaxSpeedMs val progress = - remap(value.toDouble(), minSpeed.toDouble(), maxSpeed.toDouble(), 0.0, 1.0) + remap(valueMetersPerSecond, minSpeed.toDouble(), maxSpeed.toDouble(), 0.0, 1.0) + powerbar.showValueIfNull = valueMetersPerSecond != 0.0 + + @ColorRes val zoneColorRes = Zone.entries[(progress * Zone.entries.size).roundToInt().coerceIn(0.. - bottomSelectedSource = settings.source - topSelectedSource = settings.topBarSource - onlyShowWhileRiding = settings.onlyShowWhileRiding - showLabelOnBars = settings.showLabelOnBars - colorBasedOnZones = settings.useZoneColors - barSize = settings.barSize + ctx.streamSettings() + .combine(karooSystem.streamUserProfile()) { settings, profile -> settings to profile } + .distinctUntilChanged() + .collect { (settings, profile) -> + bottomSelectedSource = settings.source + topSelectedSource = settings.topBarSource + onlyShowWhileRiding = settings.onlyShowWhileRiding + showLabelOnBars = settings.showLabelOnBars + colorBasedOnZones = settings.useZoneColors + barSize = settings.barSize + minCadence = settings.minCadence.toString() + maxCadence = settings.maxCadence.toString() + isImperial = profile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL + minSpeed = (if(isImperial) settings.minSpeed * 2.23694f else settings.minSpeed * 3.6f).roundToInt().toString() + maxSpeed = (if(isImperial) settings.maxSpeed * 2.23694f else settings.maxSpeed * 3.6f).roundToInt().toString() } } @@ -149,6 +178,50 @@ fun MainScreen() { } } + if (topSelectedSource == SelectedSource.SPEED || topSelectedSource == SelectedSource.SPEED_3S || + bottomSelectedSource == SelectedSource.SPEED || bottomSelectedSource == SelectedSource.SPEED_3S){ + + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { + OutlinedTextField(value = minSpeed, modifier = Modifier.weight(1f).absolutePadding(right = 2.dp), + onValueChange = { minSpeed = it }, + label = { Text("Min Speed") }, + suffix = { Text(if (isImperial) "mph" else "kph") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + singleLine = true + ) + + OutlinedTextField(value = maxSpeed, modifier = Modifier.weight(1f).absolutePadding(left = 2.dp), + onValueChange = { maxSpeed = it }, + label = { Text("Max Speed") }, + suffix = { Text(if (isImperial) "mph" else "kph") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + singleLine = true + ) + } + } + + if (bottomSelectedSource == SelectedSource.CADENCE || topSelectedSource == SelectedSource.CADENCE || + bottomSelectedSource == SelectedSource.CADENCE_3S || topSelectedSource == SelectedSource.CADENCE_3S){ + + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { + OutlinedTextField(value = minCadence, modifier = Modifier.weight(1f).absolutePadding(right = 2.dp), + onValueChange = { minCadence = it }, + label = { Text("Min Cadence") }, + suffix = { Text("rpm") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + singleLine = true + ) + + OutlinedTextField(value = maxCadence, modifier = Modifier.weight(1f).absolutePadding(left = 2.dp), + onValueChange = { maxCadence = it }, + label = { Text("Min Cadence") }, + suffix = { Text("rpm") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + singleLine = true + ) + } + } + Row(verticalAlignment = Alignment.CenterVertically) { Switch(checked = colorBasedOnZones, onCheckedChange = { colorBasedOnZones = it}) Spacer(modifier = Modifier.width(10.dp)) @@ -170,17 +243,23 @@ fun MainScreen() { FilledTonalButton(modifier = Modifier .fillMaxWidth() .height(50.dp), onClick = { - val newSettings = PowerbarSettings( - source = bottomSelectedSource, topBarSource = topSelectedSource, - onlyShowWhileRiding = onlyShowWhileRiding, showLabelOnBars = showLabelOnBars, - useZoneColors = colorBasedOnZones, - barSize = barSize - ) + val minSpeedSetting = (minSpeed.toIntOrNull()?.toFloat()?.div((if(isImperial) 2.23694f else 3.6f))) ?: PowerbarSettings.defaultMinSpeedMs + val maxSpeedSetting = (maxSpeed.toIntOrNull()?.toFloat()?.div((if(isImperial) 2.23694f else 3.6f))) ?: PowerbarSettings.defaultMaxSpeedMs - coroutineScope.launch { - saveSettings(ctx, newSettings) - savedDialogVisible = true - } + val newSettings = PowerbarSettings( + source = bottomSelectedSource, topBarSource = topSelectedSource, + onlyShowWhileRiding = onlyShowWhileRiding, showLabelOnBars = showLabelOnBars, + useZoneColors = colorBasedOnZones, + minCadence = minCadence.toIntOrNull() ?: PowerbarSettings.defaultMinCadence, + maxCadence = maxCadence.toIntOrNull() ?: PowerbarSettings.defaultMaxCadence, + minSpeed = minSpeedSetting, maxSpeed = maxSpeedSetting, + barSize = barSize + ) + + coroutineScope.launch { + saveSettings(ctx, newSettings) + savedDialogVisible = true + } }) { Icon(Icons.Default.Done, contentDescription = "Save") Spacer(modifier = Modifier.width(5.dp))