From c2dd635681f98c38690503c56720309a8ddc2d3f Mon Sep 17 00:00:00 2001
From: SpySethyDev <87845069+SpySethyDev@users.noreply.github.com>
Date: Sat, 3 Oct 2026 14:11:38 +1000
Subject: [PATCH 1/9] Added Island plugin for alerts on charging, low battery
and critical battery
---
.../data/repository/SettingsRepository.kt | 7 +
.../batteryalerts/BatteryAlertsPlugin.kt | 208 ++++++++++++++++++
.../island/service/IslandCoordinator.kt | 2 +
.../ui/features/display/IslandSettingsUI.kt | 20 ++
.../IslandBatteryAlertsOptionsBottomSheet.kt | 61 +++++
.../sheets/IslandLauncherOnlyToggle.kt | 11 +-
.../essentials/viewmodels/MainViewModel.kt | 7 +
app/src/main/res/values/strings.xml | 8 +
8 files changed, 322 insertions(+), 2 deletions(-)
create mode 100644 app/src/main/java/com/sameerasw/essentials/island/plugins/batteryalerts/BatteryAlertsPlugin.kt
create mode 100644 app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandBatteryAlertsOptionsBottomSheet.kt
diff --git a/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt b/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt
index b7a919d01..17f7fc1da 100644
--- a/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt
+++ b/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt
@@ -495,6 +495,10 @@ class SettingsRepository(
const val KEY_ISLAND_BATTERY_PERCENTAGE_CONDITIONAL = "island_battery_percentage_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"
@@ -3684,6 +3688,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)
diff --git a/app/src/main/java/com/sameerasw/essentials/island/plugins/batteryalerts/BatteryAlertsPlugin.kt b/app/src/main/java/com/sameerasw/essentials/island/plugins/batteryalerts/BatteryAlertsPlugin.kt
new file mode 100644
index 000000000..1fc8b298a
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/island/plugins/batteryalerts/BatteryAlertsPlugin.kt
@@ -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
+
+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()
+
+ // Trigger a brief peek when charger is plugged in or alert state changes
+ 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 {
+ AndroidColor.parseColor(hex)
+ } 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,
+ 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)
+ }
+}
diff --git a/app/src/main/java/com/sameerasw/essentials/island/service/IslandCoordinator.kt b/app/src/main/java/com/sameerasw/essentials/island/service/IslandCoordinator.kt
index 500421049..4a56baea2 100644
--- a/app/src/main/java/com/sameerasw/essentials/island/service/IslandCoordinator.kt
+++ b/app/src/main/java/com/sameerasw/essentials/island/service/IslandCoordinator.kt
@@ -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
@@ -118,6 +119,7 @@ class IslandCoordinator(
NetworkPlugin(),
DevicesPlugin(),
BriefPlugin(),
+ BatteryAlertsPlugin(),
)
private var scope: CoroutineScope? = null
diff --git a/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSettingsUI.kt
index 5a847862e..634c30635 100644
--- a/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSettingsUI.kt
+++ b/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSettingsUI.kt
@@ -64,6 +64,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
@@ -114,6 +115,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 showAlarmOptionsSheet by remember { mutableStateOf(false) }
@@ -250,6 +252,18 @@ fun IslandSettingsUI(
modifier = Modifier.highlight(highlightSetting == "island_show_time_battery"),
)
+ IconToggleItem(
+ 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"),
+ )
+
IconToggleItem(
iconRes = R.drawable.rounded_flashlight_on_24,
title = stringResource(R.string.feat_flashlight_title),
@@ -683,6 +697,12 @@ fun IslandSettingsUI(
)
}
+ if (showBatteryAlertsOptionsSheet) {
+ IslandBatteryAlertsOptionsBottomSheet(
+ onDismissRequest = { showBatteryAlertsOptionsSheet = false },
+ )
+ }
+
if (showDevicesBatterySheet) {
IslandDevicesBatteryBottomSheet(
viewModel = viewModel,
diff --git a/app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandBatteryAlertsOptionsBottomSheet.kt b/app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandBatteryAlertsOptionsBottomSheet.kt
new file mode 100644
index 000000000..92f5f342e
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandBatteryAlertsOptionsBottomSheet.kt
@@ -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,
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandLauncherOnlyToggle.kt b/app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandLauncherOnlyToggle.kt
index 1d995ad4e..1fac44d56 100644
--- a/app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandLauncherOnlyToggle.kt
+++ b/app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandLauncherOnlyToggle.kt
@@ -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,
diff --git a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt
index 869462133..da5c176bf 100644
--- a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt
+++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt
@@ -246,6 +246,7 @@ class MainViewModel : ViewModel() {
val isIslandShowCalendar = mutableStateOf(false)
val isIslandShowConsciousGate = mutableStateOf(true)
val isIslandShowTimeBattery = mutableStateOf(false)
+ val isIslandShowBatteryAlerts = mutableStateOf(false)
val isIslandShowFlashlight = mutableStateOf(true)
val islandBatteryStyle = mutableStateOf(SettingsRepository.ISLAND_BATTERY_STYLE_RING)
val isIslandBatteryPercentageEnabled = mutableStateOf(false)
@@ -2303,6 +2304,7 @@ class MainViewModel : ViewModel() {
isIslandShowCalendar.value = settingsRepository.isIslandShowCalendarEnabled()
isIslandShowConsciousGate.value = settingsRepository.isIslandShowConsciousGateEnabled()
isIslandShowTimeBattery.value = settingsRepository.isIslandShowTimeBatteryEnabled()
+ isIslandShowBatteryAlerts.value = settingsRepository.isIslandShowBatteryAlertsEnabled()
isIslandShowFlashlight.value = settingsRepository.isIslandShowFlashlightEnabled()
islandBatteryStyle.value = settingsRepository.getIslandBatteryStyle()
isIslandBatteryPercentageEnabled.value = settingsRepository.isIslandBatteryPercentageEnabled()
@@ -5585,6 +5587,11 @@ class MainViewModel : ViewModel() {
settingsRepository.setIslandBatteryStyle(value)
}
+ fun setIslandShowBatteryAlerts(enabled: Boolean) {
+ isIslandShowBatteryAlerts.value = enabled
+ settingsRepository.setIslandShowBatteryAlertsEnabled(enabled)
+ }
+
fun setIslandBatteryPercentageEnabled(enabled: Boolean) {
isIslandBatteryPercentageEnabled.value = enabled
settingsRepository.setIslandBatteryPercentageEnabled(enabled)
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 060c84c60..ee88a97f0 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -2836,6 +2836,14 @@
Battery style
Ring
Icon
+ Battery alerts
+ Battery alert options
+ Charging connected
+ Low battery (6-20%)
+ Critical battery (≤ 5%)
+ Charging
+ Low battery
+ Critical battery
Value
Font size
Hide complication inside the app
From 9f252ca7e2c7774cdf7124fe4184b406e3ab46be Mon Sep 17 00:00:00 2001
From: SpySethyDev <87845069+SpySethyDev@users.noreply.github.com>
Date: Sat, 3 Oct 2026 14:14:51 +1000
Subject: [PATCH 2/9] Changed battery color function to toColorInt() which is
more updated
---
.../island/plugins/batteryalerts/BatteryAlertsPlugin.kt | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/app/src/main/java/com/sameerasw/essentials/island/plugins/batteryalerts/BatteryAlertsPlugin.kt b/app/src/main/java/com/sameerasw/essentials/island/plugins/batteryalerts/BatteryAlertsPlugin.kt
index 1fc8b298a..b3f1732fa 100644
--- a/app/src/main/java/com/sameerasw/essentials/island/plugins/batteryalerts/BatteryAlertsPlugin.kt
+++ b/app/src/main/java/com/sameerasw/essentials/island/plugins/batteryalerts/BatteryAlertsPlugin.kt
@@ -18,6 +18,7 @@ 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"
@@ -115,7 +116,7 @@ class BatteryAlertsPlugin : BaseIslandPlugin() {
private fun getBatteryColor(state: String): Color {
fun parse(hex: String, fallback: Int) = try {
- AndroidColor.parseColor(hex)
+ hex.toColorInt()
} catch (_: Exception) {
fallback
}
From 23cc0c7607964c5a28ed14f942057506f1ea4506 Mon Sep 17 00:00:00 2001
From: Seth
Date: Wed, 7 Oct 2026 12:16:30 +1100
Subject: [PATCH 3/9] Registered Battery Alerts in FeatureRegistry and cleanup
styles
---
.../sameerasw/essentials/domain/registry/FeatureRegistry.kt | 6 ++++++
.../com/sameerasw/essentials/island/model/IslandModels.kt | 1 +
.../island/plugins/batteryalerts/BatteryAlertsPlugin.kt | 3 +--
.../display/sheets/IslandBatteryAlertsOptionsBottomSheet.kt | 2 +-
app/src/main/res/values/strings.xml | 4 ++--
5 files changed, 11 insertions(+), 5 deletions(-)
diff --git a/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt b/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt
index 6982afeed..31baba684 100644
--- a/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt
+++ b/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt
@@ -759,6 +759,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,
diff --git a/app/src/main/java/com/sameerasw/essentials/island/model/IslandModels.kt b/app/src/main/java/com/sameerasw/essentials/island/model/IslandModels.kt
index f649c7a9b..3918ad742 100644
--- a/app/src/main/java/com/sameerasw/essentials/island/model/IslandModels.kt
+++ b/app/src/main/java/com/sameerasw/essentials/island/model/IslandModels.kt
@@ -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
diff --git a/app/src/main/java/com/sameerasw/essentials/island/plugins/batteryalerts/BatteryAlertsPlugin.kt b/app/src/main/java/com/sameerasw/essentials/island/plugins/batteryalerts/BatteryAlertsPlugin.kt
index b3f1732fa..9a39992a9 100644
--- a/app/src/main/java/com/sameerasw/essentials/island/plugins/batteryalerts/BatteryAlertsPlugin.kt
+++ b/app/src/main/java/com/sameerasw/essentials/island/plugins/batteryalerts/BatteryAlertsPlugin.kt
@@ -54,7 +54,6 @@ class BatteryAlertsPlugin : BaseIslandPlugin() {
readBattery(intent)
val newAlertState = currentAlertState()
- // Trigger a brief peek when charger is plugged in or alert state changes
if ((isCharging && !wasCharging) || (newAlertState != prevAlertState && newAlertState.isNotEmpty())) {
peekAlert()
} else if (!peeking) {
@@ -183,7 +182,7 @@ class BatteryAlertsPlugin : BaseIslandPlugin() {
val item = IslandItem(
key = ITEM_KEY,
- priority = IslandPriority.BATTERY,
+ priority = IslandPriority.BATTERY_ALERT,
placement = CompactPlacement.Dynamic,
accent = alertColor,
compactVisible = false,
diff --git a/app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandBatteryAlertsOptionsBottomSheet.kt b/app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandBatteryAlertsOptionsBottomSheet.kt
index 92f5f342e..22fb33d44 100644
--- a/app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandBatteryAlertsOptionsBottomSheet.kt
+++ b/app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandBatteryAlertsOptionsBottomSheet.kt
@@ -58,4 +58,4 @@ fun IslandBatteryAlertsOptionsBottomSheet(
}
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index ee88a97f0..63842b1e7 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -2839,8 +2839,8 @@
Battery alerts
Battery alert options
Charging connected
- Low battery (6-20%)
- Critical battery (≤ 5%)
+ Low battery (11-20%)
+ Critical battery (0-10%)
Charging
Low battery
Critical battery
From 3682a243a865f63978e35d7a1c25d4b0e118c88d Mon Sep 17 00:00:00 2001
From: Seth
Date: Wed, 7 Oct 2026 12:43:54 +1100
Subject: [PATCH 4/9] Added 'formatted='false'' to strings to fix the
validation error for strings.xml
---
app/src/main/res/values-de/strings.xml | 2 +-
app/src/main/res/values-tr/strings.xml | 2 +-
app/src/main/res/values/strings.xml | 6 +++---
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 1eeb61bf3..e726e2f81 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -1762,7 +1762,7 @@ Zudem muss die Einstellung unter Umständen mehrfach angewendet werden, damit si
Dynamischer Farbverlauf am oberen Rand für bevorstehende Ereignisse und Reisen
Komplikations-Widgets anpassen
Warnung bei niedrigem Akkustand
- Warnung anzeigen, wenn der Akku der Uhr oder des Telefons unter 20 % liegt
+ Warnung anzeigen, wenn der Akku der Uhr oder des Telefons unter 20 % liegt
Sind wir da (Reise-ETA)
Verbleibende Zeit/Entfernung zum Ziel anzeigen
Nächster Alarm
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 9a33fd8e6..b52796195 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -1626,7 +1626,7 @@ Ve ayarın etkili olması için birden fazla kez uygulanması gerekebilir.Ne gösterileceğini seçin
Parıltı göster
Yaklaşan etkinliklere ve seyahate dinamik üst vurgu gradyanı
- Saat veya telefon pil seviyesi %20\'nin altına düştüğünde uyarı göster
+ Saat veya telefon pil seviyesi %20\'nin altına düştüğünde uyarı göster
Pil seviyesi düşük uyarısı
Bugünün yaklaşan toplantılarını göster
Takvim etkinlikleri
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 63842b1e7..a79a892d1 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -2512,7 +2512,7 @@
Complications
Choose what to show
Low battery alert
- Show alert when watch or phone battery is below 20%
+ Show alert when watch or phone battery is below 20%
Flashlight
Show when phone flashlight is on
Are We There (Travel ETA)
@@ -2839,8 +2839,8 @@
Battery alerts
Battery alert options
Charging connected
- Low battery (11-20%)
- Critical battery (0-10%)
+ Low battery (11-20%)
+ Critical battery (0-10%)
Charging
Low battery
Critical battery
From efd72d520516e203b0492cd3419e6472eea1fe5f Mon Sep 17 00:00:00 2001
From: sameerasw
Date: Wed, 7 Oct 2026 10:55:06 +0530
Subject: [PATCH 5/9] feat: add Porter Shizuku bridge integration
---
app/build.gradle.kts | 1 +
app/src/main/AndroidManifest.xml | 1 +
.../com/sameerasw/essentials/EssentialsApp.kt | 5 +++
.../essentials/utils/security/ShizukuUtils.kt | 38 +++++++++++++++++++
4 files changed, 45 insertions(+)
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index b4f9cf939..b4ef78ee9 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -206,6 +206,7 @@ dependencies {
// Shizuku
implementation(libs.shizuku.api)
implementation(libs.shizuku.provider)
+ implementation("com.github.d4rken-org.porter-api:shizuku-bridge:+")
implementation(libs.androidx.car.app)
// Hidden API Bypass
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index d703b77be..f74942df9 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -102,6 +102,7 @@
+
diff --git a/app/src/main/java/com/sameerasw/essentials/EssentialsApp.kt b/app/src/main/java/com/sameerasw/essentials/EssentialsApp.kt
index 697ea779d..3aa5b63d6 100644
--- a/app/src/main/java/com/sameerasw/essentials/EssentialsApp.kt
+++ b/app/src/main/java/com/sameerasw/essentials/EssentialsApp.kt
@@ -19,6 +19,10 @@ import android.os.Looper
import android.widget.Toast
import com.sameerasw.essentials.data.repository.SettingsRepository
import com.sameerasw.essentials.utils.ShizukuUtils
+import eu.darken.porter.bridge.PorterShizukuBridge
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
import io.sentry.android.core.SentryAndroid
import org.lsposed.hiddenapibypass.HiddenApiBypass
@@ -49,6 +53,7 @@ class EssentialsApp : Application() {
} catch (_: Throwable) {
}
+ PorterShizukuBridge.start(CoroutineScope(SupervisorJob() + Dispatchers.Default))
ShizukuUtils.initialize()
com.sameerasw.essentials.utils.CarConnectionMonitor
.initialize(this)
diff --git a/app/src/main/java/com/sameerasw/essentials/utils/security/ShizukuUtils.kt b/app/src/main/java/com/sameerasw/essentials/utils/security/ShizukuUtils.kt
index 54758d30f..b36591727 100644
--- a/app/src/main/java/com/sameerasw/essentials/utils/security/ShizukuUtils.kt
+++ b/app/src/main/java/com/sameerasw/essentials/utils/security/ShizukuUtils.kt
@@ -27,6 +27,14 @@ object ShizukuUtils {
fun isShizukuInstalled(context: Context): Boolean = shizukuPermissionInfo(context) != null
+ private const val PORTER_PACKAGE = "eu.darken.porter"
+ private const val PORTER_ACTION_START = "eu.darken.porter.START"
+ private const val PORTER_ACTION_STOP = "eu.darken.porter.STOP"
+ private const val PORTER_TOKEN_EXTRA = "auth"
+
+ fun isPorterInstalled(context: Context): Boolean =
+ runCatching { context.packageManager.getPackageInfo(PORTER_PACKAGE, 0) }.isSuccess
+
fun getShizukuPackageName(context: Context): String =
shizukuPermissionInfo(context)?.packageName ?: "moe.shizuku.privileged.api"
@@ -188,6 +196,11 @@ object ShizukuUtils {
context: android.content.Context,
start: Boolean,
) {
+ if (!isShizukuInstalled(context) && isPorterInstalled(context)) {
+ togglePorter(context, start)
+ return
+ }
+
val shizukuPackage = getShizukuPackageName(context)
if (isSheveryFork(context, shizukuPackage)) {
@@ -226,4 +239,29 @@ object ShizukuUtils {
android.util.Log.e("ShizukuUtils", "Failed to ${if (start) "start" else "stop"} Shizuku", e)
}
}
+
+ private fun togglePorter(
+ context: Context,
+ start: Boolean,
+ ) {
+ val token =
+ com.sameerasw.essentials.data.repository
+ .SettingsRepository(context)
+ .getShizukuAuthToken()
+ if (token.isEmpty()) {
+ android.util.Log.w("ShizukuUtils", "Porter token is missing, cannot toggle Porter")
+ return
+ }
+ try {
+ val intent =
+ android.content.Intent(if (start) PORTER_ACTION_START else PORTER_ACTION_STOP).apply {
+ `package` = PORTER_PACKAGE
+ putExtra(PORTER_TOKEN_EXTRA, token)
+ addFlags(android.content.Intent.FLAG_INCLUDE_STOPPED_PACKAGES)
+ }
+ context.sendBroadcast(intent)
+ } catch (e: Exception) {
+ android.util.Log.e("ShizukuUtils", "Failed to ${if (start) "start" else "stop"} Porter", e)
+ }
+ }
}
From fc0592b95d3bf29206a11a8175761e1154a88bcb Mon Sep 17 00:00:00 2001
From: sameerasw
Date: Wed, 7 Oct 2026 11:15:30 +0530
Subject: [PATCH 6/9] feat: add privileged mode selection with support for
multiple backends with Dhizuku
---
.../data/repository/SettingsRepository.kt | 1 +
.../receivers/ShizukuActionReceiver.kt | 3 +-
.../ui/activities/SettingsActivity.kt | 12 +-
.../ui/composables/WelcomeScreen.kt | 10 +-
.../ui/core/pickers/PrivilegedModePicker.kt | 111 ++++++++++++++++++
.../essentials/utils/PrivilegedMode.kt | 28 +++++
.../sameerasw/essentials/utils/ShellUtils.kt | 30 ++++-
.../essentials/utils/security/ShizukuUtils.kt | 6 +-
.../essentials/viewmodels/MainViewModel.kt | 26 ++--
app/src/main/res/values/strings.xml | 6 +
10 files changed, 208 insertions(+), 25 deletions(-)
create mode 100644 app/src/main/java/com/sameerasw/essentials/ui/core/pickers/PrivilegedModePicker.kt
create mode 100644 app/src/main/java/com/sameerasw/essentials/utils/PrivilegedMode.kt
diff --git a/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt b/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt
index b657e3506..df98d326b 100644
--- a/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt
+++ b/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt
@@ -303,6 +303,7 @@ class SettingsRepository(
const val KEY_DEFAULT_TAB = "default_tab"
const val KEY_APP_ICON = "app_icon_style"
const val KEY_USE_ROOT = "use_root"
+ const val KEY_PRIVILEGED_MODE = "privileged_mode"
const val KEY_PITCH_BLACK_THEME_ENABLED = "pitch_black_theme_enabled"
const val KEY_ENABLE_UNSUPPORTED_FEATURES = "enable_unsupported_features"
const val KEY_SHOW_LEGACY_FEATURES = "show_legacy_features"
diff --git a/app/src/main/java/com/sameerasw/essentials/services/receivers/ShizukuActionReceiver.kt b/app/src/main/java/com/sameerasw/essentials/services/receivers/ShizukuActionReceiver.kt
index 72ed70721..d9cc5dfc8 100644
--- a/app/src/main/java/com/sameerasw/essentials/services/receivers/ShizukuActionReceiver.kt
+++ b/app/src/main/java/com/sameerasw/essentials/services/receivers/ShizukuActionReceiver.kt
@@ -16,6 +16,7 @@ import android.content.Intent
import android.widget.Toast
import com.sameerasw.essentials.R
import com.sameerasw.essentials.data.repository.SettingsRepository
+import com.sameerasw.essentials.utils.ShellUtils
import com.sameerasw.essentials.utils.ShizukuUtils
class ShizukuActionReceiver : BroadcastReceiver() {
@@ -28,7 +29,7 @@ class ShizukuActionReceiver : BroadcastReceiver() {
context.getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
notificationManager?.cancel(9001)
- if (!ShizukuUtils.isSheveryFork(context)) {
+ if (ShellUtils.usesAuthToken(context)) {
val token = SettingsRepository(context).getShizukuAuthToken()
if (token.isEmpty()) {
Toast
diff --git a/app/src/main/java/com/sameerasw/essentials/ui/activities/SettingsActivity.kt b/app/src/main/java/com/sameerasw/essentials/ui/activities/SettingsActivity.kt
index 79f645f50..fd6407d6a 100644
--- a/app/src/main/java/com/sameerasw/essentials/ui/activities/SettingsActivity.kt
+++ b/app/src/main/java/com/sameerasw/essentials/ui/activities/SettingsActivity.kt
@@ -104,6 +104,7 @@ import com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenuItem
import com.sameerasw.essentials.ui.components.sliders.ConfigSliderItem
import com.sameerasw.essentials.ui.core.cards.FeatureCard
import com.sameerasw.essentials.ui.core.cards.IconToggleItem
+import com.sameerasw.essentials.ui.core.pickers.PrivilegedModePicker
import com.sameerasw.essentials.ui.core.cards.PermissionCard
import com.sameerasw.essentials.ui.core.containers.RoundedCardContainer
import com.sameerasw.essentials.ui.core.pickers.AppIconPicker
@@ -928,15 +929,12 @@ fun SettingsContent(
}
}
- IconToggleItem(
- iconRes = R.drawable.rounded_numbers_24,
- title = stringResource(R.string.setting_use_root_title),
- description = stringResource(R.string.setting_use_root_desc),
- isChecked = viewModel.isRootEnabled.value,
- onCheckedChange = { viewModel.setRootEnabled(it, context) },
+ PrivilegedModePicker(
+ selectedMode = viewModel.privilegedMode.value,
+ onModeSelected = { viewModel.setPrivilegedMode(it, context) },
)
- Row(
+ if (viewModel.usesAuthToken.value) Row(
modifier =
Modifier
.fillMaxWidth()
diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/WelcomeScreen.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/WelcomeScreen.kt
index bf01dce27..b9ba3379a 100644
--- a/app/src/main/java/com/sameerasw/essentials/ui/composables/WelcomeScreen.kt
+++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/WelcomeScreen.kt
@@ -89,6 +89,7 @@ import com.sameerasw.essentials.ui.components.HelpAndGuidesContent
import com.sameerasw.essentials.ui.components.WhatsNewCustomContent
import com.sameerasw.essentials.ui.components.text.SimpleMarkdown
import com.sameerasw.essentials.ui.core.cards.IconToggleItem
+import com.sameerasw.essentials.ui.core.pickers.PrivilegedModePicker
import com.sameerasw.essentials.ui.core.containers.RoundedCardContainer
import com.sameerasw.essentials.ui.core.pickers.CrashReportingPicker
import com.sameerasw.essentials.ui.core.pickers.LanguagePicker
@@ -1092,12 +1093,9 @@ fun PreferencesStepContent(
onCheckedChange = { viewModel.setBlurEnabled(it, context) },
enabled = !isBlurProblematic,
)
- IconToggleItem(
- iconRes = R.drawable.rounded_numbers_24,
- title = stringResource(R.string.setting_use_root_title),
- description = stringResource(R.string.setting_use_root_desc),
- isChecked = isRootEnabled,
- onCheckedChange = { viewModel.setRootEnabled(it, context) },
+ PrivilegedModePicker(
+ selectedMode = viewModel.privilegedMode.value,
+ onModeSelected = { viewModel.setPrivilegedMode(it, context) },
)
}
diff --git a/app/src/main/java/com/sameerasw/essentials/ui/core/pickers/PrivilegedModePicker.kt b/app/src/main/java/com/sameerasw/essentials/ui/core/pickers/PrivilegedModePicker.kt
new file mode 100644
index 000000000..5e23627b0
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/ui/core/pickers/PrivilegedModePicker.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (c) 2026 sameerasw.com
+ * License: MIT License
+ *
+ * Feature Module: UI Core Components
+ * File: PrivilegedModePicker.kt
+ * Description: Dropdown picker for the privileged access backend.
+ */
+
+package com.sameerasw.essentials.ui.core.pickers
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.ListItemDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.sameerasw.essentials.R
+import com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenu
+import com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenuItem
+import com.sameerasw.essentials.utils.HapticUtil
+import com.sameerasw.essentials.utils.PrivilegedMode
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun PrivilegedModePicker(
+ selectedMode: PrivilegedMode,
+ onModeSelected: (PrivilegedMode) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ val view = LocalView.current
+ var isMenuExpanded by remember { mutableStateOf(false) }
+
+ ListItem(
+ onClick = {
+ HapticUtil.performVirtualKeyHaptic(view)
+ isMenuExpanded = true
+ },
+ modifier = modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ leadingContent = {
+ Icon(
+ painter = painterResource(id = R.drawable.rounded_numbers_24),
+ contentDescription = null,
+ modifier = Modifier.size(24.dp),
+ tint = MaterialTheme.colorScheme.primary,
+ )
+ },
+ trailingContent = {
+ Box {
+ Surface(
+ onClick = {
+ HapticUtil.performVirtualKeyHaptic(view)
+ isMenuExpanded = true
+ },
+ shape = RoundedCornerShape(12.dp),
+ color = MaterialTheme.colorScheme.primaryContainer,
+ contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
+ ) {
+ Text(
+ text = stringResource(selectedMode.labelRes),
+ style = MaterialTheme.typography.labelLarge,
+ modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp),
+ )
+ }
+
+ SegmentedDropdownMenu(
+ expanded = isMenuExpanded,
+ onDismissRequest = { isMenuExpanded = false },
+ ) {
+ PrivilegedMode.entries.forEach { mode ->
+ SegmentedDropdownMenuItem(
+ text = { Text(stringResource(mode.labelRes)) },
+ onClick = {
+ isMenuExpanded = false
+ onModeSelected(mode)
+ },
+ )
+ }
+ }
+ }
+ },
+ colors = ListItemDefaults.colors(containerColor = MaterialTheme.colorScheme.surfaceBright),
+ contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
+ content = {
+ Text(
+ text = stringResource(R.string.setting_privileged_mode_title),
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurface,
+ )
+ },
+ )
+}
diff --git a/app/src/main/java/com/sameerasw/essentials/utils/PrivilegedMode.kt b/app/src/main/java/com/sameerasw/essentials/utils/PrivilegedMode.kt
new file mode 100644
index 000000000..1134af9d9
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/utils/PrivilegedMode.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2026 sameerasw.com
+ * License: MIT License
+ *
+ * Feature Module: Core Utilities
+ * File: PrivilegedMode.kt
+ * Description: Privileged access backends selectable by the user.
+ */
+
+package com.sameerasw.essentials.utils
+
+import com.sameerasw.essentials.R
+
+enum class PrivilegedMode(
+ val key: String,
+ val labelRes: Int,
+) {
+ AUTO("auto", R.string.privileged_mode_auto),
+ SHIZUKU("shizuku", R.string.privileged_mode_shizuku),
+ DHIZUKU("dhizuku", R.string.privileged_mode_dhizuku),
+ PORTER("porter", R.string.privileged_mode_porter),
+ ROOT("root", R.string.privileged_mode_root),
+ ;
+
+ companion object {
+ fun fromKey(key: String?): PrivilegedMode = entries.firstOrNull { it.key == key } ?: AUTO
+ }
+}
diff --git a/app/src/main/java/com/sameerasw/essentials/utils/ShellUtils.kt b/app/src/main/java/com/sameerasw/essentials/utils/ShellUtils.kt
index d0a7c5658..6ed8e9512 100644
--- a/app/src/main/java/com/sameerasw/essentials/utils/ShellUtils.kt
+++ b/app/src/main/java/com/sameerasw/essentials/utils/ShellUtils.kt
@@ -23,12 +23,38 @@ object ShellUtils {
private var lastAlertTime = 0L
private const val ALERT_COOLDOWN = 180000L // 3 minutes
- fun isRootEnabled(context: Context): Boolean {
+ fun getSelectedMode(context: Context): PrivilegedMode {
val prefs =
context.getSharedPreferences(SettingsRepository.PREFS_NAME, Context.MODE_PRIVATE)
- return prefs.getBoolean(SettingsRepository.KEY_USE_ROOT, false)
+ val stored = prefs.getString(SettingsRepository.KEY_PRIVILEGED_MODE, null)
+ if (stored != null) return PrivilegedMode.fromKey(stored)
+ return if (prefs.getBoolean(SettingsRepository.KEY_USE_ROOT, false)) {
+ PrivilegedMode.ROOT
+ } else {
+ PrivilegedMode.AUTO
+ }
+ }
+
+ fun resolveMode(context: Context): PrivilegedMode {
+ val selected = getSelectedMode(context)
+ if (selected != PrivilegedMode.AUTO) return selected
+ return when {
+ ShizukuUtils.isShizukuInstalled(context) -> PrivilegedMode.SHIZUKU
+ ShizukuUtils.isPorterInstalled(context) -> PrivilegedMode.PORTER
+ ShizukuUtils.isDhizukuInstalled(context) -> PrivilegedMode.DHIZUKU
+ else -> PrivilegedMode.SHIZUKU
+ }
}
+ fun isRootEnabled(context: Context): Boolean = resolveMode(context) == PrivilegedMode.ROOT
+
+ fun usesAuthToken(context: Context): Boolean =
+ when (resolveMode(context)) {
+ PrivilegedMode.SHIZUKU -> !ShizukuUtils.isSheveryFork(context)
+ PrivilegedMode.PORTER -> true
+ else -> false
+ }
+
fun isAvailable(context: Context): Boolean =
if (isRootEnabled(context)) {
RootUtils.isRootAvailable()
diff --git a/app/src/main/java/com/sameerasw/essentials/utils/security/ShizukuUtils.kt b/app/src/main/java/com/sameerasw/essentials/utils/security/ShizukuUtils.kt
index b36591727..6deb13ff6 100644
--- a/app/src/main/java/com/sameerasw/essentials/utils/security/ShizukuUtils.kt
+++ b/app/src/main/java/com/sameerasw/essentials/utils/security/ShizukuUtils.kt
@@ -27,11 +27,15 @@ object ShizukuUtils {
fun isShizukuInstalled(context: Context): Boolean = shizukuPermissionInfo(context) != null
+ private const val DHIZUKU_PACKAGE = "com.rosan.dhizuku"
private const val PORTER_PACKAGE = "eu.darken.porter"
private const val PORTER_ACTION_START = "eu.darken.porter.START"
private const val PORTER_ACTION_STOP = "eu.darken.porter.STOP"
private const val PORTER_TOKEN_EXTRA = "auth"
+ fun isDhizukuInstalled(context: Context): Boolean =
+ runCatching { context.packageManager.getPackageInfo(DHIZUKU_PACKAGE, 0) }.isSuccess
+
fun isPorterInstalled(context: Context): Boolean =
runCatching { context.packageManager.getPackageInfo(PORTER_PACKAGE, 0) }.isSuccess
@@ -196,7 +200,7 @@ object ShizukuUtils {
context: android.content.Context,
start: Boolean,
) {
- if (!isShizukuInstalled(context) && isPorterInstalled(context)) {
+ if (ShellUtils.resolveMode(context) == PrivilegedMode.PORTER) {
togglePorter(context, start)
return
}
diff --git a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt
index b9bdda050..a05b7b0b6 100644
--- a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt
+++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt
@@ -46,6 +46,7 @@ import androidx.lifecycle.viewModelScope
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
+import com.sameerasw.essentials.EssentialsApp
import com.sameerasw.essentials.R
import com.sameerasw.essentials.data.repository.SettingsRepository
import com.sameerasw.essentials.data.repository.UpdateRepository
@@ -81,6 +82,7 @@ import com.sameerasw.essentials.utils.PermissionUtils
import com.sameerasw.essentials.weather.overcast.OvercastWeather
import com.sameerasw.essentials.utils.RefreshRateUtils
import com.sameerasw.essentials.utils.RootUtils
+import com.sameerasw.essentials.utils.PrivilegedMode
import com.sameerasw.essentials.utils.ShellUtils
import com.sameerasw.essentials.utils.ShizukuUtils
import com.sameerasw.essentials.utils.SurfaceFlingerControl
@@ -498,6 +500,8 @@ class MainViewModel : ViewModel() {
val isUpdateNotificationEnabled = mutableStateOf(true)
val isPreReleaseCheckEnabled = mutableStateOf(false)
val isRootEnabled = mutableStateOf(false)
+ val privilegedMode = mutableStateOf(PrivilegedMode.AUTO)
+ val usesAuthToken = mutableStateOf(true)
val isRootAvailable = mutableStateOf(false)
val isRootPermissionGranted = mutableStateOf(false)
val hasPendingUpdates = mutableStateOf(false)
@@ -1058,9 +1062,7 @@ class MainViewModel : ViewModel() {
}
}
- SettingsRepository.KEY_USE_ROOT ->
- isRootEnabled.value =
- settingsRepository.getBoolean(key)
+ SettingsRepository.KEY_USE_ROOT, SettingsRepository.KEY_PRIVILEGED_MODE -> refreshPrivilegedMode()
SettingsRepository.KEY_CHECK_PRE_RELEASES_ENABLED ->
isPreReleaseCheckEnabled.value =
@@ -2148,7 +2150,7 @@ class MainViewModel : ViewModel() {
settingsRepository.getFloat(SettingsRepository.KEY_EDGE_LIGHTING_INDICATOR_Y, 2f)
isAodEnabled.value = settingsRepository.isAodEnabled()
- isRootEnabled.value = settingsRepository.getBoolean(SettingsRepository.KEY_USE_ROOT)
+ refreshPrivilegedMode()
if (isRootEnabled.value) {
isRootAvailable.value = RootUtils.isRootAvailable()
@@ -3127,15 +3129,23 @@ class MainViewModel : ViewModel() {
* @param enabled [Boolean] Target enabled.
* @param context [Context] Target context.
*/
- fun setRootEnabled(
- enabled: Boolean,
+ fun setPrivilegedMode(
+ mode: PrivilegedMode,
context: Context,
) {
- settingsRepository.putBoolean(SettingsRepository.KEY_USE_ROOT, enabled)
- isRootEnabled.value = enabled
+ settingsRepository.putString(SettingsRepository.KEY_PRIVILEGED_MODE, mode.key)
+ settingsRepository.putBoolean(SettingsRepository.KEY_USE_ROOT, mode == PrivilegedMode.ROOT)
+ refreshPrivilegedMode()
check(context)
}
+ private fun refreshPrivilegedMode() {
+ val ctx = EssentialsApp.context
+ privilegedMode.value = ShellUtils.getSelectedMode(ctx)
+ isRootEnabled.value = ShellUtils.isRootEnabled(ctx)
+ usesAuthToken.value = ShellUtils.usesAuthToken(ctx)
+ }
+
/**
* Executes the set user dictionary enabled operation.
*
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 8e58fed45..bf0161284 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1783,6 +1783,12 @@
Play alarm ringtone and show full-screen alert upon arrival
Use Root
Instead of Shizuku
+ Privileged access
+ Auto
+ Shizuku
+ Dhizuku
+ Porter
+ Root
Root access not available. Please check your root manager.
Keyboard
From 651c9d7d33ee7cb24312ff3521ca8663b23db1c1 Mon Sep 17 00:00:00 2001
From: SpySethyDev <87845069+SpySethyDev@users.noreply.github.com>
Date: Wed, 7 Oct 2026 17:32:29 +1100
Subject: [PATCH 7/9] Added proper closing for Weather in IslandSettingsUI.kt,
Added Battery Alerts to reorderable items
---
.../ui/features/display/IslandSettingsUI.kt | 24 +++++++++++++++++++
1 file changed, 24 insertions(+)
diff --git a/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSettingsUI.kt
index b0355dcd7..1a15ae6cc 100644
--- a/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSettingsUI.kt
+++ b/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSettingsUI.kt
@@ -308,6 +308,18 @@ fun IslandSettingsUI(
stringResource(R.string.weather_error_overcast_permission)
} else {
null
+ },
+ isChecked = viewModel.isIslandShowWeather.value,
+ onCheckedChange = { checked ->
+ HapticUtil.performVirtualKeyHaptic(view)
+ viewModel.setIslandShowWeather(checked)
+ if (checked && !OvercastWeather.isAvailable(context)) {
+ requestingPermissionsFor = Pair(R.string.lock_screen_clock_weather, listOf("OVERCAST_WEATHER"))
+ }
+ },
+ onSettingsClick = { showWeatherOptionsSheet = true },
+ modifier = Modifier.highlight(highlightSetting == "island_show_weather"),
+ )
Column(
modifier = Modifier
.fillMaxWidth()
@@ -381,6 +393,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,
From e31373c3c5838687e6f88f1072c1250388031c72 Mon Sep 17 00:00:00 2001
From: sameerasw
Date: Wed, 7 Oct 2026 12:25:18 +0530
Subject: [PATCH 8/9] feat: add Porter installation options and links to setup
and help guides
---
.../essentials/ui/components/HelpAndGuides.kt | 1 +
.../ui/composables/SetupFeatures.kt | 20 +++++++++++++++++++
app/src/main/res/values/strings.xml | 2 ++
3 files changed, 23 insertions(+)
diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/HelpAndGuides.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/HelpAndGuides.kt
index 0db474969..5e929ea4c 100644
--- a/app/src/main/java/com/sameerasw/essentials/ui/components/HelpAndGuides.kt
+++ b/app/src/main/java/com/sameerasw/essentials/ui/components/HelpAndGuides.kt
@@ -95,6 +95,7 @@ fun HelpAndGuidesContent() {
links =
listOf(
stringResource(R.string.perm_shizuku_title) to "https://github.com/rushiranpise/Shizuku-Next",
+ stringResource(R.string.perm_porter_title) to "https://github.com/d4rken-org/porter",
),
),
InstructionSection(
diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/SetupFeatures.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/SetupFeatures.kt
index 1c129d43f..afddc3254 100644
--- a/app/src/main/java/com/sameerasw/essentials/ui/composables/SetupFeatures.kt
+++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/SetupFeatures.kt
@@ -421,6 +421,16 @@ fun SetupFeatures(
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context.startActivity(intent)
},
+ secondaryActionLabel = R.string.perm_porter_install_action,
+ secondaryAction = {
+ val intent =
+ Intent(
+ Intent.ACTION_VIEW,
+ "https://github.com/d4rken-org/porter".toUri(),
+ )
+ intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ context.startActivity(intent)
+ },
isGranted = isShizukuAvailable,
),
)
@@ -733,6 +743,16 @@ fun SetupFeatures(
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context.startActivity(intent)
},
+ secondaryActionLabel = R.string.perm_porter_install_action,
+ secondaryAction = {
+ val intent =
+ Intent(
+ Intent.ACTION_VIEW,
+ "https://github.com/d4rken-org/porter".toUri(),
+ )
+ intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ context.startActivity(intent)
+ },
isGranted = isShizukuAvailable,
),
)
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index bf0161284..cb31b3f5e 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -935,6 +935,8 @@
Shizuku
Required for advanced commands. Install Shizuku to proceed.
Install Shizuku
+ Porter
+ Install Porter
Grant Permission
Required to run power-saving commands while maps is navigating.
Requires Shizuku or Root
From 65aae390485740393b3970df2c7b88de37444942 Mon Sep 17 00:00:00 2001
From: SpySethyDev <87845069+SpySethyDev@users.noreply.github.com>
Date: Thu, 8 Oct 2026 19:30:38 +1100
Subject: [PATCH 9/9] Fixed issues with 'Max items to show' being misplaced
between plugins toggles
---
.../ui/features/display/IslandSettingsUI.kt | 95 -------------------
1 file changed, 95 deletions(-)
diff --git a/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSettingsUI.kt
index 1a15ae6cc..097a964b1 100644
--- a/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSettingsUI.kt
+++ b/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSettingsUI.kt
@@ -225,101 +225,6 @@ fun IslandSettingsUI(
spacing = 2.dp,
cornerRadius = 24.dp,
) {
- IconToggleItem(
- iconRes = R.drawable.rounded_call_24,
- title = stringResource(R.string.island_show_calls_title),
- isChecked = viewModel.isIslandShowCalls.value && PermissionUtils.hasCallPermissions(context),
- onCheckedChange = { checked ->
- HapticUtil.performVirtualKeyHaptic(view)
- if (checked && !PermissionUtils.hasCallPermissions(context)) {
- requestingPermissionsFor = Pair(R.string.island_title, listOf("READ_PHONE_STATE", "ANSWER_PHONE_CALLS", "READ_CONTACTS"))
- } else {
- viewModel.setIslandShowCalls(checked)
- }
- },
- onSettingsClick = { showCallOptionsSheet = true },
- modifier = Modifier.highlight(highlightSetting == "island_show_calls"),
- )
-
- IconToggleItem(
- iconRes = R.drawable.rounded_notifications_unread_24,
- title = stringResource(R.string.island_section_notifications),
- isChecked = viewModel.isIslandShowNotifications.value,
- onCheckedChange = { checked ->
- HapticUtil.performVirtualKeyHaptic(view)
- viewModel.setIslandShowNotifications(checked)
- },
- onSettingsClick = { showNotificationOptionsSheet = true },
- modifier = Modifier.highlight(highlightSetting == "island_show_notifications"),
- )
-
- IconToggleItem(
- iconRes = R.drawable.rounded_schedule_24,
- title = stringResource(R.string.island_show_time_battery_title),
- isChecked = viewModel.isIslandShowTimeBattery.value,
- onCheckedChange = { checked ->
- HapticUtil.performVirtualKeyHaptic(view)
- viewModel.setIslandShowTimeBattery(checked)
- },
- onSettingsClick = { showTimeBatteryOptionsSheet = true },
- modifier = Modifier.highlight(highlightSetting == "island_show_time_battery"),
- )
-
- IconToggleItem(
- 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"),
- )
-
- IconToggleItem(
- iconRes = R.drawable.rounded_flashlight_on_24,
- title = stringResource(R.string.feat_flashlight_title),
- isChecked = viewModel.isIslandShowFlashlight.value,
- onCheckedChange = { checked ->
- HapticUtil.performVirtualKeyHaptic(view)
- viewModel.setIslandShowFlashlight(checked)
- },
- modifier = Modifier.highlight(highlightSetting == "island_show_flashlight"),
- )
-
- IconToggleItem(
- iconRes = R.drawable.rounded_timer_24,
- title = stringResource(R.string.island_show_timers_title),
- isChecked = viewModel.isIslandShowTimers.value,
- onCheckedChange = { checked ->
- HapticUtil.performVirtualKeyHaptic(view)
- viewModel.setIslandShowTimers(checked)
- },
- onSettingsClick = { showTimerOptionsSheet = true },
- modifier = Modifier.highlight(highlightSetting == "island_show_timers"),
- )
-
- IconToggleItem(
- iconRes = R.drawable.rounded_partly_cloudy_day_24,
- title = stringResource(R.string.lock_screen_clock_weather),
- description =
- if (viewModel.isIslandShowWeather.value && !viewModel.isOvercastWeatherPermissionGranted.value) {
- stringResource(R.string.weather_error_overcast_permission)
- } else {
- null
- },
- isChecked = viewModel.isIslandShowWeather.value,
- onCheckedChange = { checked ->
- HapticUtil.performVirtualKeyHaptic(view)
- viewModel.setIslandShowWeather(checked)
- if (checked && !OvercastWeather.isAvailable(context)) {
- requestingPermissionsFor = Pair(R.string.lock_screen_clock_weather, listOf("OVERCAST_WEATHER"))
- }
- },
- onSettingsClick = { showWeatherOptionsSheet = true },
- modifier = Modifier.highlight(highlightSetting == "island_show_weather"),
- )
Column(
modifier = Modifier
.fillMaxWidth()