539 lines
26 KiB
Kotlin
539 lines
26 KiB
Kotlin
package de.timklge.karoopowerbar
|
|
|
|
import android.annotation.SuppressLint
|
|
import android.content.BroadcastReceiver
|
|
import android.content.Context
|
|
import android.content.Context.WINDOW_SERVICE
|
|
import android.content.Intent
|
|
import android.content.IntentFilter
|
|
import android.graphics.PixelFormat
|
|
import android.os.Build
|
|
import android.util.DisplayMetrics
|
|
import android.util.Log
|
|
import android.view.Gravity
|
|
import android.view.LayoutInflater
|
|
import android.view.View
|
|
import android.view.ViewGroup
|
|
import android.view.WindowInsets
|
|
import android.view.WindowManager
|
|
import androidx.annotation.ColorRes
|
|
import com.mapbox.geojson.LineString
|
|
import com.mapbox.turf.TurfConstants.UNIT_METERS
|
|
import com.mapbox.turf.TurfMeasurement
|
|
import de.timklge.karoopowerbar.KarooPowerbarExtension.Companion.TAG
|
|
import de.timklge.karoopowerbar.screens.SelectedSource
|
|
import io.hammerhead.karooext.KarooSystemService
|
|
import io.hammerhead.karooext.models.DataPoint
|
|
import io.hammerhead.karooext.models.DataType
|
|
import io.hammerhead.karooext.models.OnNavigationState
|
|
import io.hammerhead.karooext.models.StreamState
|
|
import io.hammerhead.karooext.models.UserProfile
|
|
import kotlinx.coroutines.CoroutineScope
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.Job
|
|
import kotlinx.coroutines.delay
|
|
import kotlinx.coroutines.flow.combine
|
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
import kotlinx.coroutines.flow.map
|
|
import kotlinx.coroutines.launch
|
|
import kotlinx.coroutines.withContext
|
|
import java.util.Locale
|
|
import kotlin.math.roundToInt
|
|
|
|
fun remap(value: Double?, fromMin: Double, fromMax: Double, toMin: Double, toMax: Double): Double? {
|
|
if (value == null) return null
|
|
|
|
return (value - fromMin) * (toMax - toMin) / (fromMax - fromMin) + toMin
|
|
}
|
|
|
|
enum class PowerbarLocation {
|
|
TOP, BOTTOM
|
|
}
|
|
|
|
class Window(
|
|
private val context: Context,
|
|
val powerbarLocation: PowerbarLocation = PowerbarLocation.BOTTOM,
|
|
val showLabel: Boolean,
|
|
val barBackground: Boolean,
|
|
val powerbarBarSize: CustomProgressBarBarSize,
|
|
val powerbarFontSize: CustomProgressBarFontSize,
|
|
) {
|
|
companion object {
|
|
val FIELD_TARGET_VALUE_ID = "FIELD_WORKOUT_TARGET_VALUE_ID";
|
|
val FIELD_TARGET_MIN_ID = "FIELD_WORKOUT_TARGET_MIN_VALUE_ID";
|
|
val FIELD_TARGET_MAX_ID = "FIELD_WORKOUT_TARGET_MAX_VALUE_ID";
|
|
}
|
|
|
|
private val rootView: View
|
|
private var layoutParams: WindowManager.LayoutParams? = null
|
|
private val windowManager: WindowManager
|
|
private val layoutInflater: LayoutInflater
|
|
|
|
private val powerbar: CustomProgressBar
|
|
|
|
var selectedSource: SelectedSource = SelectedSource.POWER
|
|
|
|
init {
|
|
layoutParams = WindowManager.LayoutParams(
|
|
WindowManager.LayoutParams.WRAP_CONTENT,
|
|
WindowManager.LayoutParams.WRAP_CONTENT,
|
|
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
|
|
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE.or(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE),
|
|
PixelFormat.TRANSLUCENT
|
|
)
|
|
|
|
layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
|
rootView = layoutInflater.inflate(R.layout.popup_window, null)
|
|
powerbar = rootView.findViewById(R.id.progressBar)
|
|
powerbar.progress = null
|
|
|
|
windowManager = context.getSystemService(WINDOW_SERVICE) as WindowManager
|
|
val displayMetrics = DisplayMetrics()
|
|
|
|
if (Build.VERSION.SDK_INT >= 30) {
|
|
val windowMetrics = windowManager.currentWindowMetrics
|
|
val insets = windowMetrics.windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.systemBars())
|
|
val bounds = windowMetrics.bounds
|
|
displayMetrics.widthPixels = bounds.width() - insets.left - insets.right
|
|
displayMetrics.heightPixels = bounds.height() - insets.top - insets.bottom
|
|
} else {
|
|
@Suppress("DEPRECATION")
|
|
windowManager.defaultDisplay.getMetrics(displayMetrics)
|
|
}
|
|
|
|
layoutParams?.gravity = when (powerbarLocation) {
|
|
PowerbarLocation.TOP -> Gravity.TOP
|
|
PowerbarLocation.BOTTOM -> Gravity.BOTTOM
|
|
}
|
|
if (powerbarLocation == PowerbarLocation.TOP) {
|
|
layoutParams?.y = 0
|
|
} else {
|
|
layoutParams?.y = 0
|
|
}
|
|
layoutParams?.width = displayMetrics.widthPixels
|
|
layoutParams?.alpha = 0.8f
|
|
}
|
|
|
|
private val karooSystem: KarooSystemService = KarooSystemService(context)
|
|
|
|
private var serviceJob: Job? = null
|
|
|
|
@SuppressLint("UnspecifiedRegisterReceiverFlag")
|
|
suspend fun open() {
|
|
serviceJob = CoroutineScope(Dispatchers.Default).launch {
|
|
val filter = IntentFilter("de.timklge.HIDE_POWERBAR")
|
|
if (Build.VERSION.SDK_INT >= 33) {
|
|
context.registerReceiver(hideReceiver, filter, Context.RECEIVER_EXPORTED)
|
|
} else {
|
|
context.registerReceiver(hideReceiver, filter)
|
|
}
|
|
|
|
karooSystem.connect { connected ->
|
|
Log.i(TAG, "Karoo system service connected: $connected")
|
|
}
|
|
|
|
powerbar.progressColor = context.resources.getColor(R.color.zone7)
|
|
powerbar.progress = null
|
|
powerbar.location = powerbarLocation
|
|
powerbar.showLabel = showLabel
|
|
powerbar.barBackground = barBackground
|
|
powerbar.fontSize = powerbarFontSize
|
|
powerbar.barSize = powerbarBarSize
|
|
powerbar.invalidate()
|
|
|
|
Log.i(TAG, "Streaming $selectedSource")
|
|
|
|
when (selectedSource){
|
|
SelectedSource.POWER -> streamPower(PowerStreamSmoothing.RAW)
|
|
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)
|
|
SelectedSource.ROUTE_PROGRESS -> streamRouteProgress(::getRouteProgress)
|
|
SelectedSource.REMAINING_ROUTE -> streamRouteProgress(::getRemainingRouteProgress)
|
|
SelectedSource.GRADE -> streamGrade()
|
|
SelectedSource.NONE -> {}
|
|
}
|
|
}
|
|
|
|
try {
|
|
withContext(Dispatchers.Main) {
|
|
if (rootView.windowToken == null && rootView.parent == null) {
|
|
windowManager.addView(rootView, layoutParams)
|
|
}
|
|
}
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, e.toString())
|
|
}
|
|
}
|
|
|
|
data class BarProgress(
|
|
val progress: Double?,
|
|
val label: String,
|
|
)
|
|
|
|
private fun getRouteProgress(userProfile: UserProfile, riddenDistance: Double?, routeEndAt: Double?, distanceToDestination: Double?): BarProgress {
|
|
val routeProgress = if (routeEndAt != null && riddenDistance != null) remap(riddenDistance, 0.0, routeEndAt, 0.0, 1.0) else null
|
|
val routeProgressInUserUnit = when (userProfile.preferredUnit.distance) {
|
|
UserProfile.PreferredUnit.UnitType.IMPERIAL -> riddenDistance?.times(0.000621371)?.roundToInt() // Miles
|
|
else -> riddenDistance?.times(0.001)?.roundToInt() // Kilometers
|
|
}
|
|
|
|
return BarProgress(routeProgress, "$routeProgressInUserUnit")
|
|
}
|
|
|
|
private fun getRemainingRouteProgress(userProfile: UserProfile, riddenDistance: Double?, routeEndAt: Double?, distanceToDestination: Double?): BarProgress {
|
|
val routeProgress = if (routeEndAt != null && riddenDistance != null) remap(riddenDistance, 0.0, routeEndAt, 0.0, 1.0) else null
|
|
val distanceToDestinationInUserUnit = when (userProfile.preferredUnit.distance) {
|
|
UserProfile.PreferredUnit.UnitType.IMPERIAL -> distanceToDestination?.times(0.000621371)?.roundToInt() // Miles
|
|
else -> distanceToDestination?.times(0.001)?.roundToInt() // Kilometers
|
|
}
|
|
|
|
return BarProgress(routeProgress, "$distanceToDestinationInUserUnit")
|
|
}
|
|
|
|
private suspend fun streamRouteProgress(routeProgressProvider: (UserProfile, Double?, Double?, Double?) -> BarProgress) {
|
|
data class StreamData(
|
|
val userProfile: UserProfile,
|
|
val distanceToDestination: Double?,
|
|
val navigationState: OnNavigationState,
|
|
val riddenDistance: Double?
|
|
)
|
|
|
|
var lastKnownRoutePolyline: String? = null
|
|
var lastKnownRouteLength: Double? = null
|
|
|
|
combine(karooSystem.streamUserProfile(), karooSystem.streamDataFlow(DataType.Type.DISTANCE_TO_DESTINATION), karooSystem.streamNavigationState(), karooSystem.streamDataFlow(DataType.Type.DISTANCE)) { userProfile, distanceToDestination, navigationState, riddenDistance ->
|
|
StreamData(
|
|
userProfile,
|
|
(distanceToDestination as? StreamState.Streaming)?.dataPoint?.values?.get(DataType.Field.DISTANCE_TO_DESTINATION),
|
|
navigationState,
|
|
(riddenDistance as? StreamState.Streaming)?.dataPoint?.values?.get(DataType.Field.DISTANCE)
|
|
)
|
|
}.distinctUntilChanged().throttle(5_000).collect { (userProfile, distanceToDestination, navigationState, riddenDistance) ->
|
|
val state = navigationState.state
|
|
val routePolyline = when (state) {
|
|
is OnNavigationState.NavigationState.NavigatingRoute -> state.routePolyline
|
|
is OnNavigationState.NavigationState.NavigatingToDestination -> state.polyline
|
|
else -> null
|
|
}
|
|
|
|
if (routePolyline != lastKnownRoutePolyline) {
|
|
lastKnownRoutePolyline = routePolyline
|
|
lastKnownRouteLength = when (state){
|
|
is OnNavigationState.NavigationState.NavigatingRoute -> state.routeDistance
|
|
is OnNavigationState.NavigationState.NavigatingToDestination -> try {
|
|
TurfMeasurement.length(LineString.fromPolyline(state.polyline, 5), UNIT_METERS)
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to calculate route length", e)
|
|
null
|
|
}
|
|
else -> null
|
|
}
|
|
}
|
|
|
|
val routeEndAt = lastKnownRouteLength?.plus((distanceToDestination ?: 0.0))
|
|
val barProgress = routeProgressProvider(userProfile, riddenDistance, routeEndAt, distanceToDestination)
|
|
|
|
powerbar.progressColor = context.getColor(R.color.zone0)
|
|
powerbar.progress = barProgress.progress
|
|
powerbar.label = barProgress.label
|
|
powerbar.invalidate()
|
|
}
|
|
}
|
|
|
|
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()
|
|
|
|
data class StreamData(val userProfile: UserProfile, val value: Double?, val settings: PowerbarSettings? = null)
|
|
|
|
combine(karooSystem.streamUserProfile(), speedFlow, settingsFlow) { userProfile, speed, settings ->
|
|
StreamData(userProfile, speed, settings)
|
|
}.distinctUntilChanged().throttle(1_000).collect { streamData ->
|
|
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 && valueMetersPerSecond != null) {
|
|
val minSpeed = streamData.settings?.minSpeed ?: PowerbarSettings.defaultMinSpeedMs
|
|
val maxSpeed = streamData.settings?.maxSpeed ?: PowerbarSettings.defaultMaxSpeedMs
|
|
val progress = remap(valueMetersPerSecond, minSpeed.toDouble(), maxSpeed.toDouble(), 0.0, 1.0) ?: 0.0
|
|
|
|
@ColorRes val zoneColorRes = Zone.entries[(progress * Zone.entries.size).roundToInt().coerceIn(0..<Zone.entries.size)].colorResource
|
|
|
|
powerbar.progressColor = if (streamData.settings?.useZoneColors == true) {
|
|
context.getColor(zoneColorRes)
|
|
} else {
|
|
context.getColor(R.color.zone0)
|
|
}
|
|
powerbar.progress = if (value > 0) progress else null
|
|
powerbar.label = "$value"
|
|
|
|
Log.d(TAG, "Speed: $value min: $minSpeed max: $maxSpeed")
|
|
} else {
|
|
powerbar.progressColor = context.getColor(R.color.zone0)
|
|
powerbar.progress = null
|
|
powerbar.label = "?"
|
|
|
|
Log.d(TAG, "Speed: Unavailable")
|
|
}
|
|
powerbar.invalidate()
|
|
}
|
|
}
|
|
|
|
private suspend fun streamGrade() {
|
|
@ColorRes
|
|
fun getInclineIndicatorColor(percent: Float): Int? {
|
|
return when(percent) {
|
|
in -Float.MAX_VALUE..<-7.5f -> R.color.eleDarkBlue // Dark blue
|
|
in -7.5f..<-4.6f -> R.color.eleLightBlue // Light blue
|
|
in -4.6f..<-2f -> R.color.eleWhite // White
|
|
in 2f..<4.6f -> R.color.eleDarkGreen // Dark green
|
|
in 4.6f..<7.5f -> R.color.eleLightGreen // Light green
|
|
in 7.5f..<12.5f -> R.color.eleYellow // Yellow
|
|
in 12.5f..<15.5f -> R.color.eleLightOrange // Light Orange
|
|
in 15.5f..<19.5f -> R.color.eleDarkOrange // Dark Orange
|
|
in 19.5f..<23.5f -> R.color.eleRed // Red
|
|
in 23.5f..Float.MAX_VALUE -> R.color.elePurple // Purple
|
|
else -> null
|
|
}
|
|
}
|
|
|
|
val gradeFlow = karooSystem.streamDataFlow(DataType.Type.ELEVATION_GRADE)
|
|
.map { (it as? StreamState.Streaming)?.dataPoint?.singleValue }
|
|
.distinctUntilChanged()
|
|
|
|
data class StreamData(val userProfile: UserProfile, val value: Double?, val settings: PowerbarSettings? = null)
|
|
|
|
val settingsFlow = context.streamSettings()
|
|
|
|
combine(karooSystem.streamUserProfile(), gradeFlow, settingsFlow) { userProfile, grade, settings ->
|
|
StreamData(userProfile, grade, settings)
|
|
}.distinctUntilChanged().throttle(1_000).collect { streamData ->
|
|
val value = streamData.value
|
|
|
|
if (value != null) {
|
|
val minGradient = streamData.settings?.minGradient ?: PowerbarSettings.defaultMinGradient
|
|
val maxGradient = streamData.settings?.maxGradient ?: PowerbarSettings.defaultMaxGradient
|
|
|
|
powerbar.progressColor = getInclineIndicatorColor(value.toFloat()) ?: context.getColor(R.color.zone0)
|
|
powerbar.progress = remap(value.toDouble(), minGradient.toDouble(), maxGradient.toDouble(), 0.0, 1.0)
|
|
powerbar.label = "${String.format(Locale.getDefault(), "%.1f", value)}%"
|
|
|
|
Log.d(TAG, "Grade: $value")
|
|
} else {
|
|
powerbar.progressColor = context.getColor(R.color.zone0)
|
|
powerbar.progress = null
|
|
powerbar.label = "?"
|
|
|
|
Log.d(TAG, "Grade: Unavailable")
|
|
}
|
|
powerbar.invalidate()
|
|
}
|
|
}
|
|
|
|
private suspend fun streamCadence(smoothed: Boolean) {
|
|
val cadenceFlow = karooSystem.streamDataFlow(if(smoothed) DataType.Type.SMOOTHED_3S_AVERAGE_CADENCE else DataType.Type.CADENCE)
|
|
.map { (it as? StreamState.Streaming)?.dataPoint?.singleValue }
|
|
.distinctUntilChanged()
|
|
|
|
data class StreamData(val userProfile: UserProfile, val value: Double?, val settings: PowerbarSettings? = null, val cadenceTarget: DataPoint? = null)
|
|
|
|
val settingsFlow = context.streamSettings()
|
|
val cadenceTargetFlow = karooSystem.streamDataFlow("TYPE_WORKOUT_CADENCE_TARGET_ID")
|
|
.map { (it as? StreamState.Streaming)?.dataPoint }
|
|
.distinctUntilChanged()
|
|
|
|
combine(karooSystem.streamUserProfile(), cadenceFlow, settingsFlow, cadenceTargetFlow) { userProfile, speed, settings, cadenceTarget ->
|
|
StreamData(userProfile, speed, settings, cadenceTarget)
|
|
}.distinctUntilChanged().throttle(1_000).collect { streamData ->
|
|
val value = streamData.value?.roundToInt()
|
|
|
|
if (value != null) {
|
|
val minCadence = streamData.settings?.minCadence ?: PowerbarSettings.defaultMinCadence
|
|
val maxCadence = streamData.settings?.maxCadence ?: PowerbarSettings.defaultMaxCadence
|
|
val progress = remap(value.toDouble(), minCadence.toDouble(), maxCadence.toDouble(), 0.0, 1.0) ?: 0.0
|
|
|
|
powerbar.minTarget = remap(streamData.cadenceTarget?.values?.get(FIELD_TARGET_MIN_ID)?.toDouble(), minCadence.toDouble(), maxCadence.toDouble(), 0.0, 1.0)
|
|
powerbar.maxTarget = remap(streamData.cadenceTarget?.values?.get(FIELD_TARGET_MAX_ID)?.toDouble(), minCadence.toDouble(), maxCadence.toDouble(), 0.0, 1.0)
|
|
powerbar.target = remap(streamData.cadenceTarget?.values?.get(FIELD_TARGET_VALUE_ID)?.toDouble(), minCadence.toDouble(), maxCadence.toDouble(), 0.0, 1.0)
|
|
|
|
@ColorRes val zoneColorRes = Zone.entries[(progress * Zone.entries.size).roundToInt().coerceIn(0..<Zone.entries.size)].colorResource
|
|
|
|
powerbar.progressColor = if (streamData.settings?.useZoneColors == true) {
|
|
context.getColor(zoneColorRes)
|
|
} else {
|
|
context.getColor(R.color.zone0)
|
|
}
|
|
powerbar.progress = if (value > 0) progress else null
|
|
powerbar.label = "$value"
|
|
|
|
Log.d(TAG, "Cadence: $value min: $minCadence max: $maxCadence")
|
|
} else {
|
|
powerbar.progressColor = context.getColor(R.color.zone0)
|
|
powerbar.progress = null
|
|
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 }
|
|
.distinctUntilChanged()
|
|
|
|
val settingsFlow = context.streamSettings()
|
|
val hrTargetFlow = karooSystem.streamDataFlow("TYPE_WORKOUT_HEART_RATE_TARGET_ID")
|
|
.map { (it as? StreamState.Streaming)?.dataPoint }
|
|
.distinctUntilChanged()
|
|
|
|
data class StreamData(val userProfile: UserProfile, val value: Double?, val settings: PowerbarSettings? = null, val heartrateTarget: DataPoint? = null)
|
|
|
|
combine(karooSystem.streamUserProfile(), hrFlow, settingsFlow, hrTargetFlow) { userProfile, hr, settings, hrTarget ->
|
|
StreamData(userProfile, hr, settings, hrTarget)
|
|
}.distinctUntilChanged().throttle(1_000).collect { streamData ->
|
|
val value = streamData.value?.roundToInt()
|
|
|
|
if (value != null) {
|
|
val customMinHr = if (streamData.settings?.useCustomHrRange == true) streamData.settings.minHr else null
|
|
val customMaxHr = if (streamData.settings?.useCustomHrRange == true) streamData.settings.maxHr else null
|
|
val minHr = customMinHr ?: streamData.userProfile.restingHr
|
|
val maxHr = customMaxHr ?: streamData.userProfile.maxHr
|
|
val progress = remap(value.toDouble(), minHr.toDouble(), maxHr.toDouble(), 0.0, 1.0)
|
|
|
|
powerbar.minTarget = remap(streamData.heartrateTarget?.values?.get(FIELD_TARGET_MIN_ID), minHr.toDouble(), maxHr.toDouble(), 0.0, 1.0)
|
|
powerbar.maxTarget = remap(streamData.heartrateTarget?.values?.get(FIELD_TARGET_MAX_ID), minHr.toDouble(), maxHr.toDouble(), 0.0, 1.0)
|
|
powerbar.target = remap(streamData.heartrateTarget?.values?.get(FIELD_TARGET_VALUE_ID), minHr.toDouble(), maxHr.toDouble(), 0.0, 1.0)
|
|
|
|
powerbar.progressColor = if (streamData.settings?.useZoneColors == true) {
|
|
context.getColor(getZone(streamData.userProfile.heartRateZones, value)?.colorResource ?: R.color.zone7)
|
|
} else {
|
|
context.getColor(R.color.zone0)
|
|
}
|
|
powerbar.progress = if (value > 0) progress else null
|
|
powerbar.label = "$value"
|
|
|
|
Log.d(TAG, "Hr: $value min: $minHr max: $maxHr")
|
|
} else {
|
|
powerbar.progressColor = context.getColor(R.color.zone0)
|
|
powerbar.progress = null
|
|
powerbar.label = "?"
|
|
|
|
Log.d(TAG, "Hr: Unavailable")
|
|
}
|
|
powerbar.invalidate()
|
|
}
|
|
}
|
|
|
|
enum class PowerStreamSmoothing(val dataTypeId: String){
|
|
RAW(DataType.Type.POWER),
|
|
SMOOTHED_3S(DataType.Type.SMOOTHED_3S_AVERAGE_POWER),
|
|
SMOOTHED_10S(DataType.Type.SMOOTHED_10S_AVERAGE_POWER),
|
|
}
|
|
|
|
private suspend fun streamPower(smoothed: PowerStreamSmoothing) {
|
|
val powerFlow = karooSystem.streamDataFlow(smoothed.dataTypeId)
|
|
.map { (it as? StreamState.Streaming)?.dataPoint?.singleValue }
|
|
.distinctUntilChanged()
|
|
|
|
val settingsFlow = context.streamSettings()
|
|
|
|
val powerTargetFlow = karooSystem.streamDataFlow("TYPE_WORKOUT_POWER_TARGET_ID") // TYPE_WORKOUT_HEART_RATE_TARGET_ID, TYPE_WORKOUT_CADENCE_TARGET_ID,
|
|
.map { (it as? StreamState.Streaming)?.dataPoint }
|
|
.distinctUntilChanged()
|
|
|
|
data class StreamData(val userProfile: UserProfile, val value: Double?, val settings: PowerbarSettings? = null, val powerTarget: DataPoint? = null)
|
|
|
|
combine(karooSystem.streamUserProfile(), powerFlow, settingsFlow, powerTargetFlow) { userProfile, hr, settings, powerTarget ->
|
|
StreamData(userProfile, hr, settings, powerTarget)
|
|
}.distinctUntilChanged().throttle(1_000).collect { streamData ->
|
|
val value = streamData.value?.roundToInt()
|
|
|
|
if (value != null) {
|
|
val customMinPower = if (streamData.settings?.useCustomPowerRange == true) streamData.settings.minPower else null
|
|
val customMaxPower = if (streamData.settings?.useCustomPowerRange == true) streamData.settings.maxPower else null
|
|
val minPower = customMinPower ?: streamData.userProfile.powerZones.first().min
|
|
val maxPower = customMaxPower ?: (streamData.userProfile.powerZones.last().min + 30)
|
|
val progress = remap(value.toDouble(), minPower.toDouble(), maxPower.toDouble(), 0.0, 1.0)
|
|
|
|
powerbar.minTarget = remap(streamData.powerTarget?.values?.get(FIELD_TARGET_MIN_ID), minPower.toDouble(), maxPower.toDouble(), 0.0, 1.0)
|
|
powerbar.maxTarget = remap(streamData.powerTarget?.values?.get(FIELD_TARGET_MAX_ID), minPower.toDouble(), maxPower.toDouble(), 0.0, 1.0)
|
|
powerbar.target = remap(streamData.powerTarget?.values?.get(FIELD_TARGET_VALUE_ID), minPower.toDouble(), maxPower.toDouble(), 0.0, 1.0)
|
|
|
|
powerbar.progressColor = if (streamData.settings?.useZoneColors == true) {
|
|
context.getColor(getZone(streamData.userProfile.powerZones, value)?.colorResource ?: R.color.zone7)
|
|
} else {
|
|
context.getColor(R.color.zone0)
|
|
}
|
|
powerbar.progress = if (value > 0) progress else null
|
|
powerbar.label = "${value}W"
|
|
|
|
Log.d(TAG, "Power: $value min: $minPower max: $maxPower")
|
|
} else {
|
|
powerbar.progressColor = context.getColor(R.color.zone0)
|
|
powerbar.progress = null
|
|
powerbar.label = "?"
|
|
|
|
Log.d(TAG, "Power: Unavailable")
|
|
}
|
|
powerbar.invalidate()
|
|
}
|
|
}
|
|
|
|
private var currentHideJob: Job? = null
|
|
|
|
fun close() {
|
|
try {
|
|
context.unregisterReceiver(hideReceiver)
|
|
if (currentHideJob != null){
|
|
currentHideJob?.cancel()
|
|
currentHideJob = null
|
|
}
|
|
serviceJob?.cancel()
|
|
(context.getSystemService(WINDOW_SERVICE) as WindowManager).removeView(rootView)
|
|
rootView.invalidate()
|
|
(rootView.parent as? ViewGroup)?.removeAllViews()
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to dispose window", e)
|
|
}
|
|
}
|
|
|
|
private val hideReceiver = object : BroadcastReceiver() {
|
|
override fun onReceive(context: Context, intent: Intent) {
|
|
val action = intent.action
|
|
if (action == "de.timklge.HIDE_POWERBAR") {
|
|
val location = when (intent.getStringExtra("location")) {
|
|
"top" -> PowerbarLocation.TOP
|
|
"bottom" -> PowerbarLocation.BOTTOM
|
|
else -> PowerbarLocation.TOP
|
|
}
|
|
val duration = intent.getLongExtra("duration", 15_000)
|
|
Log.d(TAG, "Received broadcast to hide $location powerbar for $duration ms")
|
|
|
|
if (location == powerbarLocation) {
|
|
currentHideJob?.cancel()
|
|
currentHideJob = CoroutineScope(Dispatchers.Main).launch {
|
|
rootView.visibility = View.INVISIBLE
|
|
withContext(Dispatchers.Default) {
|
|
delay(duration)
|
|
}
|
|
rootView.visibility = View.VISIBLE
|
|
currentHideJob = null
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} |