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()