diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5d8c6f9..ee85c30 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 = 8 - versionName = "1.2.3" + versionCode = 9 + versionName = "1.3" } signingConfigs { diff --git a/app/manifest.json b/app/manifest.json index 41f38a2..9fe0745 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.2.3", - "latestVersionCode": 8, + "latestVersion": "1.3", + "latestVersionCode": 9, "developer": "timklge", "description": "Adds a colored power bar to the bottom of the screen", - "releaseNotes": "Add option to set bar to single color" + "releaseNotes": "Add size setting, cadence and speed data sources" } \ 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 0e9e6c2..44d473c 100644 --- a/app/src/main/kotlin/de/timklge/karoopowerbar/CustomProgressBar.kt +++ b/app/src/main/kotlin/de/timklge/karoopowerbar/CustomProgressBar.kt @@ -11,6 +11,14 @@ import android.util.AttributeSet import android.view.View import androidx.annotation.ColorInt import androidx.core.graphics.ColorUtils +import kotlinx.serialization.Serializable + +@Serializable +enum class CustomProgressBarSize(val id: String, val label: String, val fontSize: Float, val barHeight: Float) { + SMALL("small", "Small", 35f, 10f), + MEDIUM("medium", "Medium", 40f, 15f), + LARGE("large", "Large", 60f, 25f), +} class CustomProgressBar @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null @@ -21,8 +29,11 @@ class CustomProgressBar @JvmOverloads constructor( var showLabel: Boolean = true @ColorInt var progressColor: Int = 0xFF2b86e6.toInt() - val fontSize = 40f - val barHeight = 14f + var size = CustomProgressBarSize.MEDIUM + set(value) { + field = value + textPaint.textSize = value.fontSize + } private val linePaint = Paint().apply { isAntiAlias = true @@ -69,7 +80,7 @@ class CustomProgressBar @JvmOverloads constructor( private val textPaint = Paint().apply { color = Color.WHITE strokeWidth = 3f - textSize = fontSize + textSize = size.fontSize typeface = Typeface.create(Typeface.MONOSPACE, Typeface.BOLD); textAlign = Paint.Align.CENTER } @@ -88,10 +99,10 @@ class CustomProgressBar @JvmOverloads constructor( 1f, 15f, ((canvas.width.toDouble() - 1f) * progress.coerceIn(0.0, 1.0)).toFloat(), - 15f + barHeight + 15f + size.barHeight ) - canvas.drawRect(0f, 15f, canvas.width.toFloat(), 15f + barHeight, backgroundPaint) + canvas.drawRect(0f, 15f, canvas.width.toFloat(), 15f + size.barHeight, backgroundPaint) if (progress > 0.0) { canvas.drawRoundRect(rect, 2f, 2f, blurPaint) @@ -102,7 +113,10 @@ class CustomProgressBar @JvmOverloads constructor( if (showLabel){ val textBounds = textPaint.measureText(label) val xOffset = (textBounds + 20).coerceAtLeast(10f) / 2f - val yOffset = (fontSize - 15f) / 2 + val yOffset = when(size){ + CustomProgressBarSize.SMALL -> (size.fontSize - size.barHeight) / 2 + 2f + CustomProgressBarSize.MEDIUM, CustomProgressBarSize.LARGE -> (size.fontSize - size.barHeight) / 2 + } val x = (rect.right - xOffset).coerceIn(0f..canvas.width-xOffset*2f) val y = rect.top - yOffset val r = x + xOffset * 2 @@ -112,19 +126,19 @@ class CustomProgressBar @JvmOverloads constructor( canvas.drawRoundRect(x, y, r, b, 2f, 2f, blurPaint) canvas.drawRoundRect(x, y, r, b, 2f, 2f, lineStrokePaint) - canvas.drawText(label, x + xOffset, rect.top + barHeight + 6, textPaint) + canvas.drawText(label, x + xOffset, rect.top + size.barHeight + 6, textPaint) } } } PowerbarLocation.BOTTOM -> { val rect = RectF( 1f, - canvas.height.toFloat() - 1f - barHeight, + canvas.height.toFloat() - 1f - size.barHeight, ((canvas.width.toDouble() - 1f) * progress.coerceIn(0.0, 1.0)).toFloat(), canvas.height.toFloat() ) - canvas.drawRect(0f, canvas.height.toFloat() - barHeight - 1f, canvas.width.toFloat(), canvas.height.toFloat(), backgroundPaint) + canvas.drawRect(0f, canvas.height.toFloat() - size.barHeight, canvas.width.toFloat(), canvas.height.toFloat(), backgroundPaint) if (progress > 0.0) { canvas.drawRoundRect(rect, 2f, 2f, blurPaint) @@ -135,7 +149,11 @@ class CustomProgressBar @JvmOverloads constructor( if (showLabel){ val textBounds = textPaint.measureText(label) val xOffset = (textBounds + 20).coerceAtLeast(10f) / 2f - val yOffset = (fontSize + 0f) / 2 + val yOffset = when(size){ + CustomProgressBarSize.SMALL -> size.fontSize / 2 + 2f + CustomProgressBarSize.MEDIUM -> size.fontSize / 2 + CustomProgressBarSize.LARGE -> size.fontSize / 2 - 5f + } val x = (rect.right - xOffset).coerceIn(0f..canvas.width-xOffset*2f) val y = (rect.top - yOffset) val r = x + xOffset * 2 @@ -145,7 +163,7 @@ class CustomProgressBar @JvmOverloads constructor( canvas.drawRoundRect(x, y, r, b, 2f, 2f, blurPaint) canvas.drawRoundRect(x, y, r, b, 2f, 2f, lineStrokePaint) - canvas.drawText(label, x + xOffset, rect.top + barHeight - 1, textPaint) + canvas.drawText(label, x + xOffset, rect.top + size.barHeight - 1, textPaint) } } } diff --git a/app/src/main/kotlin/de/timklge/karoopowerbar/Extensions.kt b/app/src/main/kotlin/de/timklge/karoopowerbar/Extensions.kt index 7383877..f010141 100644 --- a/app/src/main/kotlin/de/timklge/karoopowerbar/Extensions.kt +++ b/app/src/main/kotlin/de/timklge/karoopowerbar/Extensions.kt @@ -31,6 +31,7 @@ data class PowerbarSettings( val onlyShowWhileRiding: Boolean = true, val showLabelOnBars: Boolean = true, val useZoneColors: Boolean = true, + val barSize: CustomProgressBarSize = CustomProgressBarSize.MEDIUM ){ companion object { val defaultSettings = Json.encodeToString(PowerbarSettings()) diff --git a/app/src/main/kotlin/de/timklge/karoopowerbar/ForegroundService.kt b/app/src/main/kotlin/de/timklge/karoopowerbar/ForegroundService.kt index 674bd91..54a606c 100644 --- a/app/src/main/kotlin/de/timklge/karoopowerbar/ForegroundService.kt +++ b/app/src/main/kotlin/de/timklge/karoopowerbar/ForegroundService.kt @@ -53,7 +53,7 @@ class ForegroundService : Service() { windows.clear() if (settings.source != SelectedSource.NONE && showBars) { - Window(this@ForegroundService, PowerbarLocation.BOTTOM, settings.showLabelOnBars).apply { + Window(this@ForegroundService, PowerbarLocation.BOTTOM, settings.showLabelOnBars, settings.barSize).apply { selectedSource = settings.source windows.add(this) open() @@ -61,7 +61,7 @@ class ForegroundService : Service() { } if (settings.topBarSource != SelectedSource.NONE && showBars){ - Window(this@ForegroundService, PowerbarLocation.TOP, settings.showLabelOnBars).apply { + Window(this@ForegroundService, PowerbarLocation.TOP, settings.showLabelOnBars, settings.barSize).apply { selectedSource = settings.topBarSource open() windows.add(this) diff --git a/app/src/main/kotlin/de/timklge/karoopowerbar/KarooPowerbarExtension.kt b/app/src/main/kotlin/de/timklge/karoopowerbar/KarooPowerbarExtension.kt index 817bd5a..adccc5e 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.2.3") { +class KarooPowerbarExtension : KarooExtension("karoo-powerbar", "1.3") { companion object { const val TAG = "karoo-powerbar" diff --git a/app/src/main/kotlin/de/timklge/karoopowerbar/Window.kt b/app/src/main/kotlin/de/timklge/karoopowerbar/Window.kt index 8a0f770..7dd6bd7 100644 --- a/app/src/main/kotlin/de/timklge/karoopowerbar/Window.kt +++ b/app/src/main/kotlin/de/timklge/karoopowerbar/Window.kt @@ -44,7 +44,8 @@ enum class PowerbarLocation { class Window( private val context: Context, val powerbarLocation: PowerbarLocation = PowerbarLocation.BOTTOM, - val showLabel: Boolean + val showLabel: Boolean, + val powerbarSize: CustomProgressBarSize ) { private val rootView: View private var layoutParams: WindowManager.LayoutParams? = null @@ -120,6 +121,7 @@ class Window( powerbar.progress = 0.0 powerbar.location = powerbarLocation powerbar.showLabel = showLabel + powerbar.size = powerbarSize powerbar.invalidate() Log.i(TAG, "Streaming $selectedSource") @@ -129,6 +131,10 @@ class Window( SelectedSource.POWER_3S -> streamPower(PowerStreamSmoothing.SMOOTHED_3S) SelectedSource.POWER_10S -> streamPower(PowerStreamSmoothing.SMOOTHED_10S) SelectedSource.HEART_RATE -> streamHeartrate() + SelectedSource.SPEED -> streamSpeed(false) + SelectedSource.SPEED_3S -> streamSpeed(true) + SelectedSource.CADENCE -> streamCadence(false) + SelectedSource.CADENCE_3S -> streamCadence(true) else -> {} } } @@ -144,6 +150,114 @@ 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 } + .distinctUntilChanged() + + val settingsFlow = context.streamSettings() + + karooSystem.streamUserProfile() + .distinctUntilChanged() + .combine(speedFlow) { userProfile, speed -> StreamData(userProfile, speed) } + .combine(settingsFlow) { streamData, settings -> streamData.copy(settings = settings) } + .distinctUntilChanged() + .collect { streamData -> + val valueMetersPerSecond = streamData.value?.roundToInt() + 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 + val progress = + remap(value.toDouble(), minSpeed.toDouble(), maxSpeed.toDouble(), 0.0, 1.0) + + powerbar.progressColor = if (streamData.settings?.useZoneColors == true) { + context.getColor(getZone(speedZones, value)?.colorResource ?: R.color.zone7) + } else { + context.getColor(R.color.zone0) + } + powerbar.progress = progress + powerbar.label = "$value" + + Log.d(TAG, "Speed: $value min: $minSpeed max: $maxSpeed") + } else { + powerbar.progressColor = context.getColor(R.color.zone0) + powerbar.progress = 0.0 + powerbar.label = "?" + + Log.d(TAG, "Speed: Unavailable") + } + powerbar.invalidate() + } + } + + private suspend fun streamCadence(smoothed: Boolean) { + val speedFlow = karooSystem.streamDataFlow(if(smoothed) DataType.Type.SMOOTHED_3S_AVERAGE_CADENCE else DataType.Type.CADENCE) + .map { (it as? StreamState.Streaming)?.dataPoint?.singleValue } + .distinctUntilChanged() + + val settingsFlow = context.streamSettings() + + karooSystem.streamUserProfile() + .distinctUntilChanged() + .combine(speedFlow) { userProfile, speed -> StreamData(userProfile, speed) } + .combine(settingsFlow) { streamData, settings -> streamData.copy(settings = settings) } + .distinctUntilChanged() + .collect { streamData -> + val value = streamData.value?.roundToInt() + + if (value != null) { + val minCadence = cadenceZones.first().min + val maxCadence = cadenceZones.last().min + 5 + val progress = + remap(value.toDouble(), minCadence.toDouble(), maxCadence.toDouble(), 0.0, 1.0) + + powerbar.progressColor = if (streamData.settings?.useZoneColors == true) { + context.getColor(getZone(cadenceZones, value)?.colorResource ?: R.color.zone7) + } else { + context.getColor(R.color.zone0) + } + powerbar.progress = progress + powerbar.label = "$value" + + Log.d(TAG, "Cadence: $value min: $minCadence max: $maxCadence") + } else { + powerbar.progressColor = context.getColor(R.color.zone0) + powerbar.progress = 0.0 + powerbar.label = "?" + + Log.d(TAG, "Cadence: Unavailable") + } + powerbar.invalidate() + } + } + private suspend fun streamHeartrate() { val hrFlow = karooSystem.streamDataFlow(DataType.Type.HEART_RATE) .map { (it as? StreamState.Streaming)?.dataPoint?.singleValue } diff --git a/app/src/main/kotlin/de/timklge/karoopowerbar/screens/MainScreen.kt b/app/src/main/kotlin/de/timklge/karoopowerbar/screens/MainScreen.kt index 2dc208f..b8cbe78 100644 --- a/app/src/main/kotlin/de/timklge/karoopowerbar/screens/MainScreen.kt +++ b/app/src/main/kotlin/de/timklge/karoopowerbar/screens/MainScreen.kt @@ -39,6 +39,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat.startActivity import androidx.lifecycle.compose.LifecycleResumeEffect +import de.timklge.karoopowerbar.CustomProgressBarSize import de.timklge.karoopowerbar.PowerbarSettings import de.timklge.karoopowerbar.saveSettings import de.timklge.karoopowerbar.streamSettings @@ -52,6 +53,10 @@ enum class SelectedSource(val id: String, val label: String) { POWER("power", "Power"), POWER_3S("power_3s", "Power (3 second avg)"), POWER_10S("power_10s", "Power (10 second avg)"), + SPEED("speed", "Speed"), + SPEED_3S("speed_3s", "Speed (3 second avg"), + CADENCE("cadence", "Cadence"), + CADENCE_3S("cadence_3s", "Cadence (3 second avg)"), } @OptIn(ExperimentalMaterial3Api::class) @@ -72,6 +77,7 @@ fun MainScreen() { var onlyShowWhileRiding by remember { mutableStateOf(false) } var colorBasedOnZones by remember { mutableStateOf(false) } var showLabelOnBars by remember { mutableStateOf(true) } + var barSize by remember { mutableStateOf(CustomProgressBarSize.MEDIUM) } LaunchedEffect(Unit) { givenPermissions = Settings.canDrawOverlays(ctx) @@ -82,6 +88,7 @@ fun MainScreen() { onlyShowWhileRiding = settings.onlyShowWhileRiding showLabelOnBars = settings.showLabelOnBars colorBasedOnZones = settings.useZoneColors + barSize = settings.barSize } } @@ -132,6 +139,16 @@ fun MainScreen() { } } + apply { + val dropdownOptions = CustomProgressBarSize.entries.toList().map { unit -> DropdownOption(unit.id, unit.label) } + val dropdownInitialSelection by remember(barSize) { + mutableStateOf(dropdownOptions.find { option -> option.id == barSize.id }!!) + } + Dropdown(label = "Bar Size", options = dropdownOptions, selected = dropdownInitialSelection) { selectedOption -> + barSize = CustomProgressBarSize.entries.find { unit -> unit.id == selectedOption.id }!! + } + } + Row(verticalAlignment = Alignment.CenterVertically) { Switch(checked = colorBasedOnZones, onCheckedChange = { colorBasedOnZones = it}) Spacer(modifier = Modifier.width(10.dp)) @@ -156,7 +173,8 @@ fun MainScreen() { val newSettings = PowerbarSettings( source = bottomSelectedSource, topBarSource = topSelectedSource, onlyShowWhileRiding = onlyShowWhileRiding, showLabelOnBars = showLabelOnBars, - useZoneColors = colorBasedOnZones + useZoneColors = colorBasedOnZones, + barSize = barSize ) coroutineScope.launch {