fix #1: Add ability to a second power bar to the top of the screen

This commit is contained in:
Tim Kluge 2024-12-08 17:05:11 +01:00
parent 7eed81a109
commit dbaac0cec0
5 changed files with 125 additions and 45 deletions

View File

@ -14,6 +14,7 @@ class CustomProgressBar @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null context: Context, attrs: AttributeSet? = null
) : View(context, attrs) { ) : View(context, attrs) {
var progress: Double = 0.5 var progress: Double = 0.5
var location: PowerbarLocation = PowerbarLocation.BOTTOM
@ColorInt var progressColor: Int = 0xFF2b86e6.toInt() @ColorInt var progressColor: Int = 0xFF2b86e6.toInt()
override fun onDrawForeground(canvas: Canvas) { override fun onDrawForeground(canvas: Canvas) {
@ -48,6 +49,24 @@ class CustomProgressBar @JvmOverloads constructor(
strokeWidth = 2f strokeWidth = 2f
} }
when(location){
PowerbarLocation.TOP -> {
val rect = RectF(
1f,
1f,
((canvas.width.toDouble() - 1f) * progress.coerceIn(0.0, 1.0)).toFloat(),
canvas.height.toFloat() - 1f - 4f
)
canvas.drawRoundRect(0f, 2f, canvas.width.toFloat(), canvas.height.toFloat() - 4f, 2f, 2f, background)
if (progress > 0.0) {
canvas.drawRoundRect(rect, 2f, 2f, blurPaint)
canvas.drawRoundRect(rect, 2f, 2f, linePaint)
canvas.drawRoundRect(rect.right-4, rect.top, rect.right+4, rect.bottom, 2f, 2f, blurPaintHighlight)
}
}
PowerbarLocation.BOTTOM -> {
val rect = RectF( val rect = RectF(
1f, 1f,
1f + 4f, 1f + 4f,
@ -55,14 +74,14 @@ class CustomProgressBar @JvmOverloads constructor(
canvas.height.toFloat() - 1f canvas.height.toFloat() - 1f
) )
val corners = 2f
canvas.drawRoundRect(0f, 2f + 4f, canvas.width.toFloat(), canvas.height.toFloat(), 2f, 2f, background) canvas.drawRoundRect(0f, 2f + 4f, canvas.width.toFloat(), canvas.height.toFloat(), 2f, 2f, background)
if (progress > 0.0) { if (progress > 0.0) {
canvas.drawRoundRect(rect, corners, corners, blurPaint) canvas.drawRoundRect(rect, 2f, 2f, blurPaint)
canvas.drawRoundRect(rect, corners, corners, linePaint) canvas.drawRoundRect(rect, 2f, 2f, linePaint)
}
canvas.drawRoundRect(rect.right-4, rect.top, rect.right+4, rect.bottom, 2f, 2f, blurPaintHighlight) canvas.drawRoundRect(rect.right-4, rect.top, rect.right+4, rect.bottom, 2f, 2f, blurPaintHighlight)
} }
} }
}
}
}

View File

@ -27,6 +27,7 @@ val settingsKey = stringPreferencesKey("settings")
@Serializable @Serializable
data class PowerbarSettings( data class PowerbarSettings(
val source: SelectedSource = SelectedSource.POWER, val source: SelectedSource = SelectedSource.POWER,
val topBarSource: SelectedSource = SelectedSource.NONE,
){ ){
companion object { companion object {
val defaultSettings = Json.encodeToString(PowerbarSettings()) val defaultSettings = Json.encodeToString(PowerbarSettings())

View File

@ -1,6 +1,5 @@
package de.timklge.karoopowerbar package de.timklge.karoopowerbar
import android.R
import android.app.Notification import android.app.Notification
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
@ -9,18 +8,46 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.IBinder import android.os.IBinder
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import de.timklge.karoopowerbar.screens.SelectedSource
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
class ForegroundService : Service() { class ForegroundService : Service() {
override fun onBind(intent: Intent?): IBinder { override fun onBind(intent: Intent?): IBinder {
throw UnsupportedOperationException("Not yet implemented") throw UnsupportedOperationException("Not yet implemented")
} }
private val windows = mutableSetOf<Window>()
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
setupForeground() setupForeground()
val window = Window(this)
window.open() CoroutineScope(Dispatchers.IO).launch {
applicationContext.streamSettings()
.collectLatest { settings ->
windows.forEach { it.close() }
windows.clear()
if (settings.source != SelectedSource.NONE) {
Window(this@ForegroundService, PowerbarLocation.BOTTOM).apply {
selectedSource = settings.source
windows.add(this)
open()
}
}
if (settings.topBarSource != SelectedSource.NONE){
Window(this@ForegroundService, PowerbarLocation.TOP).apply {
selectedSource = settings.topBarSource
open()
windows.add(this)
}
}
}
}
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@ -45,7 +72,7 @@ class ForegroundService : Service() {
val notification: Notification = notificationBuilder.setOngoing(true) val notification: Notification = notificationBuilder.setOngoing(true)
.setContentTitle("Powerbar service running") .setContentTitle("Powerbar service running")
.setContentText("Displaying on top of other apps") .setContentText("Displaying on top of other apps")
.setSmallIcon(R.drawable.ic_menu_add) .setSmallIcon(R.drawable.ic_launcher)
.setPriority(NotificationManager.IMPORTANCE_MIN) .setPriority(NotificationManager.IMPORTANCE_MIN)
.setCategory(Notification.CATEGORY_SERVICE) .setCategory(Notification.CATEGORY_SERVICE)
.build() .build()

View File

@ -20,26 +20,34 @@ import io.hammerhead.karooext.models.UserProfile
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
fun remap(value: Double, fromMin: Double, fromMax: Double, toMin: Double, toMax: Double): Double { fun remap(value: Double, fromMin: Double, fromMax: Double, toMin: Double, toMax: Double): Double {
return (value - fromMin) * (toMax - toMin) / (fromMax - fromMin) + toMin return (value - fromMin) * (toMax - toMin) / (fromMax - fromMin) + toMin
} }
enum class PowerbarLocation {
TOP, BOTTOM
}
class Window( class Window(
private val context: Context private val context: Context,
val powerbarLocation: PowerbarLocation = PowerbarLocation.BOTTOM
) { ) {
private val rootView: View private val rootView: View
private var layoutParams: WindowManager.LayoutParams? = null private var layoutParams: WindowManager.LayoutParams? = null
private val windowManager: WindowManager private val windowManager: WindowManager
private val layoutInflater: LayoutInflater private val layoutInflater: LayoutInflater
private val powerbar: CustomProgressBar private val powerbar: CustomProgressBar
var selectedSource: SelectedSource = SelectedSource.POWER
init { init {
layoutParams = WindowManager.LayoutParams( layoutParams = WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT,
@ -68,7 +76,15 @@ class Window(
windowManager.defaultDisplay.getMetrics(displayMetrics) windowManager.defaultDisplay.getMetrics(displayMetrics)
} }
layoutParams?.gravity = Gravity.BOTTOM layoutParams?.gravity = when (powerbarLocation) {
PowerbarLocation.TOP -> Gravity.TOP
PowerbarLocation.BOTTOM -> Gravity.BOTTOM
}
if (powerbarLocation == PowerbarLocation.TOP) {
layoutParams?.y = 10
} else {
layoutParams?.y = 0
}
layoutParams?.width = displayMetrics.widthPixels layoutParams?.width = displayMetrics.widthPixels
layoutParams?.alpha = 1.0f layoutParams?.alpha = 1.0f
} }
@ -79,36 +95,37 @@ class Window(
private var serviceJob: Job? = null private var serviceJob: Job? = null
fun open() { suspend fun open() {
serviceJob = CoroutineScope(Dispatchers.IO).launch { serviceJob = CoroutineScope(Dispatchers.Default).launch {
karooSystem.connect { connected -> karooSystem.connect { connected ->
if (connected) { if (connected) {
Log.i(KarooPowerbarExtension.TAG, "Connected") Log.i(KarooPowerbarExtension.TAG, "Connected")
} }
} }
context.streamSettings().distinctUntilChanged().collectLatest { settings ->
powerbar.progressColor = context.resources.getColor(R.color.zoneAerobic) powerbar.progressColor = context.resources.getColor(R.color.zoneAerobic)
powerbar.progress = 0.0 powerbar.progress = 0.0
powerbar.invalidate() powerbar.invalidate()
Log.i(KarooPowerbarExtension.TAG, "Streaming ${settings.source}") Log.i(KarooPowerbarExtension.TAG, "Streaming $selectedSource")
when (settings.source){ when (selectedSource){
SelectedSource.POWER -> streamPower(PowerStreamSmoothing.RAW) SelectedSource.POWER -> streamPower(PowerStreamSmoothing.RAW)
SelectedSource.POWER_3S -> streamPower(PowerStreamSmoothing.SMOOTHED_3S) SelectedSource.POWER_3S -> streamPower(PowerStreamSmoothing.SMOOTHED_3S)
SelectedSource.POWER_10S -> streamPower(PowerStreamSmoothing.SMOOTHED_10S) SelectedSource.POWER_10S -> streamPower(PowerStreamSmoothing.SMOOTHED_10S)
SelectedSource.HEART_RATE -> streamHeartrate() SelectedSource.HEART_RATE -> streamHeartrate()
} else -> {}
} }
} }
try { try {
withContext(Dispatchers.Main) {
if (rootView.windowToken == null && rootView.parent == null) { if (rootView.windowToken == null && rootView.parent == null) {
windowManager.addView(rootView, layoutParams) windowManager.addView(rootView, layoutParams)
} }
}
} catch (e: Exception) { } catch (e: Exception) {
Log.d(KarooPowerbarExtension.TAG, e.toString()) Log.e(KarooPowerbarExtension.TAG, e.toString())
} }
} }

View File

@ -44,6 +44,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
enum class SelectedSource(val id: String, val label: String) { enum class SelectedSource(val id: String, val label: String) {
NONE("none", "None"),
HEART_RATE("hr", "Heart Rate"), HEART_RATE("hr", "Heart Rate"),
POWER("power", "Power"), POWER("power", "Power"),
POWER_3S("power_3s", "Power (3 second avg)"), POWER_3S("power_3s", "Power (3 second avg)"),
@ -58,7 +59,9 @@ fun MainScreen() {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val karooSystem = remember { KarooSystemService(ctx) } val karooSystem = remember { KarooSystemService(ctx) }
var selectedSource by remember { mutableStateOf(SelectedSource.POWER) } var bottomSelectedSource by remember { mutableStateOf(SelectedSource.POWER) }
var topSelectedSource by remember { mutableStateOf(SelectedSource.NONE) }
var savedDialogVisible by remember { mutableStateOf(false) } var savedDialogVisible by remember { mutableStateOf(false) }
var showAlerts by remember { mutableStateOf(false) } var showAlerts by remember { mutableStateOf(false) }
var givenPermissions by remember { mutableStateOf(false) } var givenPermissions by remember { mutableStateOf(false) }
@ -67,7 +70,8 @@ fun MainScreen() {
givenPermissions = Settings.canDrawOverlays(ctx) givenPermissions = Settings.canDrawOverlays(ctx)
ctx.streamSettings().collect { settings -> ctx.streamSettings().collect { settings ->
selectedSource = settings.source bottomSelectedSource = settings.source
topSelectedSource = settings.topBarSource
} }
} }
@ -98,18 +102,30 @@ fun MainScreen() {
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(10.dp)) { .fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(10.dp)) {
val powerSourceDropdownOptions = SelectedSource.entries.toList().map { unit -> DropdownOption(unit.id, unit.label) } apply {
val powerSourceInitialSelection by remember(selectedSource) { val dropdownOptions = SelectedSource.entries.toList().map { unit -> DropdownOption(unit.id, unit.label) }
mutableStateOf(powerSourceDropdownOptions.find { option -> option.id == selectedSource.id }!!) val dropdownInitialSelection by remember(bottomSelectedSource) {
mutableStateOf(dropdownOptions.find { option -> option.id == bottomSelectedSource.id }!!)
}
Dropdown(label = "Bottom Bar", options = dropdownOptions, selected = dropdownInitialSelection) { selectedOption ->
bottomSelectedSource = SelectedSource.entries.find { unit -> unit.id == selectedOption.id }!!
}
}
apply {
val dropdownOptions = SelectedSource.entries.toList().map { unit -> DropdownOption(unit.id, unit.label) }
val dropdownInitialSelection by remember(topSelectedSource) {
mutableStateOf(dropdownOptions.find { option -> option.id == topSelectedSource.id }!!)
}
Dropdown(label = "Top Bar", options = dropdownOptions, selected = dropdownInitialSelection) { selectedOption ->
topSelectedSource = SelectedSource.entries.find { unit -> unit.id == selectedOption.id }!!
} }
Dropdown(label = "Data Source", options = powerSourceDropdownOptions, selected = powerSourceInitialSelection) { selectedOption ->
selectedSource = SelectedSource.entries.find { unit -> unit.id == selectedOption.id }!!
} }
FilledTonalButton(modifier = Modifier FilledTonalButton(modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(50.dp), onClick = { .height(50.dp), onClick = {
val newSettings = PowerbarSettings(source = selectedSource) val newSettings = PowerbarSettings(source = bottomSelectedSource, topBarSource = topSelectedSource)
coroutineScope.launch { coroutineScope.launch {
saveSettings(ctx, newSettings) saveSettings(ctx, newSettings)