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
) : View(context, attrs) {
var progress: Double = 0.5
var location: PowerbarLocation = PowerbarLocation.BOTTOM
@ColorInt var progressColor: Int = 0xFF2b86e6.toInt()
override fun onDrawForeground(canvas: Canvas) {
@ -48,21 +49,39 @@ class CustomProgressBar @JvmOverloads constructor(
strokeWidth = 2f
}
val rect = RectF(
1f,
1f + 4f,
((canvas.width.toDouble() - 1f) * progress.coerceIn(0.0, 1.0)).toFloat(),
canvas.height.toFloat() - 1f
)
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
)
val corners = 2f
canvas.drawRoundRect(0f, 2f + 4f, canvas.width.toFloat(), canvas.height.toFloat(), 2f, 2f, background)
canvas.drawRoundRect(0f, 2f, canvas.width.toFloat(), canvas.height.toFloat() - 4f, 2f, 2f, background)
if (progress > 0.0) {
canvas.drawRoundRect(rect, corners, corners, blurPaint)
canvas.drawRoundRect(rect, corners, corners, linePaint)
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(
1f,
1f + 4f,
((canvas.width.toDouble() - 1f) * progress.coerceIn(0.0, 1.0)).toFloat(),
canvas.height.toFloat() - 1f
)
canvas.drawRoundRect(0f, 2f + 4f, canvas.width.toFloat(), canvas.height.toFloat(), 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)
}
}
}
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
data class PowerbarSettings(
val source: SelectedSource = SelectedSource.POWER,
val topBarSource: SelectedSource = SelectedSource.NONE,
){
companion object {
val defaultSettings = Json.encodeToString(PowerbarSettings())

View File

@ -1,6 +1,5 @@
package de.timklge.karoopowerbar
import android.R
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
@ -9,18 +8,46 @@ import android.content.Context
import android.content.Intent
import android.os.IBinder
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() {
override fun onBind(intent: Intent?): IBinder {
throw UnsupportedOperationException("Not yet implemented")
}
private val windows = mutableSetOf<Window>()
override fun onCreate() {
super.onCreate()
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 {
@ -45,7 +72,7 @@ class ForegroundService : Service() {
val notification: Notification = notificationBuilder.setOngoing(true)
.setContentTitle("Powerbar service running")
.setContentText("Displaying on top of other apps")
.setSmallIcon(R.drawable.ic_menu_add)
.setSmallIcon(R.drawable.ic_launcher)
.setPriority(NotificationManager.IMPORTANCE_MIN)
.setCategory(Notification.CATEGORY_SERVICE)
.build()

View File

@ -20,26 +20,34 @@ import io.hammerhead.karooext.models.UserProfile
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
fun remap(value: Double, fromMin: Double, fromMax: Double, toMin: Double, toMax: Double): Double {
return (value - fromMin) * (toMax - toMin) / (fromMax - fromMin) + toMin
}
enum class PowerbarLocation {
TOP, BOTTOM
}
class Window(
private val context: Context
private val context: Context,
val powerbarLocation: PowerbarLocation = PowerbarLocation.BOTTOM
) {
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,
@ -68,7 +76,15 @@ class Window(
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?.alpha = 1.0f
}
@ -79,36 +95,37 @@ class Window(
private var serviceJob: Job? = null
fun open() {
serviceJob = CoroutineScope(Dispatchers.IO).launch {
suspend fun open() {
serviceJob = CoroutineScope(Dispatchers.Default).launch {
karooSystem.connect { connected ->
if (connected) {
Log.i(KarooPowerbarExtension.TAG, "Connected")
}
}
context.streamSettings().distinctUntilChanged().collectLatest { settings ->
powerbar.progressColor = context.resources.getColor(R.color.zoneAerobic)
powerbar.progress = 0.0
powerbar.invalidate()
powerbar.progressColor = context.resources.getColor(R.color.zoneAerobic)
powerbar.progress = 0.0
powerbar.invalidate()
Log.i(KarooPowerbarExtension.TAG, "Streaming ${settings.source}")
Log.i(KarooPowerbarExtension.TAG, "Streaming $selectedSource")
when (settings.source){
SelectedSource.POWER -> streamPower(PowerStreamSmoothing.RAW)
SelectedSource.POWER_3S -> streamPower(PowerStreamSmoothing.SMOOTHED_3S)
SelectedSource.POWER_10S -> streamPower(PowerStreamSmoothing.SMOOTHED_10S)
SelectedSource.HEART_RATE -> streamHeartrate()
}
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()
else -> {}
}
}
try {
if (rootView.windowToken == null && rootView.parent == null) {
windowManager.addView(rootView, layoutParams)
withContext(Dispatchers.Main) {
if (rootView.windowToken == null && rootView.parent == null) {
windowManager.addView(rootView, layoutParams)
}
}
} 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
enum class SelectedSource(val id: String, val label: String) {
NONE("none", "None"),
HEART_RATE("hr", "Heart Rate"),
POWER("power", "Power"),
POWER_3S("power_3s", "Power (3 second avg)"),
@ -58,7 +59,9 @@ fun MainScreen() {
val coroutineScope = rememberCoroutineScope()
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 showAlerts by remember { mutableStateOf(false) }
var givenPermissions by remember { mutableStateOf(false) }
@ -67,7 +70,8 @@ fun MainScreen() {
givenPermissions = Settings.canDrawOverlays(ctx)
ctx.streamSettings().collect { settings ->
selectedSource = settings.source
bottomSelectedSource = settings.source
topSelectedSource = settings.topBarSource
}
}
@ -98,18 +102,30 @@ fun MainScreen() {
.verticalScroll(rememberScrollState())
.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(10.dp)) {
val powerSourceDropdownOptions = SelectedSource.entries.toList().map { unit -> DropdownOption(unit.id, unit.label) }
val powerSourceInitialSelection by remember(selectedSource) {
mutableStateOf(powerSourceDropdownOptions.find { option -> option.id == selectedSource.id }!!)
apply {
val dropdownOptions = SelectedSource.entries.toList().map { unit -> DropdownOption(unit.id, unit.label) }
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 }!!
}
}
Dropdown(label = "Data Source", options = powerSourceDropdownOptions, selected = powerSourceInitialSelection) { selectedOption ->
selectedSource = 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 }!!
}
}
FilledTonalButton(modifier = Modifier
.fillMaxWidth()
.height(50.dp), onClick = {
val newSettings = PowerbarSettings(source = selectedSource)
val newSettings = PowerbarSettings(source = bottomSelectedSource, topBarSource = topSelectedSource)
coroutineScope.launch {
saveSettings(ctx, newSettings)