diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 1c45524ce..040cd2a27 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -247,14 +247,6 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/com/sameerasw/essentials/FeatureSettingsActivity.kt b/app/src/main/java/com/sameerasw/essentials/FeatureSettingsActivity.kt
index 2dbd2aea8..fb6451133 100644
--- a/app/src/main/java/com/sameerasw/essentials/FeatureSettingsActivity.kt
+++ b/app/src/main/java/com/sameerasw/essentials/FeatureSettingsActivity.kt
@@ -73,10 +73,10 @@ import com.sameerasw.essentials.ui.composables.configs.NotificationLightingSetti
import com.sameerasw.essentials.ui.composables.configs.OtherCustomizationsSettingsUI
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
@@ -215,6 +215,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
// FAB State for Notification Lighting
var fabExpanded by remember { mutableStateOf(true) }
@@ -246,7 +249,10 @@ class FeatureSettingsActivity : AppCompatActivity() {
isNotificationLightingAccessibilityEnabled,
isNotificationListenerEnabled,
isReadPhoneStateEnabled,
- isShizukuPermissionGranted
+ isShizukuPermissionGranted,
+ isWriteSettingsEnabled,
+ isUsageStatsPermissionGranted,
+ isPostNotificationsEnabled
) {
val hasMissingPermissions = when (featureId) {
"Screen off widget" -> !isAccessibilityEnabled
@@ -264,6 +270,7 @@ class FeatureSettingsActivity : AppCompatActivity() {
"Location reached" -> !viewModel.isLocationPermissionGranted.value || !viewModel.isBackgroundLocationPermissionGranted.value
"Quick settings tiles" -> !viewModel.isWriteSettingsEnabled.value
"Screen refresh rate" -> !viewModel.isShizukuPermissionGranted.value
+ "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
@@ -280,7 +287,6 @@ class FeatureSettingsActivity : AppCompatActivity() {
context
)
- "Shut-Up!" -> !isWriteSecureSettingsEnabled || !viewModel.isUsageStatsPermissionGranted.value
else -> false
}
if (hasMissingPermissions) {
@@ -497,7 +503,7 @@ class FeatureSettingsActivity : AppCompatActivity() {
"Text and animations" -> !viewModel.isWriteSettingsEnabled.value || !isWriteSecureSettingsEnabled
"Lock screen clock" -> !isWriteSecureSettingsEnabled
"Screen refresh rate" -> !viewModel.isShizukuPermissionGranted.value
- "Shut-Up!" -> !isWriteSecureSettingsEnabled || !viewModel.isUsageStatsPermissionGranted.value
+ "Shut-Up!" -> !isWriteSecureSettingsEnabled || !isWriteSettingsEnabled || !isUsageStatsPermissionGranted || !isPostNotificationsEnabled
else -> false
}
@@ -743,6 +749,14 @@ class FeatureSettingsActivity : AppCompatActivity() {
)
}
+ "Shut-Up!" -> {
+ ShutUpSettingsUI(
+ viewModel = viewModel,
+ modifier = Modifier.padding(top = 16.dp),
+ highlightSetting = highlightSetting
+ )
+ }
+
"Always on Display" -> {
AlwaysOnDisplaySettingsUI(
viewModel = viewModel,
@@ -774,14 +788,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 263bf0fb3..d26d70d1d 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,137 +97,6 @@ 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 debe3a5f9..6ce2cbcde 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()
@@ -233,10 +235,10 @@ 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"
- const val KEY_DISABLE_ROTATION_SUGGESTION = "disable_rotation_suggestion"
const val KEY_LOCK_SCREEN_CLOCK_WEIGHT = "lock_screen_clock_weight"
const val KEY_LOCK_SCREEN_CLOCK_WIDTH = "lock_screen_clock_width"
@@ -246,6 +248,8 @@ class SettingsRepository(private val context: Context) {
const val KEY_LOCK_SCREEN_CLOCK_SELECTED_COLOR_ID = "lock_screen_clock_selected_color_id"
const val KEY_LOCK_SCREEN_CLOCK_SEED_COLOR = "lock_screen_clock_seed_color"
const val KEY_RECENT_SEARCHES = "recent_searches"
+
+ const val KEY_SHUT_UP_SERVICE_ENABLED = "shutup_service_enabled"
}
// Observe changes
@@ -505,52 +509,7 @@ class SettingsRepository(private val context: Context) {
fun updateNotificationGlanceAppSelection(packageName: String, enabled: Boolean) =
updateAppSelection(KEY_NOTIFICATION_GLANCE_SELECTED_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()
@@ -854,11 +813,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 removeTrackedRepo(fullName: String) {
val current = getTrackedRepos().toMutableList()
@@ -1453,5 +1407,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 1db1cb1e4..47ced8867 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 47dd20bfd..70ee12e8f 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
@@ -857,24 +857,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,
- isBeta = 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 = "Location reached",
title = R.string.feat_location_reached_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 f554d622f..a27d913b1 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
@@ -2,25 +2,25 @@ package com.sameerasw.essentials.services.handlers
import android.accessibilityservice.AccessibilityService
import android.app.NotificationManager
-import android.app.PendingIntent
-import android.content.BroadcastReceiver
+
import android.content.ComponentName
import android.content.Context
import android.content.Intent
-import android.content.IntentFilter
import android.content.pm.PackageManager
import android.os.Handler
import android.os.Looper
import android.provider.Settings
import android.util.Log
-import androidx.core.app.NotificationCompat
+
import com.google.gson.Gson
import com.sameerasw.essentials.domain.diy.Automation
import com.sameerasw.essentials.domain.diy.DIYRepository
import com.sameerasw.essentials.domain.model.AppSelection
import com.sameerasw.essentials.services.automation.executors.CombinedActionExecutor
-import com.sameerasw.essentials.utils.FreezeManager
+
import com.sameerasw.essentials.utils.StatusBarManager
+import com.sameerasw.essentials.utils.ShutUpManager
+import com.sameerasw.essentials.domain.model.ShutUpAppConfig
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -33,43 +33,11 @@ class AppFlowHandler(
private val service: AccessibilityService? = null
) {
private val handler = Handler(Looper.getMainLooper())
- private val scope = CoroutineScope(Dispatchers.Main)
+ private val scope = CoroutineScope(Dispatchers.Main.immediate)
private val authenticatedPackages = mutableSetOf()
private val lastLeaveTimes = mutableMapOf()
- private val activeCountdowns = mutableMapOf()
-
- private val shutUpReceiver = object : BroadcastReceiver() {
- override fun onReceive(context: Context?, intent: Intent?) {
- val packageName = intent?.getStringExtra(EXTRA_PACKAGE_NAME) ?: return
- when (intent.action) {
- ACTION_FREEZE_NOW -> {
- activeCountdowns[packageName]?.cancel()
- activeCountdowns.remove(packageName)
- context?.let { FreezeManager.freezeApp(it, packageName) }
- cancelNotification(packageName)
- }
- ACTION_ABORT_FREEZE -> {
- activeCountdowns[packageName]?.cancel()
- activeCountdowns.remove(packageName)
- cancelNotification(packageName)
- }
- }
- }
- }
-
- init {
- val filter = IntentFilter().apply {
- addAction(ACTION_FREEZE_NOW)
- addAction(ACTION_ABORT_FREEZE)
- }
- 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)
- }
- }
// App Lock State
private var lockingPackage: String? = null
@@ -96,6 +64,8 @@ class AppFlowHandler(
val prefs = context.getSharedPreferences("essentials_prefs", Context.MODE_PRIVATE)
val useUsageAccess = prefs.getBoolean("use_usage_access", false)
+ Log.d("AppFlowHandler", "onPackageChanged: packageName=$packageName, isFromUsageStats=$isFromUsageStats, useUsageAccess=$useUsageAccess, currentPackage=$currentPackage")
+
val oldPackage = currentPackage
currentPackage = packageName
@@ -108,11 +78,12 @@ class AppFlowHandler(
}
if (isFromUsageStats == useUsageAccess) {
+ Log.d("AppFlowHandler", "onPackageChanged: Processing package change because isFromUsageStats matches useUsageAccess")
checkAppLock(packageName)
checkHighlightNightLight(packageName)
checkAppAutomations(packageName)
checkGestureBarAutomation(packageName)
- checkShutUpRestore(oldPackage, packageName)
+ checkShutUp(packageName)
}
}
@@ -128,6 +99,26 @@ class AppFlowHandler(
authenticatedPackages.clear()
}
+ 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 json = prefs.getString("shut_up_selected_apps", null) ?: return
+ val configs: List = try {
+ Gson().fromJson(json, Array::class.java).toList()
+ } catch (_: Exception) {
+ return
+ }
+
+ val config = configs.find { it.packageName == packageName && it.isEnabled } ?: return
+
+ scope.launch(Dispatchers.IO) {
+ Log.d("AppFlowHandler", "checkShutUp: Immediately applying ShutUp settings for $packageName via accessibility event")
+ ShutUpManager.applyShutUpSettings(context, config)
+ }
+ }
+
private fun checkAppLock(packageName: String) {
val prefs = context.getSharedPreferences("essentials_prefs", Context.MODE_PRIVATE)
val isEnabled = prefs.getBoolean("app_lock_enabled", false)
@@ -362,220 +353,7 @@ 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,
- 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) {
@@ -590,109 +368,11 @@ class AppFlowHandler(
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)
}
}
- private fun restoreShutUpSettings(
- repository: com.sameerasw.essentials.data.repository.SettingsRepository,
- autoArchivePackage: String? = null
- ) {
- val originalSettings = repository.getShutUpOriginalSettings()
- if (originalSettings.isEmpty()) {
- if (autoArchivePackage != null) {
- startAutoArchiveCountdown(autoArchivePackage)
- }
- return
- }
- scope.launch {
- // Delay to ensure the app has fully settled before restoring system settings
- delay(2000)
-
- 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
- delay(1000)
- restartShizuku()
-
- 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() {
- try {
- val intent = Intent("moe.shizuku.privileged.api.START").apply {
- `package` = "moe.shizuku.privileged.api"
- putExtra("auth", "y95fuaRb9USHiIg724tvTHIs")
- addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES)
- }
- context.sendBroadcast(intent)
- } catch (e: Exception) {
- Log.e("AppFlowHandler", "Failed to restart Shizuku", e)
- }
- }
companion object {
- const val ACTION_FREEZE_NOW = "com.sameerasw.essentials.ACTION_FREEZE_NOW"
- const val ACTION_ABORT_FREEZE = "com.sameerasw.essentials.ACTION_ABORT_FREEZE"
- const val EXTRA_PACKAGE_NAME = "package_name"
}
}
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 5d858091f..3cf0eb18a 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
@@ -48,7 +53,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(
@@ -58,7 +63,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)
}) {
@@ -132,7 +137,9 @@ fun ShutUpPerAppSettingsSheet(
title = stringResource(R.string.shut_up_attempt_shizuku_restart),
isChecked = isAttemptShizukuRestart,
onCheckedChange = {
- viewModel.setShutUpAttemptShizukuRestartEnabled(it)
+ 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 bb363cb86..de371c75e 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
@@ -77,6 +77,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
@@ -383,47 +384,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) {
@@ -496,6 +457,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(
@@ -772,6 +811,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 cc22146e9..4b9bc7561 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,53 +1,157 @@
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.layout.Arrangement
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
-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.*
+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.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.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.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
@Composable
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
+
+ // 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)
+ }
- val configs by viewModel.shutUpConfigs
+ 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,
@@ -55,115 +159,151 @@ fun ShutUpSettingsUI(
cornerRadius = 24.dp
) {
FeatureCard(
- title = stringResource(R.string.shut_up_select_apps_title),
- description = stringResource(R.string.shut_up_select_apps_desc),
- iconRes = R.drawable.rounded_app_registration_24,
- isEnabled = true,
- showToggle = false,
- hasMoreSettings = true,
- onToggle = {},
- onClick = { isAppSelectionSheetOpen = true }
+ 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,
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
- }
+ FeatureCard(
+ title = stringResource(R.string.shut_up_select_apps_title),
+ description = stringResource(R.string.shut_up_select_apps_desc),
+ iconRes = R.drawable.rounded_app_registration_24,
+ isEnabled = true,
+ showToggle = false,
+ hasMoreSettings = false,
+ onToggle = {},
+ onClick = {
+ HapticUtil.performVirtualKeyHaptic(view)
+ isAppSelectionSheetOpen = true
}
+ )
+ }
- 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 configs by viewModel.shutUpConfigs
+ if (configs.isNotEmpty()) {
+ 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
+ }
}
- }
- 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 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
}
- },
- 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
- )
- }
- )
}
- )
+
+ 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 = {
+ editingPackageName = config.packageName
+ editingConfig = config
+ isEditSheetOpen = true
+ },
+ 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
+ )
+ }
+ )
+ }
+ )
+ }
}
}
- 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
)
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 088f7375a..efb7208a1 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(
@@ -35,11 +36,8 @@ object ServiceUtils {
it.isEnabled && it.type == Automation.Type.APP
}
- val shutUpConfigs = settingsRepository.loadShutUpConfigs()
- 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 +74,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 492ac93af..3106d4663 100644
--- a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt
+++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt
@@ -37,6 +37,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
@@ -119,6 +120,8 @@ class MainViewModel : ViewModel() {
val isBluetoothPermissionGranted = mutableStateOf(false)
val isUsageStatsPermissionGranted = mutableStateOf(false)
val appLanguage = mutableStateOf("en")
+ val isShutUpServiceEnabled = mutableStateOf(false)
+ val shutUpConfigs = mutableStateOf>(emptyList())
val isBluetoothDevicesEnabled = mutableStateOf(false)
val isCallVibrationsEnabled = mutableStateOf(false)
@@ -156,10 +159,7 @@ class MainViewModel : ViewModel() {
mutableStateOf(SettingsRepository.LIVE_WALLPAPER_TRIGGER_UNLOCK)
val liveWallpaperCustomVideos = mutableStateListOf()
- val shutUpConfigs =
- mutableStateOf>(emptyList())
- val isShutUpLoading = mutableStateOf(false)
- val isShutUpAttemptShizukuRestart = mutableStateOf(true)
+
data class CalendarAccount(
@@ -628,10 +628,7 @@ class MainViewModel : ViewModel() {
liveWallpaperCustomVideos.addAll(settingsRepository.getLiveWallpaperCustomVideos())
}
- SettingsRepository.KEY_SHUT_UP_ATTEMPT_SHIZUKU_RESTART -> {
- isShutUpAttemptShizukuRestart.value =
- settingsRepository.isShutUpAttemptShizukuRestartEnabled()
- }
+
SettingsRepository.KEY_DISABLE_ROTATION_SUGGESTION -> {
isDisableRotationSuggestionEnabled.value =
@@ -658,12 +655,21 @@ 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()
}
@@ -674,63 +680,62 @@ class MainViewModel : ViewModel() {
loadShutUpConfigs()
}
- fun setShutUpAttemptShizukuRestartEnabled(enabled: Boolean) {
- isShutUpAttemptShizukuRestart.value = enabled
- settingsRepository.setShutUpAttemptShizukuRestartEnabled(enabled)
+ 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)
+ }
}
- 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 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
}
- 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()
+ 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)
@@ -776,8 +781,6 @@ class MainViewModel : ViewModel() {
notificationLightingSystemMode.intValue =
settingsRepository.getNotificationLightingSystemMode()
- isShutUpAttemptShizukuRestart.value =
- settingsRepository.isShutUpAttemptShizukuRestartEnabled()
isDisableRotationSuggestionEnabled.value =
settingsRepository.getBoolean(SettingsRepository.KEY_DISABLE_ROTATION_SUGGESTION, false)
lockScreenClockId.value = readCurrentLockScreenClockId(context)
@@ -789,6 +792,7 @@ class MainViewModel : ViewModel() {
lockScreenClockSelectedColorId.value =
settingsRepository.getLockScreenClockSelectedColorId()
lockScreenClockSeedColor.intValue = settingsRepository.getLockScreenClockSeedColor()
+ isShutUpServiceEnabled.value = settingsRepository.isShutUpServiceEnabled()
loadShutUpConfigs()
recentSearches.value = settingsRepository.getRecentSearches()
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 2d79cf8a2..4589e99f1 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1506,6 +1506,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