From d215d56e9a1279eaa0b0c11e30b780990dc6c543 Mon Sep 17 00:00:00 2001 From: Mudit200408 Date: Sat, 30 May 2026 19:26:42 +0530 Subject: [PATCH 1/2] feat: Add WiFi, Cellular Data, and Auto Brightness automation actions --- .../sameerasw/essentials/domain/diy/Action.kt | 48 ++++++- .../executors/CombinedActionExecutor.kt | 33 +++++ .../ui/activities/AutomationEditorActivity.kt | 123 ++++++++++++++---- app/src/main/res/values/strings.xml | 6 + 4 files changed, 185 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/com/sameerasw/essentials/domain/diy/Action.kt b/app/src/main/java/com/sameerasw/essentials/domain/diy/Action.kt index 19e5bd8a3..dbc0ff745 100644 --- a/app/src/main/java/com/sameerasw/essentials/domain/diy/Action.kt +++ b/app/src/main/java/com/sameerasw/essentials/domain/diy/Action.kt @@ -62,7 +62,7 @@ sealed interface Action { ) : Action { override val title: Int get() = R.string.diy_action_dim_wallpaper override val icon: Int get() = R.drawable.rounded_mobile_screensaver_24 - override val permissions: List = listOf("shizuku", "root") + override val permissions: List = listOf("SHIZUKU", "ROOT") override val isConfigurable: Boolean = true } @@ -76,7 +76,7 @@ sealed interface Action { ) : Action { override val title: Int get() = R.string.diy_action_device_effects override val icon: Int get() = R.drawable.rounded_bed_24 - override val permissions: List = listOf("notification_policy") + override val permissions: List = listOf("NOTIFICATION_POLICY") override val isConfigurable: Boolean = true } @@ -103,7 +103,7 @@ sealed interface Action { SoundModeType.VIBRATE -> R.drawable.rounded_mobile_vibrate_24 SoundModeType.SILENT -> R.drawable.rounded_volume_off_24 } - override val permissions: List = listOf("notification_policy") + override val permissions: List = listOf("NOTIFICATION_POLICY") override val isConfigurable: Boolean = true } @@ -187,4 +187,46 @@ sealed interface Action { override val icon: Int = R.drawable.rounded_shield_lock_24 override val permissions: List = listOf("shizuku", "root") } + + @Keep + data object TurnOnWifi : Action { + override val title: Int = R.string.diy_action_wifi_on + override val icon: Int = R.drawable.rounded_android_wifi_4_bar_plus_24 + override val permissions: List = listOf("SHIZUKU", "ROOT") + } + + @Keep + data object TurnOffWifi : Action { + override val title: Int = R.string.diy_action_wifi_off + override val icon: Int = R.drawable.rounded_android_wifi_4_bar_plus_24 + override val permissions: List = listOf("SHIZUKU", "ROOT") + } + + @Keep + data object TurnOnCellularData : Action { + override val title: Int = R.string.diy_action_cellular_on + override val icon: Int = R.drawable.rounded_signal_cellular_alt_24 + override val permissions: List = listOf("SHIZUKU", "ROOT") + } + + @Keep + data object TurnOffCellularData : Action { + override val title: Int = R.string.diy_action_cellular_off + override val icon: Int = R.drawable.rounded_signal_cellular_alt_24 + override val permissions: List = listOf("SHIZUKU", "ROOT") + } + + @Keep + data object TurnOnAutoBrightness : Action { + override val title: Int = R.string.diy_action_auto_brightness_on + override val icon: Int = R.drawable.rounded_brightness_auto_24 + override val permissions: List = listOf("WRITE_SETTINGS") + } + + @Keep + data object TurnOffAutoBrightness : Action { + override val title: Int = R.string.diy_action_auto_brightness_off + override val icon: Int = R.drawable.rounded_brightness_auto_24 + override val permissions: List = listOf("WRITE_SETTINGS") + } } diff --git a/app/src/main/java/com/sameerasw/essentials/services/automation/executors/CombinedActionExecutor.kt b/app/src/main/java/com/sameerasw/essentials/services/automation/executors/CombinedActionExecutor.kt index 22a51fe8a..1c2e13f0d 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/automation/executors/CombinedActionExecutor.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/automation/executors/CombinedActionExecutor.kt @@ -475,6 +475,13 @@ object CombinedActionExecutor { ).show() } } + + is Action.TurnOnWifi -> setWifiEnabled(context, true) + is Action.TurnOffWifi -> setWifiEnabled(context, false) + is Action.TurnOnCellularData -> setCellularDataEnabled(context, true) + is Action.TurnOffCellularData -> setCellularDataEnabled(context, false) + is Action.TurnOnAutoBrightness -> setAutoBrightnessEnabled(context, true) + is Action.TurnOffAutoBrightness -> setAutoBrightnessEnabled(context, false) } } } @@ -497,4 +504,30 @@ object CombinedActionExecutor { e.printStackTrace() } } + + private fun setWifiEnabled(context: Context, enabled: Boolean) { + val state = if (enabled) "enable" else "disable" + com.sameerasw.essentials.utils.ShellUtils.runCommand(context, "svc wifi $state") + } + + private fun setCellularDataEnabled(context: Context, enabled: Boolean) { + val state = if (enabled) "enable" else "disable" + com.sameerasw.essentials.utils.ShellUtils.runCommand(context, "svc data $state") + } + + private fun setAutoBrightnessEnabled(context: Context, enabled: Boolean) { + val value = if (enabled) 1 else 0 + try { + android.provider.Settings.System.putInt( + context.contentResolver, + android.provider.Settings.System.SCREEN_BRIGHTNESS_MODE, + value + ) + } catch (e: Exception) { + com.sameerasw.essentials.utils.ShellUtils.runCommand( + context, + "settings put system screen_brightness_mode $value" + ) + } + } } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/activities/AutomationEditorActivity.kt b/app/src/main/java/com/sameerasw/essentials/ui/activities/AutomationEditorActivity.kt index 264b570de..037eceb9e 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/activities/AutomationEditorActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/activities/AutomationEditorActivity.kt @@ -37,7 +37,10 @@ import androidx.compose.material3.Text import androidx.compose.material3.carousel.HorizontalMultiBrowseCarousel import androidx.compose.material3.carousel.rememberCarouselState import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.Lifecycle import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -230,6 +233,25 @@ class AutomationEditorActivity : ComponentActivity() { var showTimeSettings by remember { mutableStateOf(false) } var configAction by remember { mutableStateOf(null) } // Generic config action + var showPermissionSheet by remember { mutableStateOf(false) } + var permissionKeysToShow by remember { mutableStateOf>(emptyList()) } + var permissionFeatureTitle by remember { mutableStateOf("") } + + // Automatic refresh on resume + val lifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current + DisposableEffect(lifecycleOwner) { + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_RESUME) { + viewModel.check(context) + } + } + lifecycleOwner.lifecycle.addObserver(observer) + onDispose { + lifecycleOwner.lifecycle.removeObserver(observer) + } + } + + // Validation val isValid = when (automationType) { Automation.Type.TRIGGER -> selectedTrigger != null && selectedAction != null Automation.Type.ACTION_SHORTCUT, Automation.Type.PIXEL_SEARCHBAR -> selectedAction != null @@ -610,7 +632,13 @@ class AutomationEditorActivity : ComponentActivity() { Action.ToggleMediaVolume, Action.LikeCurrentSong, Action.CircleToSearch, - Action.PinApp + Action.PinApp, + Action.TurnOnWifi, + Action.TurnOffWifi, + Action.TurnOnCellularData, + Action.TurnOffCellularData, + Action.TurnOnAutoBrightness, + Action.TurnOffAutoBrightness ) // Only show Device Effects on Android 15+ actions.add(Action.DeviceEffects()) @@ -671,17 +699,11 @@ class AutomationEditorActivity : ComponentActivity() { } } // Check permissions immediately on selection - // For Device Effects, we need Notification Policy Access - if (resolvedAction is Action.DeviceEffects) { - val nm = - context.getSystemService( - NOTIFICATION_SERVICE - ) as android.app.NotificationManager - if (!nm.isNotificationPolicyAccessGranted) { - val intent = - Intent(android.provider.Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS) - context.startActivity(intent) - } + val missing = getMissingPermissions(context, resolvedAction, viewModel) + if (missing.isNotEmpty()) { + permissionKeysToShow = missing + permissionFeatureTitle = resolvedAction.title + showPermissionSheet = true } }, onSettingsClick = { @@ -804,6 +826,24 @@ class AutomationEditorActivity : ComponentActivity() { } ) } + if (showPermissionSheet) { + val permissionItems = com.sameerasw.essentials.utils.PermissionUIHelper.getPermissionItems( + permissionKeysToShow, + context, + viewModel, + this@AutomationEditorActivity + ) + if (permissionItems.isNotEmpty()) { + com.sameerasw.essentials.ui.components.sheets.PermissionsBottomSheet( + onDismissRequest = { + showPermissionSheet = false + permissionKeysToShow = emptyList() + }, + featureTitle = permissionFeatureTitle, + permissions = permissionItems + ) + } + } // Bottom Actions Row( modifier = Modifier @@ -834,6 +874,19 @@ class AutomationEditorActivity : ComponentActivity() { Button( onClick = { HapticUtil.performVirtualKeyHaptic(view) + // Check for missing permissions before saving + val actionsToCheck = when (automationType) { + Automation.Type.TRIGGER -> listOfNotNull(selectedAction) + Automation.Type.ACTION_SHORTCUT, Automation.Type.PIXEL_SEARCHBAR -> listOfNotNull(selectedAction) + else -> listOfNotNull(selectedInAction, selectedOutAction) + } + val allMissingPermissions = actionsToCheck.flatMap { getMissingPermissions(context, it, viewModel) }.distinct() + if (allMissingPermissions.isNotEmpty()) { + permissionKeysToShow = allMissingPermissions + permissionFeatureTitle = R.string.tab_diy + showPermissionSheet = true + return@Button + } // Save logic if (automationType == Automation.Type.TRIGGER) { val newAutomation = Automation( @@ -884,22 +937,48 @@ class AutomationEditorActivity : ComponentActivity() { finish() }, modifier = Modifier.weight(1f), - enabled = isValid - ) { - Icon( - painter = painterResource(id = R.drawable.rounded_check_24), - contentDescription = null, - modifier = Modifier.size(20.dp) - ) - Spacer(modifier = Modifier.size(8.dp)) - Text(stringResource(R.string.action_save)) - } + enabled = isValid + ) { + Icon( + painter = painterResource(id = R.drawable.rounded_check_24), + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.size(8.dp)) + Text(stringResource(R.string.action_save)) + } } } } } } } + + private fun getMissingPermissions( + context: Context, + action: Action?, + viewModel: com.sameerasw.essentials.viewmodels.MainViewModel + ): List { + if (action == null) return emptyList() + val resolvedPermissions = action.permissions.map { permKey -> + if (permKey == "SHIZUKU" || permKey == "ROOT") { + if (com.sameerasw.essentials.utils.ShellUtils.isRootEnabled(context)) "ROOT" else "SHIZUKU" + } else { + permKey + } + }.distinct() + + return resolvedPermissions.filter { permKey -> + when (permKey) { + "SHIZUKU" -> !viewModel.isShizukuPermissionGranted.value + "ROOT" -> !viewModel.isRootPermissionGranted.value + "WRITE_SETTINGS" -> !viewModel.isWriteSettingsEnabled.value + "NOTIFICATION_POLICY" -> !viewModel.isNotificationPolicyAccessGranted.value + "WRITE_SECURE_SETTINGS" -> !viewModel.isWriteSecureSettingsEnabled.value + else -> false + } + } + } } @Composable diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 67f6aa73a..93faa2a09 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -880,6 +880,12 @@ Toggle Flashlight Turn On Low Power Mode Turn Off Low Power Mode + Turn On WiFi + Turn Off WiFi + Turn On Cellular Data + Turn Off Cellular Data + Turn On Auto Brightness + Turn Off Auto Brightness Dim Wallpaper Screen Off Media Play/Pause From 5919f1a04a9e445bca63087040c0dd9376d5d920 Mon Sep 17 00:00:00 2001 From: Mudit200408 Date: Mon, 1 Jun 2026 00:20:28 +0530 Subject: [PATCH 2/2] feat: Add Freeze/Unfreeze apps in Automations --- .../sameerasw/essentials/domain/diy/Action.kt | 21 +++++++++ .../executors/CombinedActionExecutor.kt | 11 +++++ .../ui/activities/AutomationEditorActivity.kt | 44 ++++++++++++++++++- .../components/sheets/AppSelectionSheets.kt | 4 +- app/src/main/res/values/strings.xml | 5 +++ 5 files changed, 83 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/sameerasw/essentials/domain/diy/Action.kt b/app/src/main/java/com/sameerasw/essentials/domain/diy/Action.kt index dbc0ff745..4f78b011a 100644 --- a/app/src/main/java/com/sameerasw/essentials/domain/diy/Action.kt +++ b/app/src/main/java/com/sameerasw/essentials/domain/diy/Action.kt @@ -229,4 +229,25 @@ sealed interface Action { override val icon: Int = R.drawable.rounded_brightness_auto_24 override val permissions: List = listOf("WRITE_SETTINGS") } + + @Keep + data class FreezeApps( + @SerializedName("packageNames") val packageNames: List = emptyList() + ) : Action { + override val title: Int get() = R.string.diy_action_freeze_apps + override val icon: Int get() = R.drawable.rounded_mode_cool_24 + override val permissions: List = listOf("SHIZUKU", "ROOT") + override val isConfigurable: Boolean = true + } + + @Keep + data class UnfreezeApps( + @SerializedName("packageNames") val packageNames: List = emptyList() + ) : Action { + override val title: Int get() = R.string.diy_action_unfreeze_apps + override val icon: Int get() = R.drawable.rounded_mode_cool_off_24 + override val permissions: List = listOf("SHIZUKU", "ROOT") + override val isConfigurable: Boolean = true + } } + diff --git a/app/src/main/java/com/sameerasw/essentials/services/automation/executors/CombinedActionExecutor.kt b/app/src/main/java/com/sameerasw/essentials/services/automation/executors/CombinedActionExecutor.kt index 1c2e13f0d..7a742938c 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/automation/executors/CombinedActionExecutor.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/automation/executors/CombinedActionExecutor.kt @@ -482,7 +482,18 @@ object CombinedActionExecutor { is Action.TurnOffCellularData -> setCellularDataEnabled(context, false) is Action.TurnOnAutoBrightness -> setAutoBrightnessEnabled(context, true) is Action.TurnOffAutoBrightness -> setAutoBrightnessEnabled(context, false) + is Action.FreezeApps -> { + action.packageNames.forEach { pkg -> + com.sameerasw.essentials.utils.FreezeManager.freezeApp(context, pkg) + } + } + is Action.UnfreezeApps -> { + action.packageNames.forEach { pkg -> + com.sameerasw.essentials.utils.FreezeManager.unfreezeApp(context, pkg) + } + } } + } } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/activities/AutomationEditorActivity.kt b/app/src/main/java/com/sameerasw/essentials/ui/activities/AutomationEditorActivity.kt index 037eceb9e..673d45935 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/activities/AutomationEditorActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/activities/AutomationEditorActivity.kt @@ -73,9 +73,11 @@ import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer import com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenu import com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenuItem import com.sameerasw.essentials.ui.components.pickers.SegmentedPicker +import com.sameerasw.essentials.ui.components.sheets.AppSelectionSheet import com.sameerasw.essentials.ui.components.sheets.DimWallpaperSettingsSheet import com.sameerasw.essentials.ui.components.sheets.ScreenOffSettingsSheet import com.sameerasw.essentials.ui.components.sheets.SoundModeSettingsSheet + import com.sameerasw.essentials.ui.theme.EssentialsTheme import com.sameerasw.essentials.utils.AppUtil import com.sameerasw.essentials.utils.HapticUtil @@ -230,9 +232,12 @@ class AutomationEditorActivity : ComponentActivity() { var showScreenOffSettings by remember { mutableStateOf(false) } var showDeviceEffectsSettings by remember { mutableStateOf(false) } var showSoundModeSettings by remember { mutableStateOf(false) } + var showFreezeAppsSettings by remember { mutableStateOf(false) } + var temporarySelectedAppsForAction by remember { mutableStateOf>(emptyList()) } var showTimeSettings by remember { mutableStateOf(false) } var configAction by remember { mutableStateOf(null) } // Generic config action + var showPermissionSheet by remember { mutableStateOf(false) } var permissionKeysToShow by remember { mutableStateOf>(emptyList()) } var permissionFeatureTitle by remember { mutableStateOf("") } @@ -638,7 +643,9 @@ class AutomationEditorActivity : ComponentActivity() { Action.TurnOnCellularData, Action.TurnOffCellularData, Action.TurnOnAutoBrightness, - Action.TurnOffAutoBrightness + Action.TurnOffAutoBrightness, + Action.FreezeApps(), + Action.UnfreezeApps() ) // Only show Device Effects on Android 15+ actions.add(Action.DeviceEffects()) @@ -716,6 +723,12 @@ class AutomationEditorActivity : ComponentActivity() { showDeviceEffectsSettings = true } else if (resolvedAction is Action.SoundMode) { showSoundModeSettings = true + } else if (resolvedAction is Action.FreezeApps) { + temporarySelectedAppsForAction = resolvedAction.packageNames + showFreezeAppsSettings = true + } else if (resolvedAction is Action.UnfreezeApps) { + temporarySelectedAppsForAction = resolvedAction.packageNames + showFreezeAppsSettings = true } } ) @@ -825,6 +838,35 @@ class AutomationEditorActivity : ComponentActivity() { configAction = null } ) + } + if (showFreezeAppsSettings && (configAction is Action.FreezeApps || configAction is Action.UnfreezeApps)) { + AppSelectionSheet( + onDismissRequest = { + val finalAction = when (val action = configAction) { + is Action.FreezeApps -> action.copy(packageNames = temporarySelectedAppsForAction) + is Action.UnfreezeApps -> action.copy(packageNames = temporarySelectedAppsForAction) + else -> configAction + } + if (finalAction != null) { + when (automationType) { + Automation.Type.TRIGGER, Automation.Type.ACTION_SHORTCUT, Automation.Type.PIXEL_SEARCHBAR -> selectedAction = finalAction + Automation.Type.STATE, Automation.Type.APP -> { + if (selectedActionTab == 0) selectedInAction = finalAction + else selectedOutAction = finalAction + } + } + } + showFreezeAppsSettings = false + configAction = null + }, + onLoadApps = { + temporarySelectedAppsForAction.map { AppSelection(it, true) } + }, + onSaveApps = { _, selections -> + temporarySelectedAppsForAction = selections.filter { it.isEnabled }.map { it.packageName } + }, + excludePackages = if (automationType == Automation.Type.APP) selectedApps else emptyList() + ) } if (showPermissionSheet) { val permissionItems = com.sameerasw.essentials.utils.PermissionUIHelper.getPermissionItems( diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/AppSelectionSheets.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/AppSelectionSheets.kt index a1e14b2c0..8ee95ce59 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/AppSelectionSheets.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/AppSelectionSheets.kt @@ -60,6 +60,7 @@ fun AppSelectionSheet( onLoadApps: suspend (Context) -> List, onSaveApps: suspend (Context, List) -> Unit, onAppToggle: ((Context, String, Boolean) -> Unit)? = null, + excludePackages: List = emptyList(), context: Context = LocalContext.current ) { val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) @@ -112,7 +113,8 @@ fun AppSelectionSheet( searchQuery.isEmpty() || it.appName.contains(searchQuery, ignoreCase = true) val isVisible = !it.isSystemApp || showSystemApps || it.isEnabled // Always show if enabled, or if system toggle checks out - matchesSearch && isVisible + val isExcluded = excludePackages.contains(it.packageName) + matchesSearch && isVisible && !isExcluded } .sortedWith(compareByDescending { initialEnabledPackageNames.contains(it.packageName) }.thenBy { it.appName.lowercase() }) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 93faa2a09..4d2c83380 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -898,6 +898,11 @@ Circle to Search Pin Foreground App This action requires Shizuku or Root to adjust system wallpaper dimming. + Freeze Apps + Unfreeze Apps + This action requires Shizuku or Root to freeze specific applications. + This action requires Shizuku or Root to unfreeze specific applications. + Select Trigger App Automate based on open app