diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f243d312d..f63dc0bb7 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -350,14 +350,6 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/com/sameerasw/essentials/FeatureSettingsActivity.kt b/app/src/main/java/com/sameerasw/essentials/FeatureSettingsActivity.kt
index 5df6ff98f..665459320 100644
--- a/app/src/main/java/com/sameerasw/essentials/FeatureSettingsActivity.kt
+++ b/app/src/main/java/com/sameerasw/essentials/FeatureSettingsActivity.kt
@@ -75,10 +75,10 @@ import com.sameerasw.essentials.ui.composables.configs.OtherCustomizationsSettin
import com.sameerasw.essentials.ui.composables.configs.PocketModeSettingsUI
import com.sameerasw.essentials.ui.composables.configs.QuickSettingsTilesSettingsUI
import com.sameerasw.essentials.ui.composables.configs.RefreshRateSettingsUI
+import com.sameerasw.essentials.ui.composables.configs.ShutUpSettingsUI
import com.sameerasw.essentials.ui.composables.configs.RemoteLockSettingsUI
import com.sameerasw.essentials.ui.composables.configs.ScreenLockedSecuritySettingsUI
import com.sameerasw.essentials.ui.composables.configs.ScreenOffWidgetSettingsUI
-import com.sameerasw.essentials.ui.composables.configs.ShutUpSettingsUI
import com.sameerasw.essentials.ui.composables.configs.SnoozeNotificationsSettingsUI
import com.sameerasw.essentials.ui.composables.configs.SoundModeTileSettingsUI
import com.sameerasw.essentials.ui.composables.configs.StatusBarIconSettingsUI
@@ -219,6 +219,9 @@ class FeatureSettingsActivity : AppCompatActivity() {
val isNotificationListenerEnabled by viewModel.isNotificationListenerEnabled
val isReadPhoneStateEnabled by viewModel.isReadPhoneStateEnabled
val isShizukuPermissionGranted by viewModel.isShizukuPermissionGranted
+ val isWriteSettingsEnabled by viewModel.isWriteSettingsEnabled
+ val isUsageStatsPermissionGranted by viewModel.isUsageStatsPermissionGranted
+ val isPostNotificationsEnabled by viewModel.isPostNotificationsEnabled
var watchAdbWifiEnabled by remember {
mutableStateOf(prefs.getBoolean("watch_adb_wifi_enabled", false))
@@ -286,7 +289,10 @@ class FeatureSettingsActivity : AppCompatActivity() {
isNotificationLightingAccessibilityEnabled,
isNotificationListenerEnabled,
isReadPhoneStateEnabled,
- isShizukuPermissionGranted
+ isShizukuPermissionGranted,
+ isWriteSettingsEnabled,
+ isUsageStatsPermissionGranted,
+ isPostNotificationsEnabled
) {
val hasMissingPermissions = when (featureId) {
"Screen off widget" -> !isAccessibilityEnabled
@@ -307,6 +313,7 @@ class FeatureSettingsActivity : AppCompatActivity() {
"Screen refresh rate" -> !com.sameerasw.essentials.utils.ShellUtils.hasPermission(
context
)
+ "Shut-Up!" -> !isWriteSecureSettingsEnabled || !isWriteSettingsEnabled || !isUsageStatsPermissionGranted || !isPostNotificationsEnabled
// Top level checks for other features (rarely hit if they are children, but safe to add)
"Essentials On Display" -> !isAccessibilityEnabled || !isNotificationListenerEnabled
"Call vibrations" -> !isReadPhoneStateEnabled || !isNotificationListenerEnabled
@@ -323,7 +330,6 @@ class FeatureSettingsActivity : AppCompatActivity() {
context
)
- "Shut-Up!" -> !isWriteSecureSettingsEnabled || !viewModel.isUsageStatsPermissionGranted.value
else -> false
}
if (hasMissingPermissions) {
@@ -544,7 +550,7 @@ class FeatureSettingsActivity : AppCompatActivity() {
"Screen refresh rate" -> !com.sameerasw.essentials.utils.ShellUtils.hasPermission(
context
)
- "Shut-Up!" -> !isWriteSecureSettingsEnabled || !viewModel.isUsageStatsPermissionGranted.value
+ "Shut-Up!" -> !isWriteSecureSettingsEnabled || !isWriteSettingsEnabled || !isUsageStatsPermissionGranted || !isPostNotificationsEnabled
else -> false
}
@@ -819,6 +825,13 @@ class FeatureSettingsActivity : AppCompatActivity() {
highlightSetting = highlightSetting
)
}
+ "Shut-Up!" -> {
+ ShutUpSettingsUI(
+ viewModel = viewModel,
+ modifier = Modifier.padding(top = 16.dp),
+ highlightSetting = highlightSetting
+ )
+ }
"Always on Display" -> {
AlwaysOnDisplaySettingsUI(
@@ -851,14 +864,6 @@ class FeatureSettingsActivity : AppCompatActivity() {
highlightSetting = highlightSetting
)
}
-
- "Shut-Up!" -> {
- ShutUpSettingsUI(
- viewModel = viewModel,
- modifier = Modifier.padding(top = 16.dp),
- highlightKey = highlightSetting
- )
- }
}
}
diff --git a/app/src/main/java/com/sameerasw/essentials/ShutUpShortcutActivity.kt b/app/src/main/java/com/sameerasw/essentials/ShutUpShortcutActivity.kt
index 1c0cbb82e..31c2b0cee 100644
--- a/app/src/main/java/com/sameerasw/essentials/ShutUpShortcutActivity.kt
+++ b/app/src/main/java/com/sameerasw/essentials/ShutUpShortcutActivity.kt
@@ -1,10 +1,9 @@
package com.sameerasw.essentials
-import android.content.ContentResolver
+
import android.content.Intent
import android.os.Bundle
-import android.provider.Settings
-import android.util.Log
+
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
@@ -19,8 +18,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.lifecycle.lifecycleScope
import com.sameerasw.essentials.data.repository.SettingsRepository
-import com.sameerasw.essentials.domain.model.ShutUpAppConfig
+
import com.sameerasw.essentials.ui.theme.EssentialsTheme
+
+import com.sameerasw.essentials.utils.ShutUpManager
import com.sameerasw.essentials.utils.PermissionUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
@@ -77,7 +78,7 @@ class ShutUpShortcutActivity : ComponentActivity() {
if (config != null && config.isEnabled) {
if (PermissionUtils.canWriteSecureSettings(this@ShutUpShortcutActivity)) {
- applyShutUpSettings(config, settingsRepository)
+ ShutUpManager.applyShutUpSettings(this@ShutUpShortcutActivity, config)
withContext(Dispatchers.Main) {
Toast.makeText(
this@ShutUpShortcutActivity,
@@ -96,142 +97,7 @@ class ShutUpShortcutActivity : ComponentActivity() {
}
}
- private suspend fun applyShutUpSettings(
- config: ShutUpAppConfig,
- repository: SettingsRepository
- ) {
- withContext(Dispatchers.IO) {
- val originalSettings = mutableMapOf()
-
- if (config.disableDevOptions) {
- // Backup all relevant dev settings because disabling the main toggle might reset them
- val secureSettings = listOf(
- "anr_show_background",
- "bugreport_in_power_menu",
- "display_density_forced",
- "mock_location",
- "secure_overlay_settings",
- "usb_audio_automatic_routing_disabled"
- )
- val systemSettings = listOf("show_touches", "show_key_presses")
- val globalSettings = listOf(
- "adb_allowed_connection_time",
- "adb_enabled",
- "adb_wifi_enabled",
- "always_finish_activities",
- "animator_duration_scale",
- "app_standby_enabled",
- "cached_apps_freezer",
- "default_install_location",
- "development_settings_enabled",
- "disable_window_blurs",
- "enable_freeform_support",
- "enable_non_resizable_multi_window",
- "force_allow_on_external",
- "force_desktop_mode_on_external_displays",
- "force_resizable_activities",
- "mobile_data_always_on",
- "stay_on_while_plugged_in",
- "usb_mass_storage_enabled",
- "wait_for_debugger",
- "wifi_display_certification_on",
- "wifi_display_on",
- "wifi_scan_always_enabled",
- "window_animation_scale"
- )
-
- secureSettings.forEach { key ->
- safeReadSetting(contentResolver, SettingsTable.SECURE, key)
- ?.let { originalSettings["secure:$key"] = it }
- }
- systemSettings.forEach { key ->
- safeReadSetting(contentResolver, SettingsTable.SYSTEM, key)
- ?.let { originalSettings["system:$key"] = it }
- }
- globalSettings.forEach { key ->
- safeReadSetting(contentResolver, SettingsTable.GLOBAL, key)
- ?.let { originalSettings["global:$key"] = it }
- }
-
- // Disable dev options
- Settings.Global.putString(
- contentResolver,
- Settings.Global.DEVELOPMENT_SETTINGS_ENABLED,
- "0"
- )
- }
- // Always explicitly disable USB debugging if requested, even if dev options were already disabled
- // as some apps check this specific setting directly.
- if (config.disableUsbDebugging) {
- val current =
- safeReadSetting(
- contentResolver,
- SettingsTable.GLOBAL,
- Settings.Global.ADB_ENABLED
- )
- ?: "0"
- if (current == "1") {
- if (!originalSettings.containsKey("global:${Settings.Global.ADB_ENABLED}")) {
- originalSettings["global:${Settings.Global.ADB_ENABLED}"] = "1"
- }
- Settings.Global.putString(contentResolver, Settings.Global.ADB_ENABLED, "0")
- }
- }
-
- if (config.disableWirelessDebugging) {
- val current =
- safeReadSetting(contentResolver, SettingsTable.GLOBAL, "adb_wifi_enabled")
- ?: "0"
- if (current == "1") {
- if (!originalSettings.containsKey("global:adb_wifi_enabled")) {
- originalSettings["global:adb_wifi_enabled"] = "1"
- }
- Settings.Global.putString(contentResolver, "adb_wifi_enabled", "0")
- }
- }
-
- if (config.disableAccessibility) {
- val current = safeReadSetting(
- contentResolver,
- SettingsTable.SECURE,
- Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES
- )
- if (!current.isNullOrEmpty()) {
- originalSettings["secure:${Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES}"] =
- current
- Settings.Secure.putString(
- contentResolver,
- Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
- ""
- )
- }
- }
-
- if (originalSettings.isNotEmpty()) {
- repository.saveShutUpOriginalSettings(originalSettings)
- }
- }
- }
-
- private enum class SettingsTable { SYSTEM, SECURE, GLOBAL }
-
- // Android 12+ throws SecurityException reading @hide settings that aren't
- // @Readable (e.g. show_key_presses). WRITE_SECURE_SETTINGS doesn't cover reads.
- private fun safeReadSetting(
- resolver: ContentResolver,
- table: SettingsTable,
- key: String
- ): String? = try {
- when (table) {
- SettingsTable.SYSTEM -> Settings.System.getString(resolver, key)
- SettingsTable.SECURE -> Settings.Secure.getString(resolver, key)
- SettingsTable.GLOBAL -> Settings.Global.getString(resolver, key)
- }
- } catch (e: SecurityException) {
- Log.w("ShutUpShortcut", "Skipping unreadable setting $table:$key", e)
- null
- }
private fun launchApp(packageName: String) {
val intent = packageManager.getLaunchIntentForPackage(packageName)
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 de997eca6..34cf4b710 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
@@ -13,6 +13,8 @@ import com.sameerasw.essentials.domain.model.NotificationLightingSweepPosition
import com.sameerasw.essentials.domain.model.ScaleAnimationsProfile
import com.sameerasw.essentials.domain.model.TrackedRepo
import com.sameerasw.essentials.domain.model.github.GitHubUser
+import com.sameerasw.essentials.domain.model.ShutUpAppConfig
+
import com.sameerasw.essentials.utils.RootUtils
import com.sameerasw.essentials.utils.ShizukuUtils
import kotlinx.coroutines.channels.awaitClose
@@ -23,7 +25,7 @@ class SettingsRepository(private val context: Context) {
private val prefs: SharedPreferences =
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
- private val gson = Gson()
+ private val gson = com.google.gson.GsonBuilder().create()
init {
migrateUsageAccessKey()
@@ -252,6 +254,8 @@ class SettingsRepository(private val context: Context) {
const val LIVE_WALLPAPER_TRIGGER_UNLOCK = "unlock"
const val LIVE_WALLPAPER_TRIGGER_SCREEN_ON = "screen_on"
+ const val KEY_DISABLE_ROTATION_SUGGESTION = "disable_rotation_suggestion"
+
const val KEY_SHUT_UP_SELECTED_APPS = "shut_up_selected_apps"
const val KEY_SHUT_UP_ORIGINAL_SETTINGS = "shut_up_original_settings"
const val KEY_SHUT_UP_ATTEMPT_SHIZUKU_RESTART = "shut_up_attempt_shizuku_restart"
@@ -259,7 +263,6 @@ class SettingsRepository(private val context: Context) {
const val KEY_SHUT_UP_RESTORE_MODE = "shut_up_restore_mode"
const val KEY_SHIZUKU_AUTH_TOKEN = "shizuku_auth_token"
const val KEY_EDGE_LIGHTING_SWEEP_SELECTED_SHAPES = "edge_lighting_sweep_selected_shapes"
- const val KEY_DISABLE_ROTATION_SUGGESTION = "disable_rotation_suggestion"
const val KEY_PIXEL_SEARCHBAR = "pixel_searchbar"
const val KEY_PIXEL_SEARCHBAR_TYPE = "pixel_searchbar_type"
const val KEY_PIXEL_SEARCHBAR_DATE_FORMAT = "pixel_searchbar_date_format"
@@ -290,6 +293,7 @@ class SettingsRepository(private val context: Context) {
const val KEY_POCKET_MODE_TRIGGER_DELAY = "pocket_mode_trigger_delay"
const val KEY_POCKET_MODE_LOCK_SCREEN_ONLY = "pocket_mode_lock_screen_only"
const val KEY_KEEP_PREFS = "keep_prefs"
+ const val KEY_SHUT_UP_SERVICE_ENABLED = "shutup_service_enabled"
}
// Observe changes
@@ -556,52 +560,6 @@ class SettingsRepository(private val context: Context) {
fun updatePocketModeExcludedAppSelection(packageName: String, enabled: Boolean) =
updateAppSelection(KEY_POCKET_MODE_EXCLUDED_APPS, packageName, enabled)
- fun loadShutUpConfigs(): List {
- val json = prefs.getString(KEY_SHUT_UP_SELECTED_APPS, null)
- return if (json != null) {
- try {
- gson.fromJson(
- json,
- Array::class.java
- ).toList()
- } catch (e: Exception) {
- emptyList()
- }
- } else {
- emptyList()
- }
- }
-
- fun saveShutUpConfigs(configs: List) {
- val json = gson.toJson(configs)
- putString(KEY_SHUT_UP_SELECTED_APPS, json)
- }
-
- fun updateShutUpConfig(config: com.sameerasw.essentials.domain.model.ShutUpAppConfig) {
- val current = loadShutUpConfigs().toMutableList()
- val index = current.indexOfFirst { it.packageName == config.packageName }
- if (index != -1) {
- current[index] = config
- } else {
- current.add(config)
- }
- saveShutUpConfigs(current)
- }
-
- fun saveShutUpOriginalSettings(settings: Map) {
- val json = gson.toJson(settings)
- putString(KEY_SHUT_UP_ORIGINAL_SETTINGS, json)
- }
-
- fun getShutUpOriginalSettings(): Map {
- val json = prefs.getString(KEY_SHUT_UP_ORIGINAL_SETTINGS, null) ?: return emptyMap()
- return try {
- @Suppress("UNCHECKED_CAST")
- gson.fromJson(json, Map::class.java) as Map
- } catch (e: Exception) {
- emptyMap()
- }
- }
private fun updateAppSelection(key: String, packageName: String, enabled: Boolean) {
val current = loadAppSelection(key).toMutableList()
@@ -950,11 +908,6 @@ class SettingsRepository(private val context: Context) {
saveTrackedRepos(current)
}
- fun isShutUpAttemptShizukuRestartEnabled(): Boolean =
- getBoolean(KEY_SHUT_UP_ATTEMPT_SHIZUKU_RESTART, true)
-
- fun setShutUpAttemptShizukuRestartEnabled(enabled: Boolean) =
- putBoolean(KEY_SHUT_UP_ATTEMPT_SHIZUKU_RESTART, enabled)
fun getShutUpRestoreDelay(): Int =
getInt(KEY_SHUT_UP_RESTORE_DELAY, 10)
@@ -1723,5 +1676,49 @@ class SettingsRepository(private val context: Context) {
fun getLockScreenClockSeedColor(): Int = getInt(KEY_LOCK_SCREEN_CLOCK_SEED_COLOR, 0)
fun setLockScreenClockSeedColor(value: Int) = putInt(KEY_LOCK_SCREEN_CLOCK_SEED_COLOR, value)
+
+ fun loadShutUpConfigs(): List {
+ val json = prefs.getString(KEY_SHUT_UP_SELECTED_APPS, null)
+ return if (json != null) {
+ try {
+ gson.fromJson(
+ json,
+ Array::class.java
+ ).toList()
+ } catch (e: Exception) {
+ emptyList()
+ }
+ } else {
+ emptyList()
+ }
+ }
+
+ fun saveShutUpConfigs(configs: List) {
+ val json = gson.toJson(configs)
+ putString(KEY_SHUT_UP_SELECTED_APPS, json)
+ }
+
+ fun isShutUpServiceEnabled(): Boolean {
+ return prefs.getBoolean(KEY_SHUT_UP_SERVICE_ENABLED, false)
+ }
+
+ fun setShutUpServiceEnabled(enabled: Boolean) {
+ putBoolean(KEY_SHUT_UP_SERVICE_ENABLED, enabled)
+ }
+
+ fun saveShutUpOriginalSettings(settings: Map) {
+ val json = gson.toJson(settings)
+ putString(KEY_SHUT_UP_ORIGINAL_SETTINGS, json)
+ }
+
+ fun getShutUpOriginalSettings(): Map {
+ val json = prefs.getString(KEY_SHUT_UP_ORIGINAL_SETTINGS, null) ?: return emptyMap()
+ return try {
+ @Suppress("UNCHECKED_CAST")
+ gson.fromJson(json, Map::class.java) as Map
+ } catch (e: Exception) {
+ emptyMap()
+ }
+ }
}
diff --git a/app/src/main/java/com/sameerasw/essentials/domain/model/AppSetting.kt b/app/src/main/java/com/sameerasw/essentials/domain/model/AppSetting.kt
new file mode 100644
index 000000000..62ae8eee5
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/domain/model/AppSetting.kt
@@ -0,0 +1,10 @@
+package com.sameerasw.essentials.domain.model
+
+data class AppSetting(
+ val enabled: Boolean = true,
+ val settingType: String, // "GLOBAL", "SECURE", "SYSTEM"
+ val key: String,
+ val valueOnLaunch: String,
+ val valueOnRevert: String,
+ val label: String
+)
diff --git a/app/src/main/java/com/sameerasw/essentials/domain/model/Feature.kt b/app/src/main/java/com/sameerasw/essentials/domain/model/Feature.kt
index c7e17759a..2b614ae40 100644
--- a/app/src/main/java/com/sameerasw/essentials/domain/model/Feature.kt
+++ b/app/src/main/java/com/sameerasw/essentials/domain/model/Feature.kt
@@ -39,7 +39,8 @@ abstract class Feature(
@StringRes val aboutDescription: Int? = null,
@androidx.annotation.RawRes val animationRes: Int = 0
) {
- val requiresAuth: Boolean = category == com.sameerasw.essentials.R.string.cat_protection
+ open val requiresAuth: Boolean
+ get() = category == com.sameerasw.essentials.R.string.cat_protection
abstract fun isEnabled(viewModel: MainViewModel): Boolean
diff --git a/app/src/main/java/com/sameerasw/essentials/domain/model/ShutUpAppConfig.kt b/app/src/main/java/com/sameerasw/essentials/domain/model/ShutUpAppConfig.kt
index a9a0551b1..27e019e63 100644
--- a/app/src/main/java/com/sameerasw/essentials/domain/model/ShutUpAppConfig.kt
+++ b/app/src/main/java/com/sameerasw/essentials/domain/model/ShutUpAppConfig.kt
@@ -3,9 +3,65 @@ package com.sameerasw.essentials.domain.model
data class ShutUpAppConfig(
val packageName: String,
val isEnabled: Boolean = true,
- val disableDevOptions: Boolean = true,
- val disableUsbDebugging: Boolean = true,
- val disableWirelessDebugging: Boolean = true,
- val disableAccessibility: Boolean = false,
+ val settings: List = emptyList(),
+ val attemptShizukuRestart: Boolean = false,
val autoArchive: Boolean = false
)
+
+val ShutUpAppConfig.disableDevOptions: Boolean
+ get() = settings.any { it.key == "development_settings_enabled" && it.enabled }
+
+val ShutUpAppConfig.disableUsbDebugging: Boolean
+ get() = settings.any { it.key == "adb_enabled" && it.enabled }
+
+val ShutUpAppConfig.disableWirelessDebugging: Boolean
+ get() = settings.any { it.key == "adb_wifi_enabled" && it.enabled }
+
+val ShutUpAppConfig.disableAccessibility: Boolean
+ get() = settings.any { it.key == "accessibility_enabled" && it.enabled }
+
+fun ShutUpAppConfig.copy(
+ packageName: String = this.packageName,
+ isEnabled: Boolean = this.isEnabled,
+ attemptShizukuRestart: Boolean = this.attemptShizukuRestart,
+ autoArchive: Boolean = this.autoArchive,
+ disableDevOptions: Boolean = this.disableDevOptions,
+ disableUsbDebugging: Boolean = this.disableUsbDebugging,
+ disableWirelessDebugging: Boolean = this.disableWirelessDebugging,
+ disableAccessibility: Boolean = this.disableAccessibility
+): ShutUpAppConfig {
+ val newList = settings.toMutableList()
+
+ fun updateKey(key: String, label: String, enabled: Boolean) {
+ val existing = newList.find { it.key == key }
+ if (existing != null) {
+ newList[newList.indexOf(existing)] = existing.copy(enabled = enabled)
+ } else if (enabled) {
+ val type = if (key == "accessibility_enabled") "SECURE" else "GLOBAL"
+ newList.add(
+ AppSetting(
+ label = label,
+ settingType = type,
+ key = key,
+ valueOnLaunch = "0",
+ valueOnRevert = "1",
+ enabled = true
+ )
+ )
+ }
+ }
+
+ updateKey("development_settings_enabled", "Hide Developer Options", disableDevOptions)
+ updateKey("adb_enabled", "Hide USB Debugging", disableUsbDebugging)
+ updateKey("adb_wifi_enabled", "Hide Wireless Debugging", disableWirelessDebugging)
+ updateKey("accessibility_enabled", "Hide Accessibility Services", disableAccessibility)
+
+ return ShutUpAppConfig(
+ packageName = packageName,
+ isEnabled = isEnabled,
+ settings = newList,
+ attemptShizukuRestart = attemptShizukuRestart,
+ autoArchive = autoArchive
+ )
+}
+
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 35c2e0bea..3318794f4 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
@@ -931,23 +931,36 @@ object FeatureRegistry {
override fun onToggle(viewModel: MainViewModel, context: Context, enabled: Boolean) =
viewModel.setAppLockEnabled(enabled, context)
},
+
object : Feature(
id = "Shut-Up!",
title = R.string.feat_shut_up_title,
- iconRes = R.drawable.rounded_domino_mask_24,
- category = R.string.cat_system,
+ iconRes = R.drawable.rounded_shield_lock_24,
+ category = R.string.cat_protection,
description = R.string.feat_shut_up_desc,
aboutDescription = R.string.shut_up_description,
- permissionKeys = listOf("WRITE_SECURE_SETTINGS", "USAGE_STATS"),
+ permissionKeys = listOf("WRITE_SECURE_SETTINGS", "WRITE_SETTINGS", "USAGE_STATS", "POST_NOTIFICATIONS"),
showToggle = false,
hasMoreSettings = true,
parentFeatureId = "Security",
animationRes = R.raw.shutup_animation
) {
- override fun isEnabled(viewModel: MainViewModel) = true
- override fun onToggle(viewModel: MainViewModel, context: Context, enabled: Boolean) {}
+ override val requiresAuth: Boolean = false
+
+ override fun isEnabled(viewModel: MainViewModel) =
+ viewModel.isShutUpServiceEnabled.value
+
+ override fun isToggleEnabled(viewModel: MainViewModel, context: Context) =
+ viewModel.isWriteSecureSettingsEnabled.value &&
+ viewModel.isWriteSettingsEnabled.value &&
+ viewModel.isUsageStatsPermissionGranted.value &&
+ viewModel.isPostNotificationsEnabled.value
+
+ override fun onToggle(viewModel: MainViewModel, context: Context, enabled: Boolean) =
+ viewModel.setShutUpServiceEnabled(enabled, context)
},
+
object : Feature(
id = "Pocket mode",
title = R.string.feat_pocket_mode_title,
diff --git a/app/src/main/java/com/sameerasw/essentials/domain/registry/PermissionRegistry.kt b/app/src/main/java/com/sameerasw/essentials/domain/registry/PermissionRegistry.kt
index 15c9ce40f..ecf5fb3f2 100644
--- a/app/src/main/java/com/sameerasw/essentials/domain/registry/PermissionRegistry.kt
+++ b/app/src/main/java/com/sameerasw/essentials/domain/registry/PermissionRegistry.kt
@@ -93,8 +93,9 @@ fun initPermissionRegistry() {
// Default browser permission
PermissionRegistry.register("DEFAULT_BROWSER", R.string.feat_link_actions_title)
- // Shut-Up! feature
+ // Shut-Up! permissions
PermissionRegistry.register("WRITE_SECURE_SETTINGS", R.string.feat_shut_up_title)
PermissionRegistry.register("WRITE_SETTINGS", R.string.feat_shut_up_title)
PermissionRegistry.register("USAGE_STATS", R.string.feat_shut_up_title)
+ PermissionRegistry.register("POST_NOTIFICATIONS", R.string.feat_shut_up_title)
}
diff --git a/app/src/main/java/com/sameerasw/essentials/services/ShutUpForegroundService.kt b/app/src/main/java/com/sameerasw/essentials/services/ShutUpForegroundService.kt
new file mode 100644
index 000000000..52acfb062
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/services/ShutUpForegroundService.kt
@@ -0,0 +1,317 @@
+package com.sameerasw.essentials.services
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.app.Service
+import android.app.usage.UsageEvents
+import android.app.usage.UsageStatsManager
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ServiceInfo
+import android.os.Build
+import android.os.IBinder
+import android.util.Log
+import androidx.core.app.NotificationCompat
+import com.sameerasw.essentials.R
+import com.sameerasw.essentials.data.repository.SettingsRepository
+import com.sameerasw.essentials.domain.model.ShutUpAppConfig
+import com.sameerasw.essentials.utils.FreezeManager
+import com.sameerasw.essentials.utils.ShutUpManager
+import kotlinx.coroutines.*
+
+class ShutUpForegroundService : Service() {
+
+ private val serviceScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
+ private lateinit var settingsRepository: SettingsRepository
+ private var monitorJob: Job? = null
+ private var lastPackageName: String? = null
+ private var lastQueryTime = System.currentTimeMillis() - 5000
+
+ private var pendingRestoreJob: Job? = null
+ private var pendingRestorePackage: String? = null
+ private var freezeCountdownJob: Job? = null
+
+ // Active config for the currently monitored target app (used for periodic re-enforcement)
+ private var activeTargetConfig: com.sameerasw.essentials.domain.model.ShutUpAppConfig? = null
+ private var enforceTickCount = 0
+
+ companion object {
+ private const val TAG = "ShutUpForegroundService"
+ private const val CHANNEL_ID = "shutup_service_channel"
+ private const val NOTIFICATION_ID = 1002
+ private const val NOTIFICATION_FREEZE_ID = 1003
+
+ const val ACTION_STOP_SERVICE = "ACTION_STOP_SERVICE"
+ const val ACTION_FREEZE_NOW = "ACTION_FREEZE_NOW"
+ const val ACTION_ABORT_FREEZE = "ACTION_ABORT_FREEZE"
+ const val EXTRA_PACKAGE_NAME = "package_name"
+
+ var isRunning = false
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ isRunning = true
+ settingsRepository = SettingsRepository(this)
+ createNotificationChannel()
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ when (intent?.action) {
+ ACTION_STOP_SERVICE -> {
+ stopForeground(true)
+ stopSelf()
+ return START_NOT_STICKY
+ }
+ ACTION_FREEZE_NOW -> {
+ val pkg = intent.getStringExtra(EXTRA_PACKAGE_NAME)
+ if (pkg != null) {
+ freezeCountdownJob?.cancel()
+ val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
+ notificationManager.cancel(NOTIFICATION_FREEZE_ID)
+ serviceScope.launch {
+ FreezeManager.freezeApp(this@ShutUpForegroundService, pkg)
+ }
+ }
+ return START_STICKY
+ }
+ ACTION_ABORT_FREEZE -> {
+ freezeCountdownJob?.cancel()
+ val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
+ notificationManager.cancel(NOTIFICATION_FREEZE_ID)
+ return START_STICKY
+ }
+ }
+
+ startForeground(
+ NOTIFICATION_ID,
+ createServiceNotification(),
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE else 0
+ )
+
+ startMonitoring()
+ return START_STICKY
+ }
+
+ private fun startMonitoring() {
+ if (monitorJob != null) return
+ monitorJob = serviceScope.launch {
+ while (isActive) {
+ val now = System.currentTimeMillis()
+ val currentPkg = getForegroundPackage(lastQueryTime, now)
+ if (currentPkg != null) {
+ lastQueryTime = now
+ if (currentPkg != lastPackageName) {
+ onPackageChanged(lastPackageName, currentPkg)
+ lastPackageName = currentPkg
+ enforceTickCount = 0
+ } else {
+ // Re-enforce settings every ~2s while target app stays in foreground
+ // This ensures settings stay hidden even if something re-enables them between opens
+ enforceTickCount++
+ if (enforceTickCount % 5 == 0) {
+ activeTargetConfig?.let { config ->
+ Log.d(TAG, "Re-enforcing ShutUp settings for ${config.packageName}")
+ ShutUpManager.applyShutUpSettings(this@ShutUpForegroundService, config)
+ }
+ }
+ }
+ } else {
+ lastQueryTime = now - 500
+ }
+ delay(400)
+ }
+ }
+ }
+
+ private fun getForegroundPackage(startTime: Long, endTime: Long): String? {
+ val usageStatsManager = getSystemService(USAGE_STATS_SERVICE) as UsageStatsManager
+ try {
+ val events = usageStatsManager.queryEvents(startTime, endTime)
+ val event = UsageEvents.Event()
+ var lastResumedPackage: String? = null
+ while (events.hasNextEvent()) {
+ events.getNextEvent(event)
+ if (event.eventType == UsageEvents.Event.ACTIVITY_RESUMED) {
+ lastResumedPackage = event.packageName
+ }
+ }
+ if (lastResumedPackage != null) {
+ return lastResumedPackage
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to query usage events", e)
+ }
+ return null
+ }
+
+ private suspend fun onPackageChanged(oldPkg: String?, newPkg: String?) {
+ if (newPkg == null || ShutUpManager.isPackageIgnored(newPkg)) return
+
+ val configs = settingsRepository.loadShutUpConfigs()
+
+ // 1. Leaving a Shut-Up app
+ if (oldPkg != null && configs.any { it.packageName == oldPkg && it.isEnabled }) {
+ if (newPkg != oldPkg) {
+ activeTargetConfig = null
+ pendingRestoreJob?.cancel()
+ pendingRestorePackage = oldPkg
+ pendingRestoreJob = serviceScope.launch {
+ var shouldContinue = true
+ while (shouldContinue) {
+ delay(3000) // 3 seconds debounce / check interval
+ val config = settingsRepository.loadShutUpConfigs().find { it.packageName == oldPkg }
+ if (config != null && config.isEnabled) {
+ if (!ShutUpManager.isAppRunning(this@ShutUpForegroundService, oldPkg)) {
+ if (activeTargetConfig != null) {
+ // A different Shut-Up app is now active; skip restoration to avoid
+ // re-enabling settings (USB debugging, dev options, etc.) while it is running
+ Log.d(TAG, "Skipping restore for $oldPkg — another Shut-Up app is active (${activeTargetConfig?.packageName})")
+ shouldContinue = false
+ } else {
+ ShutUpManager.revertShutUpSettings(this@ShutUpForegroundService, config)
+ ShutUpManager.restoreOriginalSettings(this@ShutUpForegroundService, settingsRepository)
+ if (config.attemptShizukuRestart) {
+ ShutUpManager.restartShizuku(this@ShutUpForegroundService)
+ }
+ if (config.autoArchive) {
+ showAutoFreezeNotification(config.packageName)
+ }
+ shouldContinue = false
+ }
+ } else {
+ Log.d(TAG, "$oldPkg is still running in the background, delaying revert settings...")
+ }
+ } else {
+ shouldContinue = false
+ }
+ }
+ pendingRestorePackage = null
+ }
+ }
+ }
+
+ // 2. Entering a Shut-Up app
+ val newConfig = configs.find { it.packageName == newPkg }
+ if (newConfig != null && newConfig.isEnabled) {
+ activeTargetConfig = newConfig
+ if (pendingRestorePackage == newPkg) {
+ pendingRestoreJob?.cancel()
+ pendingRestorePackage = null
+ }
+ freezeCountdownJob?.cancel()
+ val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
+ notificationManager.cancel(NOTIFICATION_FREEZE_ID)
+
+ // Apply inline — no extra coroutine spawn, runs directly in monitoring coroutine
+ ShutUpManager.applyShutUpSettings(this@ShutUpForegroundService, newConfig)
+ } else {
+ activeTargetConfig = null
+ }
+ }
+
+ private fun showAutoFreezeNotification(packageName: String) {
+ freezeCountdownJob?.cancel()
+ val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
+ val appName = try {
+ val appInfo = packageManager.getApplicationInfo(packageName, 0)
+ packageManager.getApplicationLabel(appInfo).toString()
+ } catch (e: Exception) {
+ packageName
+ }
+
+ freezeCountdownJob = serviceScope.launch {
+ var secondsRemaining = 5
+ while (secondsRemaining > 0) {
+ val stopIntent = Intent(this@ShutUpForegroundService, ShutUpForegroundService::class.java).apply {
+ action = ACTION_FREEZE_NOW
+ putExtra(EXTRA_PACKAGE_NAME, packageName)
+ }
+ val stopPendingIntent = PendingIntent.getService(
+ this@ShutUpForegroundService,
+ 101,
+ stopIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+
+ val abortIntent = Intent(this@ShutUpForegroundService, ShutUpForegroundService::class.java).apply {
+ action = ACTION_ABORT_FREEZE
+ }
+ val abortPendingIntent = PendingIntent.getService(
+ this@ShutUpForegroundService,
+ 102,
+ abortIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+
+ val notification = NotificationCompat.Builder(this@ShutUpForegroundService, CHANNEL_ID)
+ .setContentTitle(getString(R.string.shut_up_auto_archive_notif_title))
+ .setContentText(getString(R.string.shut_up_auto_archive_notif_text, appName, secondsRemaining))
+ .setSmallIcon(R.drawable.rounded_snowflake_24)
+ .setOngoing(true)
+ .addAction(R.drawable.rounded_snowflake_24, getString(R.string.shut_up_auto_archive_action_freeze), stopPendingIntent)
+ .addAction(R.drawable.rounded_close_24, getString(R.string.shut_up_auto_archive_action_abort), abortPendingIntent)
+ .build()
+
+ notificationManager.notify(NOTIFICATION_FREEZE_ID, notification)
+ delay(1000)
+ secondsRemaining--
+ }
+
+ FreezeManager.freezeApp(this@ShutUpForegroundService, packageName)
+ notificationManager.cancel(NOTIFICATION_FREEZE_ID)
+ }
+ }
+
+ override fun onDestroy() {
+ isRunning = false
+ monitorJob?.cancel()
+ pendingRestoreJob?.cancel()
+ freezeCountdownJob?.cancel()
+ serviceScope.cancel()
+
+ val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
+ notificationManager.cancel(NOTIFICATION_FREEZE_ID)
+
+ super.onDestroy()
+ }
+
+ override fun onBind(intent: Intent?): IBinder? = null
+
+ private fun createNotificationChannel() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val channel = NotificationChannel(
+ CHANNEL_ID,
+ getString(R.string.shut_up_service_name),
+ NotificationManager.IMPORTANCE_LOW
+ ).apply {
+ description = getString(R.string.shut_up_service_desc)
+ }
+ val manager = getSystemService(NotificationManager::class.java)
+ manager.createNotificationChannel(channel)
+ }
+ }
+
+ private fun createServiceNotification(): Notification {
+ val stopIntent = Intent(this, ShutUpForegroundService::class.java).apply {
+ action = ACTION_STOP_SERVICE
+ }
+ val stopPendingIntent = PendingIntent.getService(
+ this,
+ 201,
+ stopIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+
+ return NotificationCompat.Builder(this, CHANNEL_ID)
+ .setContentTitle(getString(R.string.shut_up_service_notification_title))
+ .setContentText(getString(R.string.shut_up_service_notification_desc))
+ .setSmallIcon(R.drawable.rounded_shield_lock_24)
+ .setOngoing(true)
+ .addAction(R.drawable.rounded_close_24, getString(R.string.action_stop), stopPendingIntent)
+ .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
+ .build()
+ }
+}
diff --git a/app/src/main/java/com/sameerasw/essentials/services/handlers/AppFlowHandler.kt b/app/src/main/java/com/sameerasw/essentials/services/handlers/AppFlowHandler.kt
index 1a6b75f94..c6e9eb172 100644
--- a/app/src/main/java/com/sameerasw/essentials/services/handlers/AppFlowHandler.kt
+++ b/app/src/main/java/com/sameerasw/essentials/services/handlers/AppFlowHandler.kt
@@ -33,58 +33,7 @@ class AppFlowHandler(
private val service: AccessibilityService? = null
) {
private val handler = Handler(Looper.getMainLooper())
- private val scope = CoroutineScope(Dispatchers.Main)
-
- private val authenticatedPackages = mutableSetOf()
- private val lastLeaveTimes = mutableMapOf()
- private val activeCountdowns = mutableMapOf()
-
- private val shutUpReceiver = object : BroadcastReceiver() {
- override fun onReceive(context: Context?, intent: Intent?) {
- when (intent?.action) {
- ACTION_FREEZE_NOW -> {
- val packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME) ?: return
- activeCountdowns[packageName]?.cancel()
- activeCountdowns.remove(packageName)
- context?.let { FreezeManager.freezeApp(it, packageName) }
- cancelNotification(packageName)
- }
-
- ACTION_ABORT_FREEZE -> {
- val packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME) ?: return
- activeCountdowns[packageName]?.cancel()
- activeCountdowns.remove(packageName)
- cancelNotification(packageName)
- }
-
- ACTION_RESTORE_NOW -> {
- cancelRestoreNotification()
- val autoPkg = intent.getStringExtra(EXTRA_AUTO_ARCHIVE_PACKAGE)
- val pkgName = intent.getStringExtra(EXTRA_PACKAGE_NAME)
- val settingsRepo = com.sameerasw.essentials.data.repository.SettingsRepository(
- context ?: return
- )
- val config = if (pkgName != null) {
- settingsRepo.loadShutUpConfigs().find { it.packageName == pkgName }
- } else null
- restoreShutUpSettings(settingsRepo, config, autoPkg, forceRestore = true)
- }
- }
- }
- }
-
- init {
- val filter = IntentFilter().apply {
- addAction(ACTION_FREEZE_NOW)
- addAction(ACTION_ABORT_FREEZE)
- addAction(ACTION_RESTORE_NOW)
- }
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
- context.registerReceiver(shutUpReceiver, filter, Context.RECEIVER_EXPORTED)
- } else {
- context.registerReceiver(shutUpReceiver, filter)
- }
- }
+ private val scope = CoroutineScope(Dispatchers.Main.immediate)
// App Lock State
private var lockingPackage: String? = null
@@ -117,7 +66,6 @@ class AppFlowHandler(
currentPackage = packageName
if (oldPackage != null && oldPackage != packageName) {
lastLeaveTimes[oldPackage] = System.currentTimeMillis()
- checkShutUpRestore(oldPackage, packageName)
}
if (packageName != context.packageName && packageName != lockingPackage) {
lockingPackage = null
@@ -126,6 +74,7 @@ class AppFlowHandler(
checkHighlightNightLight(packageName)
checkAppAutomations(packageName)
checkGestureBarAutomation(packageName)
+ checkShutUp(packageName)
}
}
@@ -374,462 +323,23 @@ class AppFlowHandler(
return launchers.any { it.activityInfo.packageName == packageName }
}
- private fun checkShutUpRestore(oldPackage: String?, newPackage: String?) {
- Log.d("AppFlowHandler", "checkShutUpRestore: old=$oldPackage, new=$newPackage")
- if (oldPackage == null || oldPackage == newPackage) return
-
- val settingsRepository =
- com.sameerasw.essentials.data.repository.SettingsRepository(context)
- val shutUpConfigs = settingsRepository.loadShutUpConfigs()
-
- val wasShutUpConfig = shutUpConfigs.find { it.packageName == oldPackage && it.isEnabled }
-
- // Check if it was already frozen to avoid duplicate triggers (e.g. on screen off)
- val isAlreadyFrozen = oldPackage.let { FreezeManager.isAppFrozen(context, it) }
-
- // We consider the new app a Shut-Up app if it's in the list OR if it's the shortcut activity
- val isNewAppShutUp = shutUpConfigs.any { it.packageName == newPackage && it.isEnabled } ||
- newPackage == "com.sameerasw.essentials.ShutUpShortcutActivity"
-
- Log.d(
- "AppFlowHandler",
- "checkShutUpRestore: wasShutUpConfig=${wasShutUpConfig != null}, isNewAppShutUp=$isNewAppShutUp, isAlreadyFrozen=$isAlreadyFrozen"
- )
-
- // If it's already frozen, we've already handled it
- if (isAlreadyFrozen) return
-
- // If we are entering a Shut-Up app, cancel ANY pending countdowns for other apps
- if (isNewAppShutUp) {
- if (activeCountdowns.isNotEmpty()) {
- Log.d(
- "AppFlowHandler",
- "checkShutUpRestore: Entering Shut-Up app, cancelling all pending countdowns"
- )
- activeCountdowns.values.forEach { it.cancel() }
- activeCountdowns.keys.forEach { cancelNotification(it) }
- activeCountdowns.clear()
- }
- }
-
- if (wasShutUpConfig != null && !isNewAppShutUp) {
- Log.d("AppFlowHandler", "checkShutUpRestore: Triggering restoration for $oldPackage")
- restoreShutUpSettings(
- settingsRepository,
- wasShutUpConfig,
- if (wasShutUpConfig.autoArchive) wasShutUpConfig.packageName else null
- )
- }
- }
-
- private fun startAutoArchiveCountdown(packageName: String) {
- Log.d("AppFlowHandler", "startAutoArchiveCountdown: $packageName")
- // Cancel existing countdown for this app if any
- activeCountdowns[packageName]?.cancel()
-
- val appName = try {
- val appInfo = context.packageManager.getApplicationInfo(packageName, 0)
- context.packageManager.getApplicationLabel(appInfo).toString()
- } catch (e: Exception) {
- Log.e("AppFlowHandler", "Failed to get app name for $packageName", e)
- packageName
- }
-
- val job = scope.launch {
- Log.d("AppFlowHandler", "Countdown job started for $packageName")
- for (i in 10 downTo 1) {
- Log.d("AppFlowHandler", "Countdown for $packageName: $i")
- showCountdownNotification(packageName, appName, i)
- delay(1000)
- }
- // countdown finished
- Log.d("AppFlowHandler", "Countdown finished for $packageName, freezing...")
- val success = withContext(Dispatchers.IO) {
- FreezeManager.freezeApp(context, packageName)
- }
- Log.d("AppFlowHandler", "Freeze result for $packageName: $success")
- cancelNotification(packageName)
- activeCountdowns.remove(packageName)
- }
- activeCountdowns[packageName] = job
- }
-
- private fun showCountdownNotification(packageName: String, appName: String, secondsLeft: Int) {
- createNotificationChannel()
- val notificationManager =
- context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
-
- val freezeIntent = Intent(ACTION_FREEZE_NOW).apply {
- `package` = context.packageName
- putExtra(EXTRA_PACKAGE_NAME, packageName)
- }
- val freezePendingIntent = PendingIntent.getBroadcast(
- context,
- packageName.hashCode() + 1,
- freezeIntent,
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
- )
-
- val abortIntent = Intent(ACTION_ABORT_FREEZE).apply {
- `package` = context.packageName
- putExtra(EXTRA_PACKAGE_NAME, packageName)
- }
- val abortPendingIntent = PendingIntent.getBroadcast(
- context,
- packageName.hashCode() + 2,
- abortIntent,
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
- )
-
- val title =
- context.getString(com.sameerasw.essentials.R.string.shut_up_auto_archive_notif_title)
- val text = context.getString(
- com.sameerasw.essentials.R.string.shut_up_auto_archive_notif_text,
- appName,
- secondsLeft
- )
- val criticalText = secondsLeft.toString()
-
- val notification =
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
- val builder = android.app.Notification.Builder(context, "shutup_alerts_channel")
- .setSmallIcon(com.sameerasw.essentials.R.drawable.rounded_snowflake_24)
- .setContentTitle(title)
- .setContentText(text)
- .setOngoing(true)
- .setOnlyAlertOnce(true)
- .setCategory(android.app.Notification.CATEGORY_SERVICE)
- .setShowWhen(false)
- .setGroup("shutup_auto_archive")
- .setColorized(false)
-
- if (android.os.Build.VERSION.SDK_INT >= 31) {
- builder.setForegroundServiceBehavior(android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE)
- }
-
- builder.addAction(
- android.app.Notification.Action.Builder(
- android.graphics.drawable.Icon.createWithResource(
- context,
- com.sameerasw.essentials.R.drawable.rounded_snowflake_24
- ),
- context.getString(com.sameerasw.essentials.R.string.shut_up_auto_archive_action_freeze),
- freezePendingIntent
- ).build()
- )
- builder.addAction(
- android.app.Notification.Action.Builder(
- android.graphics.drawable.Icon.createWithResource(
- context,
- com.sameerasw.essentials.R.drawable.rounded_close_24
- ),
- context.getString(com.sameerasw.essentials.R.string.shut_up_auto_archive_action_abort),
- abortPendingIntent
- ).build()
- )
-
- // Live Update Status Chip
- try {
- val setRequestPromotedOngoing = builder.javaClass.getMethod(
- "setRequestPromotedOngoing",
- Boolean::class.javaPrimitiveType
- )
- setRequestPromotedOngoing.invoke(builder, true)
-
- val setShortCriticalText = builder.javaClass.getMethod(
- "setShortCriticalText",
- CharSequence::class.java
- )
- setShortCriticalText.invoke(builder, criticalText)
- } catch (_: Throwable) {
- }
-
- val extras = android.os.Bundle()
- extras.putBoolean("android.requestPromotedOngoing", true)
- extras.putString("android.shortCriticalText", criticalText)
- builder.addExtras(extras)
-
- builder.setProgress(10, secondsLeft, false)
-
- builder.build()
- } else {
- NotificationCompat.Builder(context, "shutup_alerts_channel")
- .setSmallIcon(com.sameerasw.essentials.R.drawable.rounded_snowflake_24)
- .setContentTitle(title)
- .setContentText(text)
- .setPriority(NotificationCompat.PRIORITY_MAX)
- .setCategory(NotificationCompat.CATEGORY_SERVICE)
- .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
- .setOnlyAlertOnce(true)
- .setOngoing(true)
- .setProgress(10, secondsLeft, false)
- .addAction(
- com.sameerasw.essentials.R.drawable.rounded_snowflake_24,
- context.getString(com.sameerasw.essentials.R.string.shut_up_auto_archive_action_freeze),
- freezePendingIntent
- )
- .addAction(
- com.sameerasw.essentials.R.drawable.rounded_close_24,
- context.getString(com.sameerasw.essentials.R.string.shut_up_auto_archive_action_abort),
- abortPendingIntent
- )
- .addExtras(android.os.Bundle().apply {
- putBoolean("android.requestPromotedOngoing", true)
- putString("android.shortCriticalText", criticalText)
- })
- .build()
- }
-
- Log.d("AppFlowHandler", "Showing notification for $packageName, secondsLeft=$secondsLeft")
- notificationManager.notify(packageName.hashCode(), notification)
- }
-
- private fun cancelNotification(packageName: String) {
- val notificationManager =
- context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
- notificationManager.cancel(packageName.hashCode())
- }
-
- private fun createNotificationChannel() {
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
- val notificationManager =
- context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
-
- val channel = android.app.NotificationChannel(
- "app_detection_service_channel",
- context.getString(com.sameerasw.essentials.R.string.app_detection_service_channel_name),
- NotificationManager.IMPORTANCE_HIGH
- ).apply {
- description = "Channel for app detection alerts"
- }
- notificationManager.createNotificationChannel(channel)
-
- val alertChannel = android.app.NotificationChannel(
- "shutup_alerts_channel",
- "Shut-Up! Alerts",
- NotificationManager.IMPORTANCE_MAX
- ).apply {
- description = "Live update notifications for auto archiving"
- enableVibration(false)
- setSound(null, null)
- lockscreenVisibility = android.app.Notification.VISIBILITY_PUBLIC
- }
- notificationManager.createNotificationChannel(alertChannel)
-
- val restoreChannel = android.app.NotificationChannel(
- "shutup_restore_channel",
- "Shut-Up! Restore",
- NotificationManager.IMPORTANCE_MAX
- ).apply {
- description = "Notifications for restoring Shut-Up settings"
- enableVibration(false)
- setSound(null, null)
- lockscreenVisibility = android.app.Notification.VISIBILITY_PUBLIC
- }
- notificationManager.createNotificationChannel(restoreChannel)
- }
- }
-
- private fun showRestoreNotification(
- wasShutUpConfig: com.sameerasw.essentials.domain.model.ShutUpAppConfig?,
- autoArchivePackage: String?
- ) {
- createNotificationChannel()
- val notificationManager =
- context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
-
- val restoreIntent = Intent(ACTION_RESTORE_NOW).apply {
- `package` = context.packageName
- if (autoArchivePackage != null) {
- putExtra(EXTRA_AUTO_ARCHIVE_PACKAGE, autoArchivePackage)
- }
- if (wasShutUpConfig != null) {
- putExtra(EXTRA_PACKAGE_NAME, wasShutUpConfig.packageName)
- }
- }
- val restorePendingIntent = PendingIntent.getBroadcast(
- context,
- 12345,
- restoreIntent,
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
- )
-
- val title = "Shut-Up active"
- val text = "Do you want to restore now?"
- val criticalText = "Restore Now"
-
- val notification =
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
- val builder = android.app.Notification.Builder(context, "shutup_restore_channel")
- .setSmallIcon(com.sameerasw.essentials.R.drawable.rounded_code_24)
- .setContentTitle(title)
- .setContentText(text)
- .setCategory(android.app.Notification.CATEGORY_SERVICE)
- .setVisibility(android.app.Notification.VISIBILITY_PUBLIC)
- .setOnlyAlertOnce(true)
- .setOngoing(true)
- .addAction(
- android.app.Notification.Action.Builder(
- android.graphics.drawable.Icon.createWithResource(
- context,
- com.sameerasw.essentials.R.drawable.rounded_code_24
- ),
- "Restore Now",
- restorePendingIntent
- ).build()
- )
-
- try {
- val setShortCriticalText = builder.javaClass.getMethod(
- "setShortCriticalText",
- CharSequence::class.java
- )
- setShortCriticalText.invoke(builder, criticalText)
- } catch (_: Throwable) {
- }
-
- val extras = android.os.Bundle()
- extras.putBoolean("android.requestPromotedOngoing", true)
- extras.putString("android.shortCriticalText", criticalText)
- builder.addExtras(extras)
- builder.build()
- } else {
- NotificationCompat.Builder(context, "shutup_restore_channel")
- .setSmallIcon(com.sameerasw.essentials.R.drawable.rounded_code_24)
- .setContentTitle(title)
- .setContentText(text)
- .setPriority(NotificationCompat.PRIORITY_MAX)
- .setCategory(NotificationCompat.CATEGORY_SERVICE)
- .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
- .setOnlyAlertOnce(true)
- .setOngoing(true)
- .addAction(
- com.sameerasw.essentials.R.drawable.rounded_code_24,
- "Restore Now",
- restorePendingIntent
- )
- .addExtras(android.os.Bundle().apply {
- putBoolean("android.requestPromotedOngoing", true)
- putString("android.shortCriticalText", criticalText)
- })
- .build()
- }
-
- notificationManager.notify(NOTIFICATION_ID_SHUTUP_RESTORE, notification)
- }
-
- private fun cancelRestoreNotification() {
- val notificationManager =
- context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
- notificationManager.cancel(NOTIFICATION_ID_SHUTUP_RESTORE)
- }
-
- private fun restoreShutUpSettings(
- repository: com.sameerasw.essentials.data.repository.SettingsRepository,
- wasShutUpConfig: com.sameerasw.essentials.domain.model.ShutUpAppConfig?,
- autoArchivePackage: String? = null,
- forceRestore: Boolean = false
- ) {
- val originalSettings = repository.getShutUpOriginalSettings()
- if (originalSettings.isEmpty()) {
- if (autoArchivePackage != null) {
- startAutoArchiveCountdown(autoArchivePackage)
- }
- return
- }
+ private fun checkShutUp(packageName: String) {
+ val prefs = context.getSharedPreferences("essentials_prefs", Context.MODE_PRIVATE)
+ val serviceEnabled = prefs.getBoolean("shutup_service_enabled", false)
+ if (!serviceEnabled) return
- val mode = repository.getShutUpRestoreMode()
- if (mode == "Notify" && !forceRestore) {
- scope.launch {
- val delaySeconds = repository.getShutUpRestoreDelay()
- delay(delaySeconds * 1000L)
- showRestoreNotification(wasShutUpConfig, autoArchivePackage)
- }
+ val json = prefs.getString("shut_up_selected_apps", null) ?: return
+ val configs: List = try {
+ Gson().fromJson(json, Array::class.java).toList()
+ } catch (_: Exception) {
return
}
- scope.launch {
- if (!forceRestore) {
- // Delay to ensure the app has fully settled before restoring system settings
- val delaySeconds = repository.getShutUpRestoreDelay()
- delay(delaySeconds * 1000L)
- }
-
- val canWriteSecure =
- com.sameerasw.essentials.utils.PermissionUtils.canWriteSecureSettings(context)
- val canWriteSystem = Settings.System.canWrite(context)
-
- originalSettings.forEach { (prefixedKey, value) ->
- try {
- val parts = prefixedKey.split(":", limit = 2)
- if (parts.size < 2) return@forEach
-
- val table = parts[0]
- val key = parts[1]
-
- when (table) {
- "global" -> {
- if (canWriteSecure) {
- Settings.Global.putString(context.contentResolver, key, value)
- }
- }
-
- "secure" -> {
- if (canWriteSecure) {
- Settings.Secure.putString(context.contentResolver, key, value)
- }
- }
-
- "system" -> {
- if (canWriteSystem) {
- Settings.System.putString(context.contentResolver, key, value)
- }
- }
- }
- } catch (e: Exception) {
- Log.e("AppFlowHandler", "Failed to restore setting $prefixedKey", e)
- }
- }
-
- // Clear original settings after restoration
- repository.saveShutUpOriginalSettings(emptyMap())
-
- // Wait a bit and Restart Shizuku as ADB might have been toggled back on
- if (wasShutUpConfig != null && wasShutUpConfig.disableWirelessDebugging && repository.isShutUpAttemptShizukuRestartEnabled()) {
- delay(1000)
- restartShizuku()
- }
+ val config = configs.find { it.packageName == packageName && it.isEnabled } ?: return
- android.widget.Toast.makeText(
- context,
- context.getString(com.sameerasw.essentials.R.string.shut_up_toast_restored),
- android.widget.Toast.LENGTH_SHORT
- ).show()
-
- // Start auto-archive countdown AFTER everything is restored and Shizuku is starting
- if (autoArchivePackage != null) {
- startAutoArchiveCountdown(autoArchivePackage)
- }
- }
- }
-
- private fun restartShizuku() {
- val settingsRepository =
- com.sameerasw.essentials.data.repository.SettingsRepository(context)
- val token = settingsRepository.getShizukuAuthToken()
- if (token.isEmpty()) {
- Log.w("AppFlowHandler", "Shizuku auth token is missing, cannot restart Shizuku")
- return
- }
- try {
- val intent = Intent("moe.shizuku.privileged.api.START").apply {
- `package` = "moe.shizuku.privileged.api"
- putExtra("auth", token)
- addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES)
- }
- context.sendBroadcast(intent)
- } catch (e: Exception) {
- Log.e("AppFlowHandler", "Failed to restart Shizuku", e)
+ scope.launch(Dispatchers.IO) {
+ Log.d("AppFlowHandler", "checkShutUp: Immediately applying ShutUp settings for $packageName via accessibility event")
+ com.sameerasw.essentials.utils.ShutUpManager.applyShutUpSettings(context, config)
}
}
@@ -840,5 +350,9 @@ class AppFlowHandler(
const val EXTRA_PACKAGE_NAME = "package_name"
const val EXTRA_AUTO_ARCHIVE_PACKAGE = "auto_archive_package"
const val NOTIFICATION_ID_SHUTUP_RESTORE = 9999
+
+ // Shared State
+ internal val authenticatedPackages = mutableSetOf()
+ internal val lastLeaveTimes = mutableMapOf()
}
}
diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/cards/FeatureCard.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/cards/FeatureCard.kt
index 159f215d2..dcbd6d79b 100644
--- a/app/src/main/java/com/sameerasw/essentials/ui/components/cards/FeatureCard.kt
+++ b/app/src/main/java/com/sameerasw/essentials/ui/components/cards/FeatureCard.kt
@@ -106,10 +106,12 @@ fun FeatureCard(
HapticUtil.performVirtualKeyHaptic(view)
onClick()
},
- onLongClick = {
- HapticUtil.performVirtualKeyHaptic(view)
- showMenu = true
- },
+ onLongClick = if (onPinToggle != null || onHelpClick != null || additionalMenuItems != null) {
+ {
+ HapticUtil.performVirtualKeyHaptic(view)
+ showMenu = true
+ }
+ } else null,
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
.alpha(alpha)
diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/ShutUpPerAppSettingsSheet.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/ShutUpPerAppSettingsSheet.kt
index a5732872b..d63d47e1c 100644
--- a/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/ShutUpPerAppSettingsSheet.kt
+++ b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/ShutUpPerAppSettingsSheet.kt
@@ -30,6 +30,11 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.sameerasw.essentials.R
import com.sameerasw.essentials.domain.model.ShutUpAppConfig
+import com.sameerasw.essentials.domain.model.disableDevOptions
+import com.sameerasw.essentials.domain.model.disableUsbDebugging
+import com.sameerasw.essentials.domain.model.disableWirelessDebugging
+import com.sameerasw.essentials.domain.model.disableAccessibility
+import com.sameerasw.essentials.domain.model.copy
import com.sameerasw.essentials.ui.components.cards.IconToggleItem
import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer
import com.sameerasw.essentials.viewmodels.MainViewModel
@@ -49,7 +54,7 @@ fun ShutUpPerAppSettingsSheet(
var currentConfig by remember(config) { mutableStateOf(config) }
var showShizukuRestartWarning by remember { mutableStateOf(false) }
- val isAttemptShizukuRestart by viewModel.isShutUpAttemptShizukuRestart
+ val isAttemptShizukuRestart = currentConfig.attemptShizukuRestart
if (showShizukuRestartWarning) {
AlertDialog(
@@ -59,7 +64,7 @@ fun ShutUpPerAppSettingsSheet(
confirmButton = {
TextButton(onClick = {
showShizukuRestartWarning = false
- val newConfig = currentConfig.copy(autoArchive = true)
+ val newConfig = currentConfig.copy(autoArchive = true, attemptShizukuRestart = true)
currentConfig = newConfig
onConfigChanged(newConfig)
}) {
@@ -133,14 +138,9 @@ fun ShutUpPerAppSettingsSheet(
title = stringResource(R.string.shut_up_attempt_shizuku_restart),
isChecked = isAttemptShizukuRestart,
onCheckedChange = {
- viewModel.setShutUpAttemptShizukuRestartEnabled(it)
- if (it && viewModel.shizukuAuthToken.value.isEmpty()) {
- android.widget.Toast.makeText(
- context,
- "Please enter the Shizuku auth token in Essentials settings",
- android.widget.Toast.LENGTH_LONG
- ).show()
- }
+ val newConfig = currentConfig.copy(attemptShizukuRestart = it)
+ currentConfig = newConfig
+ onConfigChanged(newConfig)
}
)
}
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 82683d8bd..3217de696 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
@@ -78,6 +78,7 @@ import com.sameerasw.essentials.FeatureSettingsActivity
import com.sameerasw.essentials.R
import com.sameerasw.essentials.domain.registry.FeatureRegistry
import com.sameerasw.essentials.domain.registry.PermissionRegistry
+import com.sameerasw.essentials.utils.PermissionUtils
import com.sameerasw.essentials.ui.activities.YourAndroidActivity
import com.sameerasw.essentials.ui.components.FavoriteCarousel
import com.sameerasw.essentials.ui.components.cards.FeatureCard
@@ -384,47 +385,7 @@ fun SetupFeatures(
}
}
- R.string.feat_shut_up_title -> {
- if (!isWriteSecureSettingsEnabled) {
- missing.add(
- PermissionItem(
- iconRes = R.drawable.rounded_security_24,
- title = R.string.perm_write_secure_title,
- description = R.string.perm_write_secure_desc_common,
- dependentFeatures = PermissionRegistry.getFeatures("WRITE_SECURE_SETTINGS"),
- actionLabel = R.string.perm_action_grant,
- action = { viewModel.requestWriteSecureSettingsPermission(context) },
- isGranted = isWriteSecureSettingsEnabled
- )
- )
- }
- if (!isWriteSettingsEnabled) {
- missing.add(
- PermissionItem(
- iconRes = R.drawable.rounded_settings_24,
- title = R.string.perm_write_settings_title,
- description = R.string.perm_write_settings_desc,
- dependentFeatures = PermissionRegistry.getFeatures("WRITE_SETTINGS"),
- actionLabel = R.string.perm_action_grant,
- action = { viewModel.requestWriteSettingsPermission(context) },
- isGranted = isWriteSettingsEnabled
- )
- )
- }
- if (!viewModel.isUsageStatsPermissionGranted.value) {
- missing.add(
- PermissionItem(
- iconRes = R.drawable.rounded_app_registration_24,
- title = R.string.perm_usage_stats_title,
- description = R.string.perm_usage_stats_desc,
- dependentFeatures = PermissionRegistry.getFeatures("USAGE_STATS"),
- actionLabel = R.string.perm_action_grant,
- action = { viewModel.requestUsageStatsPermission(context) },
- isGranted = viewModel.isUsageStatsPermissionGranted.value
- )
- )
- }
- }
+
R.string.feat_screen_locked_security_title -> {
if (isRootEnabled) {
@@ -497,6 +458,84 @@ fun SetupFeatures(
}
}
+ R.string.feat_shut_up_title -> {
+ if (!isWriteSecureSettingsEnabled) {
+ missing.add(
+ PermissionItem(
+ iconRes = R.drawable.rounded_security_24,
+ title = R.string.perm_write_secure_title,
+ description = R.string.perm_write_secure_desc_common,
+ dependentFeatures = PermissionRegistry.getFeatures("WRITE_SECURE_SETTINGS"),
+ actionLabel = R.string.perm_action_copy_adb,
+ action = {
+ val adbCommand =
+ "adb shell pm grant com.sameerasw.essentials android.permission.WRITE_SECURE_SETTINGS"
+ val clipboard =
+ context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ val clip = ClipData.newPlainText("adb_command", adbCommand)
+ clipboard.setPrimaryClip(clip)
+ },
+ secondaryActionLabel = R.string.perm_action_check,
+ secondaryAction = {
+ viewModel.isWriteSecureSettingsEnabled.value =
+ PermissionUtils.canWriteSecureSettings(context)
+ },
+ isGranted = isWriteSecureSettingsEnabled
+ )
+ )
+ }
+ if (!isWriteSettingsEnabled) {
+ missing.add(
+ PermissionItem(
+ iconRes = R.drawable.rounded_settings_24,
+ title = R.string.perm_write_settings_title,
+ description = R.string.perm_write_settings_desc,
+ dependentFeatures = PermissionRegistry.getFeatures("WRITE_SETTINGS"),
+ actionLabel = R.string.perm_action_enable,
+ action = {
+ PermissionUtils.openWriteSettings(context)
+ },
+ isGranted = isWriteSettingsEnabled
+ )
+ )
+ }
+ if (!viewModel.isUsageStatsPermissionGranted.value) {
+ missing.add(
+ PermissionItem(
+ iconRes = R.drawable.rounded_data_usage_24,
+ title = R.string.perm_usage_stats_title,
+ description = R.string.perm_usage_stats_desc,
+ dependentFeatures = PermissionRegistry.getFeatures("USAGE_STATS"),
+ actionLabel = R.string.perm_action_grant,
+ action = {
+ com.sameerasw.essentials.utils.PermissionUtils.openUsageStatsSettings(context)
+ },
+ isGranted = viewModel.isUsageStatsPermissionGranted.value
+ )
+ )
+ }
+ if (!viewModel.isPostNotificationsEnabled.value) {
+ missing.add(
+ PermissionItem(
+ iconRes = R.drawable.rounded_notifications_unread_24,
+ title = R.string.permission_post_notifications_title,
+ description = R.string.permission_post_notifications_desc,
+ dependentFeatures = PermissionRegistry.getFeatures("POST_NOTIFICATIONS"),
+ actionLabel = R.string.perm_action_grant,
+ action = {
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
+ (context as? Activity)?.requestPermissions(
+ arrayOf(android.Manifest.permission.POST_NOTIFICATIONS),
+ 1
+ )
+ }
+ },
+ isGranted = viewModel.isPostNotificationsEnabled.value
+ )
+ )
+ }
+ }
+
R.string.feat_call_vibrations_title -> {
if (!viewModel.isReadPhoneStateEnabled.value) {
missing.add(
@@ -773,6 +812,68 @@ fun SetupFeatures(
)
)
+ R.string.feat_shut_up_title -> listOf(
+ PermissionItem(
+ iconRes = R.drawable.rounded_security_24,
+ title = R.string.perm_write_secure_title,
+ description = R.string.perm_write_secure_desc_common,
+ dependentFeatures = PermissionRegistry.getFeatures("WRITE_SECURE_SETTINGS"),
+ actionLabel = R.string.perm_action_copy_adb,
+ action = {
+ val adbCommand =
+ "adb shell pm grant com.sameerasw.essentials android.permission.WRITE_SECURE_SETTINGS"
+ val clipboard =
+ context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ val clip = ClipData.newPlainText("adb_command", adbCommand)
+ clipboard.setPrimaryClip(clip)
+ },
+ secondaryActionLabel = R.string.perm_action_check,
+ secondaryAction = {
+ viewModel.isWriteSecureSettingsEnabled.value =
+ PermissionUtils.canWriteSecureSettings(context)
+ },
+ isGranted = isWriteSecureSettingsEnabled
+ ),
+ PermissionItem(
+ iconRes = R.drawable.rounded_settings_24,
+ title = R.string.perm_write_settings_title,
+ description = R.string.perm_write_settings_desc,
+ dependentFeatures = PermissionRegistry.getFeatures("WRITE_SETTINGS"),
+ actionLabel = R.string.perm_action_enable,
+ action = {
+ PermissionUtils.openWriteSettings(context)
+ },
+ isGranted = isWriteSettingsEnabled
+ ),
+ PermissionItem(
+ iconRes = R.drawable.rounded_data_usage_24,
+ title = R.string.perm_usage_stats_title,
+ description = R.string.perm_usage_stats_desc,
+ dependentFeatures = PermissionRegistry.getFeatures("USAGE_STATS"),
+ actionLabel = R.string.perm_action_grant,
+ action = {
+ com.sameerasw.essentials.utils.PermissionUtils.openUsageStatsSettings(context)
+ },
+ isGranted = viewModel.isUsageStatsPermissionGranted.value
+ ),
+ PermissionItem(
+ iconRes = R.drawable.rounded_notifications_unread_24,
+ title = R.string.permission_post_notifications_title,
+ description = R.string.permission_post_notifications_desc,
+ dependentFeatures = PermissionRegistry.getFeatures("POST_NOTIFICATIONS"),
+ actionLabel = R.string.perm_action_grant,
+ action = {
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
+ (context as? Activity)?.requestPermissions(
+ arrayOf(android.Manifest.permission.POST_NOTIFICATIONS),
+ 1
+ )
+ }
+ },
+ isGranted = viewModel.isPostNotificationsEnabled.value
+ )
+ )
+
R.string.feat_call_vibrations_title -> listOf(
PermissionItem(
iconRes = R.drawable.rounded_mobile_24,
diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/ShutUpSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/ShutUpSettingsUI.kt
index 03e1e8a1b..8842f0ea1 100644
--- a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/ShutUpSettingsUI.kt
+++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/ShutUpSettingsUI.kt
@@ -1,5 +1,12 @@
package com.sameerasw.essentials.ui.composables.configs
+import android.Manifest
+import android.content.Context
+import android.content.Intent
+import android.widget.Toast
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
@@ -9,28 +16,30 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
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.material3.ToggleButton
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
+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.domain.model.AppSelection
-import com.sameerasw.essentials.domain.model.ShutUpAppConfig
+import com.sameerasw.essentials.domain.model.*
import com.sameerasw.essentials.ui.components.cards.FeatureCard
import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer
import com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenuItem
import com.sameerasw.essentials.ui.components.pickers.RestoreModePicker
-import com.sameerasw.essentials.ui.components.sheets.AppSelectionSheet
+import com.sameerasw.essentials.ui.components.sheets.PermissionItem
+import com.sameerasw.essentials.ui.components.sheets.PermissionsBottomSheet
import com.sameerasw.essentials.ui.components.sheets.ShutUpPerAppSettingsSheet
import com.sameerasw.essentials.ui.components.sliders.ConfigSliderItem
+import com.sameerasw.essentials.ui.components.sheets.SingleAppSelectionSheet
import com.sameerasw.essentials.utils.AppUtil
+import com.sameerasw.essentials.utils.HapticUtil
+import com.sameerasw.essentials.utils.PermissionUtils
import com.sameerasw.essentials.viewmodels.MainViewModel
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@@ -38,20 +47,143 @@ import com.sameerasw.essentials.viewmodels.MainViewModel
fun ShutUpSettingsUI(
viewModel: MainViewModel,
modifier: Modifier = Modifier,
- highlightKey: String? = null
+ highlightSetting: String? = null
) {
val context = LocalContext.current
+ val view = LocalView.current
+
+ var showPermissionSheet by remember { mutableStateOf(false) }
var isAppSelectionSheetOpen by remember { mutableStateOf(false) }
- var selectedConfigForEditing by remember { mutableStateOf(null) }
+ var isEditSheetOpen by remember { mutableStateOf(false) }
+ var editingPackageName by remember { mutableStateOf("") }
+ var editingConfig by remember { mutableStateOf(null) }
+
+ // Permission states checked on composition and changes
+ var hasSecureSettings by remember { mutableStateOf(PermissionUtils.canWriteSecureSettings(context)) }
+ var hasWriteSettings by remember { mutableStateOf(PermissionUtils.canWriteSystemSettings(context)) }
+ var hasUsageStats by remember { mutableStateOf(PermissionUtils.hasUsageStatsPermission(context)) }
+ var hasNotifications by remember { mutableStateOf(PermissionUtils.isPostNotificationsEnabled(context)) }
+
+ val hasAllPermissions = hasSecureSettings && hasWriteSettings && hasUsageStats && hasNotifications
- val configs by viewModel.shutUpConfigs
+ // System Permission Launcher for Post Notifications
+ val requestNotificationLauncher = rememberLauncherForActivityResult(
+ ActivityResultContracts.RequestPermission()
+ ) { isGranted ->
+ hasNotifications = isGranted
+ }
+
+ LaunchedEffect(Unit) {
+ hasSecureSettings = PermissionUtils.canWriteSecureSettings(context)
+ hasWriteSettings = PermissionUtils.canWriteSystemSettings(context)
+ hasUsageStats = PermissionUtils.hasUsageStatsPermission(context)
+ hasNotifications = PermissionUtils.isPostNotificationsEnabled(context)
+ }
+
+ if (showPermissionSheet) {
+ PermissionsBottomSheet(
+ onDismissRequest = { showPermissionSheet = false },
+ featureTitle = R.string.feat_shut_up_title,
+ permissions = listOf(
+ PermissionItem(
+ iconRes = R.drawable.rounded_admin_panel_settings_24,
+ title = "Write Secure Settings",
+ description = "Required to toggle USB and Wireless Debugging settings",
+ isGranted = hasSecureSettings,
+ action = {
+ // Secure settings cannot be requested directly by activity intent.
+ // We show a toast instructions.
+ Toast.makeText(context, "Requires ADB or Root to grant WRITE_SECURE_SETTINGS", Toast.LENGTH_LONG).show()
+ }
+ ),
+ PermissionItem(
+ iconRes = R.drawable.rounded_settings_24,
+ title = "Write System Settings",
+ description = "Required to toggle standard settings",
+ isGranted = hasWriteSettings,
+ action = {
+ PermissionUtils.openWriteSettings(context)
+ }
+ ),
+ PermissionItem(
+ iconRes = R.drawable.rounded_data_usage_24,
+ title = "Usage Access",
+ description = "Required to detect when target apps are launched or exited",
+ isGranted = hasUsageStats,
+ action = {
+ com.sameerasw.essentials.utils.PermissionUtils.openUsageStatsSettings(context)
+ }
+ ),
+ PermissionItem(
+ iconRes = R.drawable.rounded_notifications_unread_24,
+ title = "Post Notifications",
+ description = "Required to display background service status and auto-freeze countdowns",
+ isGranted = hasNotifications,
+ action = {
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
+ requestNotificationLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
+ }
+ }
+ )
+ )
+ )
+ }
Column(
modifier = modifier
.fillMaxWidth()
- .padding(16.dp),
- verticalArrangement = Arrangement.spacedBy(4.dp)
+ .padding(horizontal = 16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
) {
+ Text(
+ text = "Monitoring Service",
+ style = MaterialTheme.typography.titleMedium,
+ modifier = Modifier.padding(start = 16.dp),
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+
+ val onToggleShutUpService: (Boolean) -> Unit = { enabled ->
+ HapticUtil.performVirtualKeyHaptic(view)
+ if (enabled) {
+ // Recheck permissions
+ hasSecureSettings = PermissionUtils.canWriteSecureSettings(context)
+ hasWriteSettings = PermissionUtils.canWriteSystemSettings(context)
+ hasUsageStats = PermissionUtils.hasUsageStatsPermission(context)
+ hasNotifications = PermissionUtils.isPostNotificationsEnabled(context)
+
+ if (hasSecureSettings && hasWriteSettings && hasUsageStats && hasNotifications) {
+ viewModel.setShutUpServiceEnabled(true, context)
+ } else {
+ showPermissionSheet = true
+ }
+ } else {
+ viewModel.setShutUpServiceEnabled(false, context)
+ }
+ }
+
+ RoundedCardContainer(
+ modifier = Modifier,
+ spacing = 2.dp,
+ cornerRadius = 24.dp
+ ) {
+ FeatureCard(
+ title = "Enable Shut-Up! Service",
+ description = "Runs in the background and applies security rules on target app launch",
+ iconRes = R.drawable.rounded_security_24,
+ isEnabled = viewModel.isShutUpServiceEnabled.value,
+ showToggle = true,
+ hasMoreSettings = false,
+ onToggle = onToggleShutUpService,
+ onClick = { onToggleShutUpService(!viewModel.isShutUpServiceEnabled.value) }
+ )
+ }
+
+ Text(
+ text = "App Configurations",
+ style = MaterialTheme.typography.titleMedium,
+ modifier = Modifier.padding(start = 16.dp),
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
RoundedCardContainer(
modifier = Modifier,
@@ -80,113 +212,141 @@ fun ShutUpSettingsUI(
iconRes = R.drawable.rounded_app_registration_24,
isEnabled = true,
showToggle = false,
- hasMoreSettings = true,
+ hasMoreSettings = false,
onToggle = {},
- onClick = { isAppSelectionSheetOpen = true }
+ onClick = {
+ HapticUtil.performVirtualKeyHaptic(view)
+ isAppSelectionSheetOpen = true
+ }
)
}
-
- RoundedCardContainer(
- modifier = Modifier,
- spacing = 2.dp,
- cornerRadius = 24.dp
- ) {
- configs.forEach { config ->
- val appName = remember(config.packageName) {
- try {
- val appInfo =
- context.packageManager.getApplicationInfo(config.packageName, 0)
- context.packageManager.getApplicationLabel(appInfo).toString()
- } catch (e: Exception) {
- config.packageName
- }
- }
-
- val appIconPainter = remember(config.packageName) {
- try {
- val drawable = context.packageManager.getApplicationIcon(config.packageName)
- androidx.compose.ui.graphics.painter.BitmapPainter(
- AppUtil.drawableToBitmap(drawable).asImageBitmap()
- )
- } catch (e: Exception) {
- null
- }
- }
-
- FeatureCard(
- title = appName,
- description = config.packageName,
- isEnabled = true,
- onToggle = {},
- onClick = { selectedConfigForEditing = config },
- iconPainter = appIconPainter,
- showToggle = false,
- hasMoreSettings = true,
- customTrailingContent = {
- IconButton(
- onClick = {
- viewModel.createShutUpShortcut(context, config)
- }
- ) {
- Icon(
- painter = painterResource(id = R.drawable.rounded_add_24),
- contentDescription = stringResource(R.string.action_create_shortcut),
- tint = MaterialTheme.colorScheme.primary
- )
+ val configs by viewModel.shutUpConfigs
+ if (configs.isNotEmpty()) {
+ RoundedCardContainer(
+ modifier = Modifier,
+ spacing = 2.dp,
+ cornerRadius = 24.dp
+ ) {
+ configs.forEach { config ->
+ ShutUpAppItem(
+ config = config,
+ viewModel = viewModel,
+ onEditClick = { packageName, cfg ->
+ editingPackageName = packageName
+ editingConfig = cfg
+ isEditSheetOpen = true
}
- },
- additionalMenuItems = { onDismiss ->
- SegmentedDropdownMenuItem(
- text = { Text(stringResource(R.string.action_remove)) },
- onClick = {
- onDismiss()
- viewModel.removeShutUpConfig(config.packageName)
- },
- leadingIcon = {
- Icon(
- painter = painterResource(id = R.drawable.rounded_delete_24),
- contentDescription = null
- )
- }
- )
- }
- )
+ )
+ }
}
}
- Text(
- text = stringResource(R.string.shut_up_description),
- style = MaterialTheme.typography.bodyMedium,
- modifier = Modifier.padding(16.dp),
- color = MaterialTheme.colorScheme.onSurfaceVariant
- )
-
if (isAppSelectionSheetOpen) {
- AppSelectionSheet(
+ SingleAppSelectionSheet(
onDismissRequest = { isAppSelectionSheetOpen = false },
- onLoadApps = { ctx ->
- viewModel.shutUpConfigs.value.map { AppSelection(it.packageName, true) }
- },
- onSaveApps = { ctx, apps -> viewModel.saveShutUpSelectedApps(ctx, apps) }
+ onAppSelected = { app ->
+ isAppSelectionSheetOpen = false
+ editingPackageName = app.packageName
+ editingConfig = configs.find { it.packageName == app.packageName }
+ isEditSheetOpen = true
+ }
)
}
- if (selectedConfigForEditing != null) {
- val frozenApps = remember { viewModel.loadFreezeSelectedApps(context) }
- val isFrozen = remember(selectedConfigForEditing) {
- frozenApps.any { it.packageName == selectedConfigForEditing?.packageName }
+ if (isEditSheetOpen) {
+ val isFrozen = remember(editingPackageName) {
+ com.sameerasw.essentials.utils.FreezeManager.isAppFrozen(context, editingPackageName)
}
-
ShutUpPerAppSettingsSheet(
- onDismissRequest = { selectedConfigForEditing = null },
- config = configs.find { it.packageName == selectedConfigForEditing?.packageName }
- ?: selectedConfigForEditing!!,
- onConfigChanged = { viewModel.updateShutUpConfig(it) },
- onCreateShortcut = { viewModel.createShutUpShortcut(context, it) },
+ onDismissRequest = { isEditSheetOpen = false },
+ config = editingConfig ?: ShutUpAppConfig(packageName = editingPackageName),
+ onConfigChanged = { updatedConfig ->
+ viewModel.updateShutUpConfig(updatedConfig)
+ editingConfig = updatedConfig
+ },
+ onCreateShortcut = { config ->
+ viewModel.createShutUpShortcut(context, config)
+ },
isFrozen = isFrozen,
viewModel = viewModel
)
}
}
}
+
+@Composable
+private fun ShutUpAppItem(
+ config: ShutUpAppConfig,
+ viewModel: MainViewModel,
+ onEditClick: (String, ShutUpAppConfig) -> Unit
+) {
+ val context = LocalContext.current
+ val appName = remember(config.packageName) {
+ try {
+ val appInfo = context.packageManager.getApplicationInfo(config.packageName, 0)
+ context.packageManager.getApplicationLabel(appInfo).toString()
+ } catch (e: Exception) {
+ config.packageName
+ }
+ }
+
+ val appIconPainter = remember(config.packageName) {
+ try {
+ val drawable = context.packageManager.getApplicationIcon(config.packageName)
+ androidx.compose.ui.graphics.painter.BitmapPainter(
+ AppUtil.drawableToBitmap(drawable).asImageBitmap()
+ )
+ } catch (e: Exception) {
+ null
+ }
+ }
+
+ val enabledCount = config.settings.count { it.enabled }
+ val descText = "${enabledCount} settings configured" +
+ (if (config.autoArchive) " • Auto-Freeze" else "") +
+ (if (config.attemptShizukuRestart) " • Shizuku restart" else "")
+
+ FeatureCard(
+ title = appName,
+ description = descText,
+ isEnabled = config.isEnabled,
+ showToggle = true,
+ onToggle = { isChecked ->
+ viewModel.updateShutUpConfig(config.copy(isEnabled = isChecked))
+ },
+ onClick = {
+ onEditClick(config.packageName, config)
+ },
+ iconPainter = appIconPainter,
+ hasMoreSettings = true,
+ additionalMenuItems = { onDismiss ->
+ SegmentedDropdownMenuItem(
+ text = { Text("Create Shortcut") },
+ onClick = {
+ onDismiss()
+ viewModel.createShutUpShortcut(context, config)
+ },
+ leadingIcon = {
+ Icon(
+ painter = painterResource(id = R.drawable.rounded_link_24),
+ contentDescription = null
+ )
+ }
+ )
+ SegmentedDropdownMenuItem(
+ text = { Text(stringResource(R.string.action_remove)) },
+ onClick = {
+ onDismiss()
+ viewModel.removeShutUpConfig(config.packageName)
+ },
+ leadingIcon = {
+ Icon(
+ painter = painterResource(id = R.drawable.rounded_delete_24),
+ contentDescription = null
+ )
+ }
+ )
+ }
+ )
+}
diff --git a/app/src/main/java/com/sameerasw/essentials/utils/ServiceUtils.kt b/app/src/main/java/com/sameerasw/essentials/utils/ServiceUtils.kt
index dc7a83829..34f8d8a38 100644
--- a/app/src/main/java/com/sameerasw/essentials/utils/ServiceUtils.kt
+++ b/app/src/main/java/com/sameerasw/essentials/utils/ServiceUtils.kt
@@ -16,6 +16,7 @@ object ServiceUtils {
startAppDetectionServiceIfNeeded(context, settingsRepository)
startBatteryNotificationServiceIfNeeded(context, settingsRepository)
+ startShutUpServiceIfNeeded(context, settingsRepository)
}
private fun startAppDetectionServiceIfNeeded(
@@ -39,7 +40,7 @@ object ServiceUtils {
val hasShutUpApps = shutUpConfigs.any { it.isEnabled }
val shouldRun =
- (isUseUsageAccess && (isAppLockEnabled || isDynamicNightLightEnabled || isHideGestureBarOnLauncherEnabled || hasAppAutomations)) || hasShutUpApps
+ isUseUsageAccess && (isAppLockEnabled || isDynamicNightLightEnabled || isHideGestureBarOnLauncherEnabled || hasAppAutomations)
val intent = Intent(context, AppDetectionService::class.java)
if (shouldRun) {
@@ -76,4 +77,23 @@ object ServiceUtils {
}
}
}
+
+ private fun startShutUpServiceIfNeeded(
+ context: Context,
+ settingsRepository: SettingsRepository
+ ) {
+ val isShutUpEnabled = settingsRepository.isShutUpServiceEnabled()
+ val intent = Intent(context, com.sameerasw.essentials.services.ShutUpForegroundService::class.java)
+ if (isShutUpEnabled) {
+ try {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ context.startForegroundService(intent)
+ } else {
+ context.startService(intent)
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+ }
}
diff --git a/app/src/main/java/com/sameerasw/essentials/utils/ShutUpManager.kt b/app/src/main/java/com/sameerasw/essentials/utils/ShutUpManager.kt
new file mode 100644
index 000000000..57f58237a
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/utils/ShutUpManager.kt
@@ -0,0 +1,316 @@
+package com.sameerasw.essentials.utils
+
+import android.content.Context
+import android.content.Intent
+import android.provider.Settings
+import android.util.Log
+import android.widget.Toast
+import com.sameerasw.essentials.R
+import com.sameerasw.essentials.data.repository.SettingsRepository
+import com.sameerasw.essentials.domain.model.ShutUpAppConfig
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.util.concurrent.CopyOnWriteArrayList
+
+object ShutUpManager {
+ private const val TAG = "ShutUpManager"
+
+ // Shared scope for async shell commands. Kept separate so we can cancel pending jobs.
+ private val shellScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
+ private val pendingShellJobs = CopyOnWriteArrayList()
+
+ private fun cancelPendingShellJobs() {
+ pendingShellJobs.forEach { it.cancel() }
+ pendingShellJobs.clear()
+ Log.d(TAG, "Cancelled all pending shell jobs")
+ }
+
+ private val ignoredSystemPackages = listOf(
+ "android",
+ "com.android.systemui",
+ "com.google.android.inputmethod.latin",
+ "com.google.android.gms"
+ )
+
+ fun isPackageIgnored(packageName: String): Boolean {
+ return ignoredSystemPackages.contains(packageName) ||
+ packageName.startsWith("com.android.inputmethod") ||
+ packageName.startsWith("com.google.android.inputmethod") ||
+ packageName.contains("autofill")
+ }
+
+ fun isAppRunning(context: Context, packageName: String): Boolean {
+ if (ShellUtils.isAvailable(context) && ShellUtils.hasPermission(context)) {
+ try {
+ val output = ShellUtils.runCommandWithOutput(context, "pidof $packageName")
+ if (!output.isNullOrBlank()) {
+ return true
+ }
+ } catch (e: Exception) {
+ Log.w(TAG, "pidof check failed for $packageName", e)
+ }
+ try {
+ val output = ShellUtils.runCommandWithOutput(context, "pgrep -f $packageName")
+ if (!output.isNullOrBlank()) {
+ return true
+ }
+ } catch (e: Exception) {
+ Log.w(TAG, "pgrep check failed for $packageName", e)
+ }
+ }
+
+ try {
+ val am = context.getSystemService(Context.ACTIVITY_SERVICE) as android.app.ActivityManager
+ val processes = am.runningAppProcesses
+ if (processes != null) {
+ for (process in processes) {
+ if (process.processName == packageName) {
+ return true
+ }
+ }
+ }
+ } catch (e: Exception) {
+ Log.w(TAG, "ActivityManager check failed for $packageName", e)
+ }
+
+ return false
+ }
+
+ fun safeWriteSetting(context: Context, type: String, key: String, value: String): Boolean {
+ val resolver = context.contentResolver
+ val resolverSuccess = try {
+ when (type.uppercase()) {
+ "GLOBAL" -> Settings.Global.putString(resolver, key, value)
+ "SECURE" -> Settings.Secure.putString(resolver, key, value)
+ "SYSTEM" -> Settings.System.putString(resolver, key, value)
+ else -> false
+ }
+ Log.d(TAG, "Successfully wrote setting via ContentResolver: [$type] $key = $value")
+ true
+ } catch (e: SecurityException) {
+ Log.e(TAG, "SecurityException writing setting via ContentResolver: [$type] $key = $value", e)
+ false
+ } catch (e: Exception) {
+ Log.e(TAG, "Error writing setting via ContentResolver: [$type] $key = $value", e)
+ false
+ }
+
+ // Run shell command asynchronously in background to shut down/enable active ports/daemons without blocking
+ if (ShellUtils.isAvailable(context) && ShellUtils.hasPermission(context)) {
+ val job = shellScope.launch {
+ try {
+ val shellType = type.lowercase()
+ ShellUtils.runCommand(context, "settings put $shellType $key $value")
+ Log.d(TAG, "Successfully executed setting put via Shell (async): [$type] $key = $value")
+ } catch (e: Exception) {
+ Log.w(TAG, "Failed to write setting via Shell (async): [$type] $key = $value", e)
+ }
+ }
+ pendingShellJobs.add(job)
+ // Prune completed jobs to avoid list growing forever
+ pendingShellJobs.removeAll { it.isCompleted || it.isCancelled }
+ }
+
+ return resolverSuccess
+ }
+
+ fun safeReadSetting(context: Context, type: String, key: String): String? {
+ val resolver = context.contentResolver
+ return try {
+ when (type.uppercase()) {
+ "GLOBAL" -> Settings.Global.getString(resolver, key)
+ "SECURE" -> Settings.Secure.getString(resolver, key)
+ "SYSTEM" -> Settings.System.getString(resolver, key)
+ else -> null
+ }
+ } catch (e: Exception) {
+ null
+ }
+ }
+
+ suspend fun applyShutUpSettings(context: Context, config: ShutUpAppConfig) {
+ Log.d(TAG, "Applying ShutUp settings for ${config.packageName}")
+ // Cancel any stale revert shell commands before applying to prevent race conditions
+ cancelPendingShellJobs()
+ withContext(Dispatchers.IO) {
+ val repository = SettingsRepository(context)
+ val currentBackup = repository.getShutUpOriginalSettings()
+ val originalSettings = currentBackup.toMutableMap()
+
+ config.settings.forEach { setting ->
+ if (setting.enabled) {
+ val resolvedType = if (setting.key == "accessibility_enabled") "SECURE" else setting.settingType
+ val prefixedKey = "${resolvedType.lowercase()}:${setting.key}"
+ if (!originalSettings.containsKey(prefixedKey)) {
+ val currentVal = safeReadSetting(context, resolvedType, setting.key)
+ if (currentVal != null) {
+ originalSettings[prefixedKey] = currentVal
+ }
+ }
+ safeWriteSetting(context, resolvedType, setting.key, setting.valueOnLaunch)
+ }
+ }
+
+ // Special handling for accessibility services
+ val disableAccessibility = config.settings.any { it.key == "accessibility_enabled" && it.enabled }
+ if (disableAccessibility) {
+ val prefixedAccKey = "secure:${Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES}"
+ if (!originalSettings.containsKey(prefixedAccKey)) {
+ val currentAccServices = safeReadSetting(context, "SECURE", Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES)
+ if (!currentAccServices.isNullOrEmpty()) {
+ originalSettings[prefixedAccKey] = currentAccServices
+ safeWriteSetting(context, "SECURE", Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, "")
+ }
+ }
+ }
+
+ if (originalSettings.isNotEmpty() && originalSettings != currentBackup) {
+ repository.saveShutUpOriginalSettings(originalSettings)
+ }
+ }
+ }
+
+ suspend fun revertShutUpSettings(context: Context, config: ShutUpAppConfig) {
+ Log.d(TAG, "Reverting ShutUp settings for ${config.packageName}")
+ // Cancel any stale apply shell commands before reverting to prevent race conditions
+ cancelPendingShellJobs()
+ withContext(Dispatchers.IO) {
+ config.settings.forEach { setting ->
+ if (setting.enabled) {
+ val resolvedType = if (setting.key == "accessibility_enabled") "SECURE" else setting.settingType
+ safeWriteSetting(context, resolvedType, setting.key, setting.valueOnRevert)
+ }
+ }
+ }
+ }
+
+ suspend fun restoreOriginalSettings(context: Context, repository: SettingsRepository) {
+ val originalSettings = repository.getShutUpOriginalSettings()
+ if (originalSettings.isEmpty()) {
+ Log.d(TAG, "No original settings to restore (backup empty)")
+ return
+ }
+
+ Log.d(TAG, "Restoring original settings from backup (${originalSettings.size} entries)")
+ withContext(Dispatchers.IO) {
+ var restoredAccessibilityServices = false
+ originalSettings.forEach { (prefixedKey, value) ->
+ try {
+ val parts = prefixedKey.split(":", limit = 2)
+ if (parts.size < 2) return@forEach
+ val table = parts[0]
+ val key = parts[1]
+ when (table) {
+ "global" -> Settings.Global.putString(context.contentResolver, key, value)
+ "secure" -> {
+ Settings.Secure.putString(context.contentResolver, key, value)
+ if (key == Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES) {
+ restoredAccessibilityServices = true
+ }
+ }
+ "system" -> Settings.System.putString(context.contentResolver, key, value)
+ }
+ Log.d(TAG, "Restored $prefixedKey = $value")
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to restore setting $prefixedKey", e)
+ }
+ }
+
+ // Re-enable accessibility master switch when the services list was restored.
+ // Clearing ENABLED_ACCESSIBILITY_SERVICES also disables the master switch,
+ // so we must explicitly turn it back on.
+ if (restoredAccessibilityServices) {
+ try {
+ Settings.Secure.putString(
+ context.contentResolver,
+ Settings.Secure.ACCESSIBILITY_ENABLED,
+ "1"
+ )
+ Log.d(TAG, "Re-enabled ACCESSIBILITY_ENABLED master switch")
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to re-enable ACCESSIBILITY_ENABLED", e)
+ }
+ }
+
+ // Clear backup so AppFlowHandler doesn't double-restore
+ repository.saveShutUpOriginalSettings(emptyMap())
+ }
+
+ withContext(Dispatchers.Main) {
+ Toast.makeText(
+ context,
+ context.getString(R.string.shut_up_toast_restored),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+
+ suspend fun restartShizuku(context: Context) {
+ Log.d(TAG, "Waiting 800ms for developer/ADB services to stabilize before restarting Shizuku")
+ delay(800)
+ Log.d(TAG, "Attempting Shizuku restart now")
+ // Try explicit ManualStartReceiver broadcast
+ try {
+ val intent = Intent("moe.shizuku.privileged.api.START").apply {
+ setClassName("moe.shizuku.privileged.api", "moe.shizuku.manager.receiver.ManualStartReceiver")
+ putExtra("auth", "y95fuaRb9USHiIg724tvTHIs")
+ addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES)
+ }
+ context.sendBroadcast(intent)
+ Log.d(TAG, "Sent explicit ManualStartReceiver broadcast")
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed explicit ManualStartReceiver broadcast", e)
+ }
+
+ // Try explicit BootReceiver broadcast
+ try {
+ val intent = Intent("moe.shizuku.privileged.api.START").apply {
+ setClassName("moe.shizuku.privileged.api", "moe.shizuku.manager.receiver.BootReceiver")
+ putExtra("auth", "y95fuaRb9USHiIg724tvTHIs")
+ addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES)
+ }
+ context.sendBroadcast(intent)
+ Log.d(TAG, "Sent explicit BootReceiver broadcast")
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed explicit BootReceiver broadcast", e)
+ }
+
+ // Try legacy/implicit broadcast
+ try {
+ val intent = Intent("moe.shizuku.privileged.api.START").apply {
+ setPackage("moe.shizuku.privileged.api")
+ putExtra("auth", "y95fuaRb9USHiIg724tvTHIs")
+ addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES)
+ }
+ context.sendBroadcast(intent)
+ Log.d(TAG, "Sent legacy implicit broadcast")
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed legacy implicit broadcast", e)
+ }
+
+ // If root is enabled, run the start script directly via root shell
+ if (ShellUtils.isRootEnabled(context)) {
+ Log.d(TAG, "Root is enabled, running Shizuku start script via root")
+ withContext(Dispatchers.IO) {
+ val scripts = listOf(
+ "sh /data/data/moe.shizuku.privileged.api/start.sh",
+ "sh /sdcard/Android/data/moe.shizuku.privileged.api/files/start.sh",
+ "sh /storage/emulated/0/Android/data/moe.shizuku.privileged.api/files/start.sh"
+ )
+ scripts.forEach { script ->
+ try {
+ val success = RootUtils.runCommand(script)
+ Log.d(TAG, "Executed root command: '$script', success: $success")
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed root command: '$script'", e)
+ }
+ }
+ }
+ }
+ }
+}
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 f47245904..affc05625 100644
--- a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt
+++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt
@@ -38,6 +38,7 @@ import com.sameerasw.essentials.data.repository.UpdateRepository
import com.sameerasw.essentials.domain.HapticFeedbackType
import com.sameerasw.essentials.domain.MapsState
import com.sameerasw.essentials.domain.model.AppSelection
+import com.sameerasw.essentials.domain.model.ShutUpAppConfig
import com.sameerasw.essentials.domain.model.DnsPreset
import com.sameerasw.essentials.domain.model.NotificationApp
import com.sameerasw.essentials.domain.model.NotificationLightingColorMode
@@ -126,6 +127,7 @@ class MainViewModel : ViewModel() {
val isBluetoothPermissionGranted = mutableStateOf(false)
val isUsageStatsPermissionGranted = mutableStateOf(false)
val appLanguage = mutableStateOf("en")
+ val isShutUpServiceEnabled = mutableStateOf(false)
val isBluetoothDevicesEnabled = mutableStateOf(false)
val isCallVibrationsEnabled = mutableStateOf(false)
@@ -185,7 +187,6 @@ class MainViewModel : ViewModel() {
val shutUpConfigs =
mutableStateOf>(emptyList())
val isShutUpLoading = mutableStateOf(false)
- val isShutUpAttemptShizukuRestart = mutableStateOf(true)
val shutUpRestoreDelay = mutableIntStateOf(10)
val shutUpRestoreMode = mutableStateOf("Auto")
val shizukuAuthToken = mutableStateOf("")
@@ -692,10 +693,7 @@ class MainViewModel : ViewModel() {
liveWallpaperCustomVideos.addAll(settingsRepository.getLiveWallpaperCustomVideos())
}
- SettingsRepository.KEY_SHUT_UP_ATTEMPT_SHIZUKU_RESTART -> {
- isShutUpAttemptShizukuRestart.value =
- settingsRepository.isShutUpAttemptShizukuRestartEnabled()
- }
+
SettingsRepository.KEY_SHUT_UP_RESTORE_DELAY -> {
shutUpRestoreDelay.intValue =
@@ -753,12 +751,22 @@ class MainViewModel : ViewModel() {
AppCompatDelegate.setApplicationLocales(appLocale)
}
+
+
+
fun loadShutUpConfigs() {
shutUpConfigs.value = settingsRepository.loadShutUpConfigs()
}
- fun updateShutUpConfig(config: com.sameerasw.essentials.domain.model.ShutUpAppConfig) {
- settingsRepository.updateShutUpConfig(config)
+ fun updateShutUpConfig(config: ShutUpAppConfig) {
+ val current = shutUpConfigs.value.toMutableList()
+ val index = current.indexOfFirst { it.packageName == config.packageName }
+ if (index != -1) {
+ current[index] = config
+ } else {
+ current.add(config)
+ }
+ settingsRepository.saveShutUpConfigs(current)
loadShutUpConfigs()
}
@@ -769,10 +777,6 @@ class MainViewModel : ViewModel() {
loadShutUpConfigs()
}
- fun setShutUpAttemptShizukuRestartEnabled(enabled: Boolean) {
- isShutUpAttemptShizukuRestart.value = enabled
- settingsRepository.setShutUpAttemptShizukuRestartEnabled(enabled)
- }
fun setShutUpRestoreDelay(delaySeconds: Int) {
shutUpRestoreDelay.intValue = delaySeconds
@@ -789,58 +793,62 @@ class MainViewModel : ViewModel() {
settingsRepository.setShizukuAuthToken(token)
}
- fun saveShutUpSelectedApps(context: Context, apps: List) {
- val currentConfigs = settingsRepository.loadShutUpConfigs().associateBy { it.packageName }
- val newConfigs = apps.filter { it.isEnabled }.map {
- currentConfigs[it.packageName] ?: com.sameerasw.essentials.domain.model.ShutUpAppConfig(
- it.packageName
- )
+ fun setShutUpServiceEnabled(enabled: Boolean, context: Context) {
+ isShutUpServiceEnabled.value = enabled
+ settingsRepository.setShutUpServiceEnabled(enabled)
+ val intent = Intent(context, com.sameerasw.essentials.services.ShutUpForegroundService::class.java)
+ if (enabled) {
+ ContextCompat.startForegroundService(context, intent)
+ } else {
+ context.stopService(intent)
}
- settingsRepository.saveShutUpConfigs(newConfigs)
- loadShutUpConfigs()
}
- fun createShutUpShortcut(
- context: Context,
- config: com.sameerasw.essentials.domain.model.ShutUpAppConfig
- ) {
- val appName = try {
- val appInfo = context.packageManager.getApplicationInfo(config.packageName, 0)
- context.packageManager.getApplicationLabel(appInfo).toString()
+ fun createShutUpShortcut(context: Context, config: ShutUpAppConfig) {
+ if (!androidx.core.content.pm.ShortcutManagerCompat.isRequestPinShortcutSupported(context)) {
+ Toast.makeText(context, "Shortcut pinning not supported by launcher", Toast.LENGTH_SHORT).show()
+ return
+ }
+
+ val pm = context.packageManager
+ val appLabel = try {
+ val appInfo = pm.getApplicationInfo(config.packageName, 0)
+ pm.getApplicationLabel(appInfo).toString()
} catch (e: Exception) {
config.packageName
}
+ val shortLabel = "Shut-Up $appLabel"
+ val longLabel = "Launch $appLabel with Shut-Up"
- val intent =
- Intent(context, com.sameerasw.essentials.ShutUpShortcutActivity::class.java).apply {
- action = Intent.ACTION_MAIN
- putExtra("package_name", config.packageName)
- data = Uri.parse("shutup://${config.packageName}")
- }
+ val iconCompat = try {
+ val bitmap = com.sameerasw.essentials.utils.AppUtil.getShortcutIcon(context, config.packageName)
+ androidx.core.graphics.drawable.IconCompat.createWithBitmap(bitmap)
+ } catch (e: Exception) {
+ null
+ }
- if (androidx.core.content.pm.ShortcutManagerCompat.isRequestPinShortcutSupported(context)) {
- val appIcon = AppUtil.getShortcutIcon(context, config.packageName)
+ val shortcutIntent = Intent(context, com.sameerasw.essentials.ShutUpShortcutActivity::class.java).apply {
+ action = Intent.ACTION_VIEW
+ putExtra("package_name", config.packageName)
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ }
- val pinShortcutInfo =
- androidx.core.content.pm.ShortcutInfoCompat.Builder(context, config.packageName)
- .setShortLabel(appName)
- .setIcon(androidx.core.graphics.drawable.IconCompat.createWithBitmap(appIcon))
- .setIntent(intent)
- .build()
+ val shortcutInfo = androidx.core.content.pm.ShortcutInfoCompat.Builder(context, "shutup_${config.packageName}")
+ .setShortLabel(shortLabel)
+ .setLongLabel(longLabel)
+ .setIntent(shortcutIntent)
+ .apply {
+ if (iconCompat != null) {
+ setIcon(iconCompat)
+ }
+ }
+ .build()
- androidx.core.content.pm.ShortcutManagerCompat.requestPinShortcut(
- context,
- pinShortcutInfo,
- null
- )
- Toast.makeText(
- context,
- context.getString(R.string.shut_up_shortcut_created, appName),
- Toast.LENGTH_SHORT
- ).show()
- }
+ androidx.core.content.pm.ShortcutManagerCompat.requestPinShortcut(context, shortcutInfo, null)
}
+
+
fun check(context: Context) {
appContext = context.applicationContext
settingsRepository = SettingsRepository(context)
@@ -887,8 +895,7 @@ class MainViewModel : ViewModel() {
notificationLightingSystemMode.intValue =
settingsRepository.getNotificationLightingSystemMode()
- isShutUpAttemptShizukuRestart.value =
- settingsRepository.isShutUpAttemptShizukuRestartEnabled()
+
shutUpRestoreDelay.intValue =
settingsRepository.getShutUpRestoreDelay()
shutUpRestoreMode.value =
@@ -936,6 +943,7 @@ class MainViewModel : ViewModel() {
lockScreenClockSelectedColorId.value =
settingsRepository.getLockScreenClockSelectedColorId()
lockScreenClockSeedColor.intValue = settingsRepository.getLockScreenClockSeedColor()
+ isShutUpServiceEnabled.value = settingsRepository.isShutUpServiceEnabled()
loadShutUpConfigs()
recentSearches.value = settingsRepository.getRecentSearches()
loadCachedWallpaper()
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 67f6aa73a..5e798220d 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1579,6 +1579,11 @@
%1$s will be archived in %2$d seconds
Freeze now
Abort
+ Shut-Up! Service
+ Monitors launched apps to disable developer settings
+ Shut-Up! is active
+ Monitoring app launch and exit
+
Lock screen clock
Customize lock screen clock on Pixels