From dbaac0cec0fb8b609331cc823585a17d9fd457bd Mon Sep 17 00:00:00 2001 From: Tim Kluge Date: Sun, 8 Dec 2024 17:05:11 +0100 Subject: [PATCH] fix #1: Add ability to a second power bar to the top of the screen --- .../karoopowerbar/CustomProgressBar.kt | 45 ++++++++++----- .../de/timklge/karoopowerbar/Extensions.kt | 1 + .../karoopowerbar/ForegroundService.kt | 37 +++++++++++-- .../kotlin/de/timklge/karoopowerbar/Window.kt | 55 ++++++++++++------- .../karoopowerbar/screens/MainScreen.kt | 32 ++++++++--- 5 files changed, 125 insertions(+), 45 deletions(-) diff --git a/app/src/main/kotlin/de/timklge/karoopowerbar/CustomProgressBar.kt b/app/src/main/kotlin/de/timklge/karoopowerbar/CustomProgressBar.kt index 981c04a..0ba00b5 100644 --- a/app/src/main/kotlin/de/timklge/karoopowerbar/CustomProgressBar.kt +++ b/app/src/main/kotlin/de/timklge/karoopowerbar/CustomProgressBar.kt @@ -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) } } \ No newline at end of file diff --git a/app/src/main/kotlin/de/timklge/karoopowerbar/Extensions.kt b/app/src/main/kotlin/de/timklge/karoopowerbar/Extensions.kt index 187f136..1f2f373 100644 --- a/app/src/main/kotlin/de/timklge/karoopowerbar/Extensions.kt +++ b/app/src/main/kotlin/de/timklge/karoopowerbar/Extensions.kt @@ -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()) diff --git a/app/src/main/kotlin/de/timklge/karoopowerbar/ForegroundService.kt b/app/src/main/kotlin/de/timklge/karoopowerbar/ForegroundService.kt index 5213a4b..f6f6b4f 100644 --- a/app/src/main/kotlin/de/timklge/karoopowerbar/ForegroundService.kt +++ b/app/src/main/kotlin/de/timklge/karoopowerbar/ForegroundService.kt @@ -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() + 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() diff --git a/app/src/main/kotlin/de/timklge/karoopowerbar/Window.kt b/app/src/main/kotlin/de/timklge/karoopowerbar/Window.kt index 7ec0909..4e6caa6 100644 --- a/app/src/main/kotlin/de/timklge/karoopowerbar/Window.kt +++ b/app/src/main/kotlin/de/timklge/karoopowerbar/Window.kt @@ -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()) } } 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 7121dd4..fa92ca2 100644 --- a/app/src/main/kotlin/de/timklge/karoopowerbar/screens/MainScreen.kt +++ b/app/src/main/kotlin/de/timklge/karoopowerbar/screens/MainScreen.kt @@ -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)