diff --git a/app/src/main/java/com/sameerasw/essentials/island/plugins/brief/BriefPlugin.kt b/app/src/main/java/com/sameerasw/essentials/island/plugins/brief/BriefPlugin.kt index ae225d3d4..379d7305e 100644 --- a/app/src/main/java/com/sameerasw/essentials/island/plugins/brief/BriefPlugin.kt +++ b/app/src/main/java/com/sameerasw/essentials/island/plugins/brief/BriefPlugin.kt @@ -16,9 +16,12 @@ import com.sameerasw.essentials.island.remote.LocalRemotePaused import com.sameerasw.essentials.island.remote.RemoteExpanded import com.sameerasw.essentials.island.ui.SurfaceBackdrop import android.Manifest +import android.content.BroadcastReceiver import android.content.Context -import android.content.Intent import android.content.IntentFilter +import android.os.PowerManager +import androidx.compose.runtime.DisposableEffect +import android.content.Intent import android.content.pm.PackageManager import android.content.ContentUris import androidx.compose.foundation.Image @@ -75,7 +78,6 @@ import com.sameerasw.essentials.island.plugins.soften import com.sameerasw.essentials.island.ui.IslandHaptics import com.sameerasw.essentials.translation.TranslatableText import kotlinx.coroutines.launch -import android.os.BatteryManager import android.os.Build import com.sameerasw.essentials.utils.BluetoothBatteryUtils import com.sameerasw.essentials.utils.BluetoothDeviceIcons @@ -123,6 +125,10 @@ import com.sameerasw.essentials.island.plugins.BaseIslandPlugin import com.sameerasw.essentials.island.ui.IslandTextStyles import com.sameerasw.essentials.island.ui.islandTextColor import com.sameerasw.essentials.island.ui.components.BatteryGlyph +import com.sameerasw.essentials.island.plugins.timebattery.BatteryStatus +import com.sameerasw.essentials.island.plugins.timebattery.batteryIdleColor +import com.sameerasw.essentials.island.plugins.timebattery.batteryStateColor +import com.sameerasw.essentials.island.plugins.timebattery.readBatteryStatus import com.sameerasw.essentials.island.ui.components.BatteryRing import com.sameerasw.essentials.island.ui.components.CameraRow import com.sameerasw.essentials.utils.CalendarEventUtil @@ -442,7 +448,18 @@ private fun BriefOverview( now = System.currentTimeMillis() } } - val battery = remember(now) { readBattery(context) } + var batteryState by remember { mutableStateOf(null) } + DisposableEffect(context) { + val receiver = object : BroadcastReceiver() { + override fun onReceive(c: Context, intent: Intent) { + batteryState = readBatteryStatus(context, if (intent.action == Intent.ACTION_BATTERY_CHANGED) intent else null) + } + } + val filter = IntentFilter(Intent.ACTION_BATTERY_CHANGED).apply { addAction(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED) } + val sticky = context.registerReceiver(receiver, filter) + batteryState = readBatteryStatus(context, sticky) + onDispose { context.unregisterReceiver(receiver) } + } val alarm = remember(now, alarmHours) { alarmHours?.let { NextAlarm.within(context, it) } } val devices by produceState(BriefDevicesCache.devices, now) { val fresh = withContext(Dispatchers.IO) { readDevices(context) } @@ -457,8 +474,10 @@ private fun BriefOverview( } } + val battery = batteryState val timeSettings = remember { SettingsRepository(context) } val time = IslandTimeFormat.format(context, timeSettings, now) + val batteryColor = battery?.let { batteryStateColor(timeSettings, it) ?: batteryIdleColor(timeSettings) } ?: accent val date = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "EEEMMMd"), Locale.getDefault()).format(Date(now)) val artwork = remember(media?.artwork) { media?.artwork?.asImageBitmap() } @@ -483,11 +502,11 @@ private fun BriefOverview( Spacer(Modifier.width(8.dp)) } } - if (battery >= 0) { + if (battery != null) { if (iconStyle) { - BatteryGlyph(battery, accent, showLevel = true) + BatteryGlyph(battery.level, batteryColor, showLevel = true, charging = battery.charging, powerSave = battery.powerSave, limited = battery.limited) } else { - BatteryRing(battery, accent, showLevel = true) + BatteryRing(battery.level, batteryColor, showLevel = true, charging = battery.charging, powerSave = battery.powerSave, limited = battery.limited) } } }, @@ -617,13 +636,6 @@ private fun BriefEventRow(event: UpcomingCalendarEvent, now: Long, accent: Color } } -private fun readBattery(context: Context): Int { - val intent = context.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED)) ?: return -1 - val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) - val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 100) - return if (level < 0 || scale <= 0) -1 else level * 100 / scale -} - @Composable private fun BriefPlayer(media: MediaSnapshot, artwork: ImageBitmap?, onClick: () -> Unit, modifier: Modifier = Modifier) { val context = LocalContext.current diff --git a/app/src/main/java/com/sameerasw/essentials/island/plugins/timebattery/BatteryStatus.kt b/app/src/main/java/com/sameerasw/essentials/island/plugins/timebattery/BatteryStatus.kt new file mode 100644 index 000000000..dc3d2a346 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/island/plugins/timebattery/BatteryStatus.kt @@ -0,0 +1,70 @@ +package com.sameerasw.essentials.island.plugins.timebattery + +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.graphics.Color as AndroidColor +import android.os.BatteryManager +import android.os.PowerManager +import androidx.compose.ui.graphics.Color +import com.sameerasw.essentials.data.repository.SettingsRepository +import com.sameerasw.essentials.island.plugins.soften +import com.sameerasw.essentials.utils.battery.ChargeLimitUtil + +data class BatteryStatus( + val level: Int, + val charging: Boolean, + val limited: Boolean, + val powerSave: Boolean, +) + +private val AUTO_CHARGING = AndroidColor.rgb(0, 230, 118) + +fun readBatteryStatus(context: Context, intent: Intent? = null): BatteryStatus? { + val batteryIntent = intent ?: context.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED)) ?: return null + val raw = batteryIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) + val scale = batteryIntent.getIntExtra(BatteryManager.EXTRA_SCALE, 100) + if (raw < 0 || scale <= 0) return null + val status = batteryIntent.getIntExtra(BatteryManager.EXTRA_STATUS, -1) + val level = (raw * 100f / scale).toInt() + val plugged = status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL + val limited = ChargeLimitUtil.isShielded(context, level, plugged) + return BatteryStatus( + level = level, + charging = plugged && !limited, + limited = limited, + powerSave = (context.getSystemService(Context.POWER_SERVICE) as? PowerManager)?.isPowerSaveMode ?: false, + ) +} + +fun batteryStateColor(settings: SettingsRepository, status: BatteryStatus): Color? { + fun parse(hex: String, fallback: Int) = try { + AndroidColor.parseColor(hex) + } catch (_: Exception) { + fallback + } + val charge = settings.getDuoBatteryChargingColor() + val level = status.level + val raw = when { + status.charging && settings.isDuoBatteryChargingColorEnabled() -> + if (charge.equals("auto", ignoreCase = true)) AUTO_CHARGING else parse(charge, AUTO_CHARGING) + status.powerSave && settings.isDuoBatteryPowerSaveColorEnabled() -> + parse(settings.getDuoBatteryPowerSaveColor(), AndroidColor.rgb(255, 152, 0)) + level in 0..settings.getDuoBatteryCriticalLevel() && settings.isDuoBatteryCriticalColorEnabled() -> + parse(settings.getDuoBatteryCriticalColor(), AndroidColor.rgb(244, 67, 54)) + level in 0..settings.getDuoBatteryLowLevel() && settings.isDuoBatteryLowColorEnabled() -> + parse(settings.getDuoBatteryLowColor(), AndroidColor.rgb(255, 235, 59)) + else -> return null + } + return Color(soften(raw)) +} + +fun batteryIdleColor(settings: SettingsRepository): Color? { + if (!settings.isIslandBatteryIdleColorEnabled()) return null + val raw = try { + AndroidColor.parseColor(settings.getIslandBatteryIdleColor()) + } catch (_: Exception) { + AndroidColor.WHITE + } + return Color(soften(raw)) +} diff --git a/app/src/main/java/com/sameerasw/essentials/island/plugins/timebattery/TimeBatteryPlugin.kt b/app/src/main/java/com/sameerasw/essentials/island/plugins/timebattery/TimeBatteryPlugin.kt index ca090cc1e..75e785237 100644 --- a/app/src/main/java/com/sameerasw/essentials/island/plugins/timebattery/TimeBatteryPlugin.kt +++ b/app/src/main/java/com/sameerasw/essentials/island/plugins/timebattery/TimeBatteryPlugin.kt @@ -17,8 +17,6 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter -import android.graphics.Color as AndroidColor -import android.os.BatteryManager import android.provider.AlarmClock import android.os.PowerManager import android.text.format.DateFormat @@ -35,7 +33,6 @@ import com.sameerasw.essentials.island.model.IslandPriority import com.sameerasw.essentials.island.model.LineContent import com.sameerasw.essentials.island.model.PluginRequest import com.sameerasw.essentials.island.plugins.BaseIslandPlugin -import com.sameerasw.essentials.island.plugins.soften import com.sameerasw.essentials.island.ui.components.BatteryGlyph import com.sameerasw.essentials.island.ui.components.BatteryRing import com.sameerasw.essentials.island.ui.components.IslandIcon @@ -203,16 +200,11 @@ class TimeBatteryPlugin : BaseIslandPlugin() { } private fun readBattery(intent: Intent?) { - val batteryIntent = intent ?: context.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED)) ?: return - val raw = batteryIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) - val scale = batteryIntent.getIntExtra(BatteryManager.EXTRA_SCALE, 100) - if (raw < 0 || scale <= 0) return - val status = batteryIntent.getIntExtra(BatteryManager.EXTRA_STATUS, -1) - level = (raw * 100f / scale).toInt() - val plugged = status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL - shielded = com.sameerasw.essentials.utils.battery.ChargeLimitUtil.isShielded(context, level, plugged) - charging = plugged && !shielded - powerSave = (context.getSystemService(Context.POWER_SERVICE) as? PowerManager)?.isPowerSaveMode ?: false + val status = readBatteryStatus(context, intent) ?: return + level = status.level + shielded = status.limited + charging = status.charging + powerSave = status.powerSave } private fun severity(): Int = when { @@ -307,7 +299,7 @@ class TimeBatteryPlugin : BaseIslandPlugin() { CompactCell("battery") { when { iconStyle -> BatteryGlyph(batteryLevel, displayColor ?: islandAccent(), showLevel = showLevel, charging = isCharging, powerSave = isSaving, limited = isLimited) - else -> BatteryRing(batteryLevel, displayColor ?: islandAccent(), showLevel = showLevel) + else -> BatteryRing(batteryLevel, displayColor ?: islandAccent(), showLevel = showLevel, charging = isCharging, powerSave = isSaving, limited = isLimited) } }, ), @@ -316,8 +308,8 @@ class TimeBatteryPlugin : BaseIslandPlugin() { LineContent( icon = { when { - iconStyle -> BatteryGlyph(batteryLevel, displayColor ?: islandAccent(), showLevel = false) - else -> BatteryRing(batteryLevel, displayColor ?: islandAccent(), showLevel = false) + iconStyle -> BatteryGlyph(batteryLevel, displayColor ?: islandAccent(), showLevel = false, charging = isCharging, powerSave = isSaving, limited = isLimited) + else -> BatteryRing(batteryLevel, displayColor ?: islandAccent(), showLevel = false, charging = isCharging, powerSave = isSaving, limited = isLimited) } }, start = "$batteryLevel%", @@ -345,39 +337,11 @@ class TimeBatteryPlugin : BaseIslandPlugin() { } } - private fun idleColor(): Color? { - if (!settings.isIslandBatteryIdleColorEnabled()) return null - val raw = try { - AndroidColor.parseColor(settings.getIslandBatteryIdleColor()) - } catch (_: Exception) { - AndroidColor.WHITE - } - return Color(soften(raw)) - } + private fun idleColor(): Color? = batteryIdleColor(settings) - private fun stateColor(): Color? { - fun parse(hex: String, fallback: Int) = try { - AndroidColor.parseColor(hex) - } catch (_: Exception) { - fallback - } - val charge = settings.getDuoBatteryChargingColor() - val raw = when { - charging && settings.isDuoBatteryChargingColorEnabled() -> - if (charge.equals("auto", ignoreCase = true)) AUTO_CHARGING else parse(charge, AUTO_CHARGING) - powerSave && settings.isDuoBatteryPowerSaveColorEnabled() -> - parse(settings.getDuoBatteryPowerSaveColor(), AndroidColor.rgb(255, 152, 0)) - level in 0..settings.getDuoBatteryCriticalLevel() && settings.isDuoBatteryCriticalColorEnabled() -> - parse(settings.getDuoBatteryCriticalColor(), AndroidColor.rgb(244, 67, 54)) - level in 0..settings.getDuoBatteryLowLevel() && settings.isDuoBatteryLowColorEnabled() -> - parse(settings.getDuoBatteryLowColor(), AndroidColor.rgb(255, 235, 59)) - else -> return null - } - return Color(soften(raw)) - } + private fun stateColor(): Color? = batteryStateColor(settings, BatteryStatus(level, charging, shielded, powerSave)) private companion object { const val BATTERY_KEY = "battery" - val AUTO_CHARGING = AndroidColor.rgb(0, 230, 118) } } diff --git a/app/src/main/java/com/sameerasw/essentials/island/ui/components/IslandComponents.kt b/app/src/main/java/com/sameerasw/essentials/island/ui/components/IslandComponents.kt index 3c64532ed..3ef827da5 100644 --- a/app/src/main/java/com/sameerasw/essentials/island/ui/components/IslandComponents.kt +++ b/app/src/main/java/com/sameerasw/essentials/island/ui/components/IslandComponents.kt @@ -12,7 +12,10 @@ import androidx.compose.animation.togetherWith import androidx.compose.foundation.Canvas import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize @@ -64,7 +67,9 @@ import androidx.compose.ui.platform.LocalFontFamilyResolver import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.offset import com.sameerasw.essentials.island.state.IslandStillness import com.sameerasw.essentials.island.ui.IslandFontFamily import com.sameerasw.essentials.island.ui.IslandMotion @@ -264,7 +269,22 @@ fun EqualizerBars( } @Composable -fun BatteryRing(level: Int, color: Color, modifier: Modifier = Modifier, showLevel: Boolean = false, size: Dp = if (showLevel) 24.dp else 18.dp) { +fun BatteryRing( + level: Int, + color: Color, + modifier: Modifier = Modifier, + showLevel: Boolean = false, + size: Dp = if (showLevel) 24.dp else 18.dp, + charging: Boolean = false, + powerSave: Boolean = false, + limited: Boolean = false, +) { + val symbol = when { + limited -> 3 + charging -> 1 + powerSave -> 2 + else -> 0 + } val sweep by animateFloatAsState(360f * level.coerceIn(0, 100) / 100f, IslandMotion.float(), label = "batterySweep") val ringColor by animateColorAsState(color, label = "batteryColor") var ringPx by remember { mutableStateOf(0) } @@ -280,7 +300,21 @@ fun BatteryRing(level: Int, color: Color, modifier: Modifier = Modifier, showLev drawArc(ringColor, -90f, sweep, false, topLeft, arcSize, style = Stroke(stroke, cap = StrokeCap.Round)) } } - if (showLevel && ringPx > 0) { + if (symbol != 0) { + Canvas(Modifier.fillMaxSize(0.46f)) { + when (symbol) { + 1 -> drawPath(batteryBoltPath(this.size.width, this.size.height), ringColor) + 3 -> drawPath(batteryShieldPath(this.size.width, this.size.height), ringColor) + else -> { + val arm = this.size.width * 0.42f + val c = center + val w = this.size.width * 0.2f + drawLine(ringColor, Offset(c.x - arm, c.y), Offset(c.x + arm, c.y), w, StrokeCap.Round) + drawLine(ringColor, Offset(c.x, c.y - arm), Offset(c.x, c.y + arm), w, StrokeCap.Round) + } + } + } + } else if (showLevel && ringPx > 0) { val label = level.coerceIn(0, 100).toString() val innerPx = ringPx * (1f - 2f * 0.12f) * 0.8f val fontPx = minOf(innerPx / (label.length * 0.62f), innerPx * 0.75f) @@ -337,7 +371,7 @@ fun BatteryGlyph( if (!showLevel) return@drawBehind paint.typeface = typeface paint.color = labelColor.toArgb() - paint.textSize = size.height * (if (label.length > 2) 0.82f else 0.94f) + paint.textSize = size.height * (if (label.length > 2) 0.74f else 0.84f) paint.letterSpacing = -0.04f paint.getTextBounds(label, 0, label.length, bounds) val textWidth = paint.measureText(label) @@ -360,21 +394,26 @@ fun BatteryGlyph( if (symbol != 0) { Canvas( Modifier - .then(if (symbol == 3) Modifier else Modifier.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)) .layout { measurable, constraints -> - val placeable = measurable.measure(constraints) val overlap = 7.dp.roundToPx() + val placeable = measurable.measure(constraints.offset(horizontal = overlap)) layout((placeable.width - overlap).coerceAtLeast(0), placeable.height) { placeable.place(-overlap, 0) } } - .then(if (symbol == 3) Modifier.size(9.dp, 11.dp) else Modifier.size(11.dp, 13.dp)), + .then(if (symbol == 2) Modifier.size(11.dp, 13.dp) else Modifier.size(9.dp, 11.dp)), ) { val stroke = 1.8.dp.toPx() val gap = 0.8.dp.toPx() + val outline = stroke * 0.7f + val cutout = { path: Path -> + drawPath(path, Color.Black, style = Stroke(gap * 3 + outline, join = StrokeJoin.Round), blendMode = BlendMode.Clear) + drawPath(path, Color.Black, blendMode = BlendMode.Clear) + drawPath(path, Color.White, style = Stroke(outline, join = StrokeJoin.Round)) + } if (symbol == 2) { val arm = size.width * 0.42f val c = center for ((width, paintColor, mode) in listOf( - Triple(stroke + gap * 2, Color.Black, BlendMode.Clear), + Triple(stroke + gap * 3, Color.Black, BlendMode.Clear), Triple(stroke, Color.White, BlendMode.SrcOver), )) { drawLine(paintColor, Offset(c.x - arm, c.y), Offset(c.x + arm, c.y), width, StrokeCap.Round, blendMode = mode) @@ -383,37 +422,50 @@ fun BatteryGlyph( } else if (symbol == 3) { val w = size.width val h = size.height - val shield = Path().apply { - moveTo(w * 0.5f, h * 0.04f) - lineTo(w * 0.94f, h * 0.2f) - lineTo(w * 0.94f, h * 0.5f) - cubicTo(w * 0.94f, h * 0.76f, w * 0.74f, h * 0.92f, w * 0.5f, h * 0.98f) - cubicTo(w * 0.26f, h * 0.92f, w * 0.06f, h * 0.76f, w * 0.06f, h * 0.5f) - lineTo(w * 0.06f, h * 0.2f) - close() - } - val outline = stroke * 0.7f - drawPath(shield, Color.Black, style = Stroke(gap * 3 + outline, join = StrokeJoin.Round), blendMode = BlendMode.Clear) - drawPath(shield, Color.Black, blendMode = BlendMode.Clear) - drawPath(shield, Color.White, style = Stroke(outline, join = StrokeJoin.Round)) + val shield = batteryShieldPath(w, h) + cutout(shield) clipRect(right = w * 0.5f) { drawPath(shield, Color.White) } } else { val w = size.width val h = size.height - val bolt = Path().apply { - moveTo(w * 0.62f, 0f) - lineTo(w * 0.08f, h * 0.58f) - lineTo(w * 0.46f, h * 0.58f) - lineTo(w * 0.36f, h) - lineTo(w * 0.92f, h * 0.40f) - lineTo(w * 0.54f, h * 0.40f) - close() - } - drawPath(bolt, Color.Black, style = Stroke(gap * 2, join = StrokeJoin.Round), blendMode = BlendMode.Clear) - drawPath(bolt, Color.White, style = Stroke(stroke * 0.3f, join = StrokeJoin.Round)) + val bolt = batteryBoltPath(w, h) + cutout(bolt) drawPath(bolt, Color.White) } } } } } + +private fun batteryShieldPath(w: Float, h: Float) = Path().apply { + moveTo(w * 0.5f, h * 0.04f) + lineTo(w * 0.94f, h * 0.2f) + lineTo(w * 0.94f, h * 0.5f) + cubicTo(w * 0.94f, h * 0.76f, w * 0.74f, h * 0.92f, w * 0.5f, h * 0.98f) + cubicTo(w * 0.26f, h * 0.92f, w * 0.06f, h * 0.76f, w * 0.06f, h * 0.5f) + lineTo(w * 0.06f, h * 0.2f) + close() +} + +private fun batteryBoltPath(w: Float, h: Float) = Path().apply { + moveTo(w * 0.62f, 0f) + lineTo(w * 0.08f, h * 0.58f) + lineTo(w * 0.46f, h * 0.58f) + lineTo(w * 0.36f, h) + lineTo(w * 0.92f, h * 0.40f) + lineTo(w * 0.54f, h * 0.40f) + close() +} + +@Preview(backgroundColor = 0xFF000000, showBackground = true) +@Composable +private fun BatteryGlyphPreview() { + Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { + BatteryGlyph(87, Color(0xFF3DDC84)) + BatteryGlyph(87, Color(0xFF3DDC84), charging = true) + BatteryGlyph(45, Color(0xFFFFC107), powerSave = true) + BatteryGlyph(80, Color(0xFF3DDC84), limited = true) + BatteryGlyph(100, Color(0xFF3DDC84), charging = true) + BatteryGlyph(15, Color(0xFFFF5252), showLevel = false, charging = true) + } +}