Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,10 @@ class SettingsRepository(
const val KEY_ISLAND_BATTERY_ICON_CONDITIONAL = "island_battery_icon_conditional"
const val KEY_ISLAND_BATTERY_ONLY_LOW = "island_battery_only_low"
const val KEY_ISLAND_DEVICES_BATTERY_ONLY_LOW = "island_devices_battery_only_low"
const val KEY_ISLAND_SHOW_BATTERY_ALERTS = "island_show_battery_alerts"
const val KEY_ISLAND_BATTERY_ALERT_CHARGING = "island_battery_alert_charging"
const val KEY_ISLAND_BATTERY_ALERT_LOW = "island_battery_alert_low"
const val KEY_ISLAND_BATTERY_ALERT_CRITICAL = "island_battery_alert_critical"
const val ISLAND_BATTERY_LOW_LEVEL = 20
const val ISLAND_BATTERY_CRITICAL_LEVEL = 10
const val KEY_ISLAND_EXPANDED_SCALE = "island_expanded_scale"
Expand Down Expand Up @@ -3896,6 +3900,9 @@ class SettingsRepository(
fun isIslandShowTimeBatteryEnabled(): Boolean = getBoolean(KEY_ISLAND_SHOW_TIME_BATTERY, false)
fun setIslandShowTimeBatteryEnabled(enabled: Boolean) = putBoolean(KEY_ISLAND_SHOW_TIME_BATTERY, enabled)

fun isIslandShowBatteryAlertsEnabled(): Boolean = getBoolean(KEY_ISLAND_SHOW_BATTERY_ALERTS, true)
fun setIslandShowBatteryAlertsEnabled(enabled: Boolean) = putBoolean(KEY_ISLAND_SHOW_BATTERY_ALERTS, enabled)

fun getIslandBatteryStyle(): String =
getString(KEY_ISLAND_BATTERY_STYLE, ISLAND_BATTERY_STYLE_RING) ?: ISLAND_BATTERY_STYLE_RING
fun setIslandBatteryStyle(value: String) = putString(KEY_ISLAND_BATTERY_STYLE, value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -762,6 +762,12 @@ object FeatureRegistry {
R.string.island_battery_style_title,
"island_show_time_battery",
),
SearchSetting(
R.string.island_battery_alerts_title,
R.string.island_battery_alerts_options_title,
"island_show_battery_alerts",
R.array.keywords_battery,
),
SearchSetting(
R.string.island_show_timers_title,
R.string.island_stopwatch_default,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ object IslandPriority {
const val CALL = -10
const val TIME = 0
const val BATTERY = 1
const val BATTERY_ALERT = 2

const val TIMER_OVERRIDE = 5
const val CALENDAR_OVERRIDE = 6
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
package com.sameerasw.essentials.island.plugins.batteryalerts

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 androidx.compose.ui.graphics.Color
import com.sameerasw.essentials.R
import com.sameerasw.essentials.data.repository.SettingsRepository
import com.sameerasw.essentials.island.model.CompactCell
import com.sameerasw.essentials.island.model.CompactPlacement
import com.sameerasw.essentials.island.model.IslandItem
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 androidx.core.graphics.toColorInt

class BatteryAlertsPlugin : BaseIslandPlugin() {
override val id = "battery_alerts_plugin"

override val settingKeys = setOf(
SettingsRepository.KEY_ISLAND_SHOW_BATTERY_ALERTS,
SettingsRepository.KEY_ISLAND_BATTERY_ALERT_CHARGING,
SettingsRepository.KEY_ISLAND_BATTERY_ALERT_LOW,
SettingsRepository.KEY_ISLAND_BATTERY_ALERT_CRITICAL,
SettingsRepository.KEY_DUO_BATTERY_CHARGING_COLOR_ENABLED,
SettingsRepository.KEY_DUO_BATTERY_CHARGING_COLOR,
SettingsRepository.KEY_DUO_BATTERY_LOW_COLOR_ENABLED,
SettingsRepository.KEY_DUO_BATTERY_LOW_COLOR,
SettingsRepository.KEY_DUO_BATTERY_CRITICAL_COLOR_ENABLED,
SettingsRepository.KEY_DUO_BATTERY_CRITICAL_COLOR,
)

private var level = -1
private var isCharging = false
private var registered = false
private var peeking = false

private val endPeek = Runnable {
peeking = false
render()
}

private val receiver = object : BroadcastReceiver() {
override fun onReceive(c: Context?, intent: Intent?) {
if (intent?.action == Intent.ACTION_BATTERY_CHANGED) {
val wasCharging = isCharging
val prevAlertState = currentAlertState()
readBattery(intent)
val newAlertState = currentAlertState()

if ((isCharging && !wasCharging) || (newAlertState != prevAlertState && newAlertState.isNotEmpty())) {
peekAlert()
} else if (!peeking) {
render()
}
}
}
}

override fun onStart() {
context.registerReceiver(receiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
registered = true
}

override fun onStop() {
ctx?.mainHandler?.removeCallbacks(endPeek)
peeking = false
if (registered) {
try { context.unregisterReceiver(receiver) } catch (_: Exception) {}
registered = false
}
}

override fun refresh() {
readBattery(null)
render()
}

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()
isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
status == BatteryManager.BATTERY_STATUS_FULL
}

private fun currentAlertState(): String {
val allowCharging = settings.getBoolean(SettingsRepository.KEY_ISLAND_BATTERY_ALERT_CHARGING, true)
val allowLowPower = settings.getBoolean(SettingsRepository.KEY_ISLAND_BATTERY_ALERT_LOW, true)
val allowCritical = settings.getBoolean(SettingsRepository.KEY_ISLAND_BATTERY_ALERT_CRITICAL, true)

val isLow = level in (SettingsRepository.ISLAND_BATTERY_CRITICAL_LEVEL + 1)..SettingsRepository.ISLAND_BATTERY_LOW_LEVEL
val isCritical = level in 0..SettingsRepository.ISLAND_BATTERY_CRITICAL_LEVEL

return when {
isCharging && allowCharging -> "charging"
isCritical && allowCritical -> "critical"
isLow && allowLowPower -> "low"
else -> ""
}
}

private fun getBatteryColor(state: String): Color {
fun parse(hex: String, fallback: Int) = try {
hex.toColorInt()
} catch (_: Exception) {
fallback
}

val raw = when (state) {
"charging" -> {
val charge = settings.getDuoBatteryChargingColor()
if (charge.equals("auto", ignoreCase = true)) AUTO_CHARGING else parse(charge, AUTO_CHARGING)
}
"low" -> {
parse(settings.getDuoBatteryLowColor(), AndroidColor.rgb(255, 235, 59))
}
"critical" -> {
parse(settings.getDuoBatteryCriticalColor(), AndroidColor.rgb(244, 67, 54))
}
else -> AUTO_CHARGING
}
return Color(soften(raw))
}

private fun peekAlert() {
val c = ctx ?: return
val duration = settings.getIslandPeekDurationMs()
peeking = true
render()
c.request(PluginRequest.Peek(ITEM_KEY, duration))
c.mainHandler.removeCallbacks(endPeek)
c.mainHandler.postDelayed(endPeek, duration + 500L)
}

private fun render() {
if (ctx == null) {
publish(emptyList())
return
}

val masterEnabled = settings.getBoolean(SettingsRepository.KEY_ISLAND_SHOW_BATTERY_ALERTS, true)
if (!masterEnabled || !peeking || level < 0) {
publish(emptyList())
return
}

val state = currentAlertState()
if (state.isEmpty()) {
publish(emptyList())
return
}

val alertTitleRes = when (state) {
"charging" -> R.string.island_battery_alert_charging
"critical" -> R.string.island_battery_alert_critical
"low" -> R.string.island_battery_alert_low
else -> null
}

if (alertTitleRes == null) {
publish(emptyList())
return
}

val alertTitle = context.getString(alertTitleRes)

val alertColor = getBatteryColor(state)

val item = IslandItem(
key = ITEM_KEY,
priority = IslandPriority.BATTERY_ALERT,
placement = CompactPlacement.Dynamic,
accent = alertColor,
compactVisible = false,
compact = listOf(
CompactCell("alert_icon") {
BatteryGlyph(level = level, color = alertColor, showLevel = true)
}
),
line = LineContent(
icon = { BatteryGlyph(level = level, color = alertColor, showLevel = false) },
start = alertTitle,
end = "$level%"
)
)

publish(item)
}

companion object {
const val ITEM_KEY = "battery_alert"
val AUTO_CHARGING = AndroidColor.rgb(0, 230, 118)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.sameerasw.essentials.island.service

import android.provider.Settings
import com.sameerasw.essentials.island.plugins.alarm.AlarmPlugin
import com.sameerasw.essentials.island.plugins.batteryalerts.BatteryAlertsPlugin
import com.sameerasw.essentials.island.plugins.timer.TimerPlugin
import com.sameerasw.essentials.island.plugins.call.CallPlugin
import android.accessibilityservice.AccessibilityService
Expand Down Expand Up @@ -139,6 +140,7 @@ class IslandCoordinator(
SignalPlugin(),
DevicesPlugin(),
BriefPlugin(),
BatteryAlertsPlugin(),
)

private var scope: CoroutineScope? = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import com.sameerasw.essentials.ui.features.display.actions.GestureActionPickerS
import com.sameerasw.essentials.ui.features.display.actions.HorizontalSlideModeSheet
import com.sameerasw.essentials.ui.features.display.actions.horizontalSlideDescription
import com.sameerasw.essentials.ui.features.display.sheets.IslandAlarmOptionsBottomSheet
import com.sameerasw.essentials.ui.features.display.sheets.IslandBatteryAlertsOptionsBottomSheet
import com.sameerasw.essentials.ui.features.display.sheets.IslandBorderOutlineOptionsBottomSheet
import com.sameerasw.essentials.ui.features.display.sheets.IslandBriefOptionsBottomSheet
import com.sameerasw.essentials.ui.features.display.sheets.IslandCallOptionsBottomSheet
Expand Down Expand Up @@ -126,6 +127,7 @@ fun IslandSettingsUI(
var showSoundModeOptionsSheet by remember { mutableStateOf(false) }
var showDevicesBatterySheet by remember { mutableStateOf(false) }
var showTimeBatteryOptionsSheet by remember { mutableStateOf(false) }
var showBatteryAlertsOptionsSheet by remember { mutableStateOf(false) }
var showTimerOptionsSheet by remember { mutableStateOf(false) }
var showCallOptionsSheet by remember { mutableStateOf(false) }
var showSignalOptionsSheet by remember { mutableStateOf(false) }
Expand Down Expand Up @@ -296,6 +298,18 @@ fun IslandSettingsUI(
onSettingsClick = { showTimeBatteryOptionsSheet = true },
modifier = Modifier.highlight(highlightSetting == "island_show_time_battery"),
)
"battery_alerts" -> IconToggleItem(
dragHandle = handle,
iconRes = R.drawable.rounded_battery_alert_24,
title = stringResource(R.string.island_battery_alerts_title),
isChecked = viewModel.isIslandShowBatteryAlerts.value,
onCheckedChange = { checked ->
HapticUtil.performVirtualKeyHaptic(view)
viewModel.setIslandShowBatteryAlerts(checked)
},
onSettingsClick = { showBatteryAlertsOptionsSheet = true },
modifier = Modifier.highlight(highlightSetting == "island_show_battery_alerts"),
)
"flashlight" -> IconToggleItem(
dragHandle = handle,
iconRes = R.drawable.rounded_flashlight_on_24,
Expand Down Expand Up @@ -812,6 +826,12 @@ fun IslandSettingsUI(
)
}

if (showBatteryAlertsOptionsSheet) {
IslandBatteryAlertsOptionsBottomSheet(
onDismissRequest = { showBatteryAlertsOptionsSheet = false },
)
}

if (showDevicesBatterySheet) {
IslandDevicesBatteryBottomSheet(
viewModel = viewModel,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.sameerasw.essentials.ui.features.display.sheets

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.sameerasw.essentials.R
import com.sameerasw.essentials.data.repository.SettingsRepository
import com.sameerasw.essentials.ui.core.containers.RoundedCardContainer
import com.sameerasw.essentials.ui.core.sheets.EssentialsBottomSheet

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun IslandBatteryAlertsOptionsBottomSheet(
onDismissRequest: () -> Unit,
) {
EssentialsBottomSheet(onDismissRequest = onDismissRequest) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.padding(bottom = 24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Text(
text = stringResource(R.string.island_battery_alerts_options_title),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(start = 8.dp, bottom = 4.dp),
)

RoundedCardContainer(spacing = 2.dp, cornerRadius = 24.dp) {
IslandPrefToggle(
settingKey = SettingsRepository.KEY_ISLAND_BATTERY_ALERT_CHARGING,
iconRes = R.drawable.rounded_bolt_24,
title = stringResource(R.string.island_battery_alert_charging_title),
defaultValue = true,
)
IslandPrefToggle(
settingKey = SettingsRepository.KEY_ISLAND_BATTERY_ALERT_LOW,
iconRes = R.drawable.rounded_battery_alert_24,
title = stringResource(R.string.island_battery_alert_low_title),
defaultValue = true,
)
IslandPrefToggle(
settingKey = SettingsRepository.KEY_ISLAND_BATTERY_ALERT_CRITICAL,
iconRes = R.drawable.rounded_battery_alert_24,
title = stringResource(R.string.island_battery_alert_critical_title),
defaultValue = true,
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,18 @@ fun IslandLauncherOnlyToggle(settingKey: String, modifier: Modifier = Modifier)
}

@Composable
fun IslandPrefToggle(settingKey: String, iconRes: Int, title: String, modifier: Modifier = Modifier, onChanged: (Boolean) -> Unit = {}) {
fun IslandPrefToggle(
settingKey: String,
iconRes: Int,
title: String,
modifier: Modifier = Modifier,
defaultValue: Boolean = false,
onChanged: (Boolean) -> Unit = {}
) {
val context = LocalContext.current
val view = LocalView.current
val settings = remember { SettingsRepository(context) }
var checked by remember { mutableStateOf(settings.getBoolean(settingKey, false)) }
var checked by remember { mutableStateOf(settings.getBoolean(settingKey, defaultValue)) }
IconToggleItem(
iconRes = iconRes,
title = title,
Expand Down
Loading
Loading