diff --git a/.gitignore b/.gitignore
index 3f88340a4..e88900855 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,7 @@
# Package Files #
*.jar
+!gradle/wrapper/gradle-wrapper.jar
*.war
*.nar
*.ear
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 95fb9c0dd..f9b222b64 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -88,6 +88,9 @@
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
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 282243c51..b1bd0a236 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
@@ -22,8 +22,11 @@ import com.sameerasw.essentials.domain.model.NotificationLightingSide
import com.sameerasw.essentials.domain.model.NotificationLightingStyle
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
@@ -34,7 +37,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()
@@ -275,6 +278,8 @@ class SettingsRepository(private val context: Context) {
const val LIVE_WALLPAPER_TRIGGER_UNLOCK = "unlock"
const val LIVE_WALLPAPER_TRIGGER_SCREEN_ON = "screen_on"
+ const val KEY_DISABLE_ROTATION_SUGGESTION = "disable_rotation_suggestion"
+
const val KEY_SHUT_UP_SELECTED_APPS = "shut_up_selected_apps"
const val KEY_SHUT_UP_ORIGINAL_SETTINGS = "shut_up_original_settings"
const val KEY_SHUT_UP_ATTEMPT_SHIZUKU_RESTART = "shut_up_attempt_shizuku_restart"
@@ -282,7 +287,6 @@ class SettingsRepository(private val context: Context) {
const val KEY_SHUT_UP_RESTORE_MODE = "shut_up_restore_mode"
const val KEY_SHIZUKU_AUTH_TOKEN = "shizuku_auth_token"
const val KEY_EDGE_LIGHTING_SWEEP_SELECTED_SHAPES = "edge_lighting_sweep_selected_shapes"
- const val KEY_DISABLE_ROTATION_SUGGESTION = "disable_rotation_suggestion"
const val KEY_ALLOW_OVERLAYS_IN_SETTINGS = "allow_overlays_in_settings"
const val KEY_NETWORK_DOWNLOAD_RATE_LIMIT = "network_download_rate_limit"
const val KEY_MOBILE_DATA_ALWAYS_ON = "mobile_data_always_on"
@@ -306,6 +310,7 @@ class SettingsRepository(private val context: Context) {
const val KEY_PIXEL_SEARCHBAR_MUSIC_ARTIST = "pixel_searchbar_music_artist"
const val KEY_PIXEL_SEARCHBAR_MUSIC_PACKAGE = "pixel_searchbar_music_package"
+
const val KEY_LOCK_SCREEN_CLOCK_WEIGHT = "lock_screen_clock_weight"
const val KEY_LOCK_SCREEN_CLOCK_WIDTH = "lock_screen_clock_width"
const val KEY_LOCK_SCREEN_CLOCK_GRADE = "lock_screen_clock_grade"
@@ -321,8 +326,8 @@ class SettingsRepository(private val context: Context) {
const val KEY_POCKET_MODE_LOCK_SCREEN_ONLY = "pocket_mode_lock_screen_only"
const val KEY_KEEP_PREFS = "keep_prefs"
const val KEY_TRANSLATION_MODE_DO_NOT_SHOW_WARNING = "translation_mode_do_not_show_warning"
-
const val KEY_LOCKDOWN_MODE = "lockdown_mode"
+ const val KEY_SHUT_UP_SERVICE_ENABLED = "shutup_service_enabled"
}
/**
@@ -927,76 +932,8 @@ class SettingsRepository(private val context: Context) {
fun updatePocketModeExcludedAppSelection(packageName: String, enabled: Boolean) =
updateAppSelection(KEY_POCKET_MODE_EXCLUDED_APPS, packageName, enabled)
- /**
- * Executes the load shut up configs operation.
- * @return The resulting 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()
- }
- }
-
- /**
- * Executes the save shut up configs operation.
- *
- * @param configs [List] Target configs.
- */
- fun saveShutUpConfigs(configs: List) {
- val json = gson.toJson(configs)
- putString(KEY_SHUT_UP_SELECTED_APPS, json)
- }
-
- /**
- * Executes the update shut up config operation.
- *
- * @param config [com.sameerasw.essentials.domain.model.ShutUpAppConfig] Target config.
- */
- 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)
- }
- /**
- * Executes the save shut up original settings operation.
- *
- * @param settings [Map Target string.
- */
- fun saveShutUpOriginalSettings(settings: Map) {
- val json = gson.toJson(settings)
- putString(KEY_SHUT_UP_ORIGINAL_SETTINGS, json)
- }
- /**
- * Executes the get shut up original settings operation.
- * @return The resulting Map data.
- */
- 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()
@@ -2820,5 +2757,60 @@ class SettingsRepository(private val context: Context) {
* @param value [Int] Target value.
*/
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 updateShutUpConfig(config: 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 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 771d0cfb5..11da9d832 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
@@ -48,7 +48,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 b20ce265a..1e3f21bf9 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
@@ -12,9 +12,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 cd9f0ab0c..b6ef3b536 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
@@ -966,10 +966,6 @@ object FeatureRegistry {
category = R.string.cat_interaction,
description = R.string.feat_button_remap_desc,
aboutDescription = R.string.about_desc_button_remap,
- permissionKeys = if (ShellUtils.isRootEnabled(EssentialsApp.context)) listOf(
- "ACCESSIBILITY",
- "ROOT"
- ) else listOf("ACCESSIBILITY", "SHIZUKU"),
showToggle = true,
searchableSettings = listOf(
SearchSetting(
@@ -1000,6 +996,20 @@ object FeatureRegistry {
parentFeatureId = "Input",
animationRes = R.raw.button_animation
) {
+ override val permissionKeys: List
+ get() {
+ val baseKeys = if (ShellUtils.isRootEnabled(EssentialsApp.context)) listOf(
+ "ACCESSIBILITY",
+ "ROOT"
+ ) else listOf("ACCESSIBILITY", "SHIZUKU")
+ val repository = com.sameerasw.essentials.data.repository.SettingsRepository(EssentialsApp.context)
+ val needsRecordAudio = repository.getString("button_remap_vol_up_action_off", "None") == "Toggle audio recording" ||
+ repository.getString("button_remap_vol_down_action_off", "None") == "Toggle audio recording" ||
+ repository.getString("button_remap_vol_up_action_on", "None") == "Toggle audio recording" ||
+ repository.getString("button_remap_vol_down_action_on", "None") == "Toggle audio recording"
+ return if (needsRecordAudio) baseKeys + "RECORD_AUDIO" else baseKeys
+ }
+
override fun isEnabled(viewModel: MainViewModel) = viewModel.isButtonRemapEnabled.value
override fun isToggleEnabled(viewModel: MainViewModel, context: Context) =
viewModel.isAccessibilityEnabled.value
@@ -1182,23 +1192,36 @@ object FeatureRegistry {
override fun onToggle(viewModel: MainViewModel, context: Context, enabled: Boolean) =
viewModel.setAppLockEnabled(enabled, context)
},
+
object : Feature(
id = "Shut-Up!",
title = R.string.feat_shut_up_title,
- iconRes = R.drawable.rounded_domino_mask_24,
- category = R.string.cat_system,
+ iconRes = R.drawable.rounded_shield_lock_24,
+ category = R.string.cat_protection,
description = R.string.feat_shut_up_desc,
aboutDescription = R.string.shut_up_description,
- permissionKeys = listOf("WRITE_SECURE_SETTINGS", "USAGE_STATS"),
+ permissionKeys = listOf("WRITE_SECURE_SETTINGS", "WRITE_SETTINGS", "USAGE_STATS", "POST_NOTIFICATIONS"),
showToggle = false,
hasMoreSettings = true,
parentFeatureId = "Security",
animationRes = R.raw.shutup_animation
) {
- override fun isEnabled(viewModel: MainViewModel) = true
- override fun onToggle(viewModel: MainViewModel, context: Context, enabled: Boolean) {}
+ override val requiresAuth: Boolean = false
+
+ override fun isEnabled(viewModel: MainViewModel) =
+ viewModel.isShutUpServiceEnabled.value
+
+ override fun isToggleEnabled(viewModel: MainViewModel, context: Context) =
+ viewModel.isWriteSecureSettingsEnabled.value &&
+ viewModel.isWriteSettingsEnabled.value &&
+ viewModel.isUsageStatsPermissionGranted.value &&
+ viewModel.isPostNotificationsEnabled.value
+
+ override fun onToggle(viewModel: MainViewModel, context: Context, enabled: Boolean) =
+ viewModel.setShutUpServiceEnabled(enabled, context)
},
+
object : Feature(
id = "Pocket mode",
title = R.string.feat_pocket_mode_title,
diff --git a/app/src/main/java/com/sameerasw/essentials/domain/registry/PermissionRegistry.kt b/app/src/main/java/com/sameerasw/essentials/domain/registry/PermissionRegistry.kt
index f063f7f42..b577f9a70 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
@@ -124,10 +124,12 @@ 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)
+ PermissionRegistry.register("RECORD_AUDIO", R.string.feat_button_remap_title)
// Power and Battery feature
PermissionRegistry.register("WRITE_SECURE_SETTINGS", R.string.feat_power_battery_title)
diff --git a/app/src/main/java/com/sameerasw/essentials/services/AppDetectionService.kt b/app/src/main/java/com/sameerasw/essentials/services/AppDetectionService.kt
index 923988a63..b6379493c 100644
--- a/app/src/main/java/com/sameerasw/essentials/services/AppDetectionService.kt
+++ b/app/src/main/java/com/sameerasw/essentials/services/AppDetectionService.kt
@@ -62,7 +62,7 @@ class AppDetectionService : Service() {
override fun onCreate() {
super.onCreate()
isRunning = true
- appFlowHandler = AppFlowHandler(this)
+ appFlowHandler = AppFlowHandler.getInstance(this)
createNotificationChannel()
val filter = IntentFilter().apply {
@@ -143,6 +143,12 @@ class AppDetectionService : Service() {
unregisterReceiver(authReceiver)
} catch (_: Exception) {
}
+ try {
+ if (::appFlowHandler.isInitialized) {
+ appFlowHandler.destroy()
+ }
+ } catch (_: Exception) {
+ }
super.onDestroy()
}
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..c23afdfad
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/services/ShutUpForegroundService.kt
@@ -0,0 +1,342 @@
+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
+ @Volatile private var monitorJob: Job? = null
+ @Volatile private var lastPackageName: String? = null
+ private var lastQueryTime = System.currentTimeMillis() - 5000
+
+ @Volatile private var pendingRestoreJob: Job? = null
+ private var pendingRestorePackage: String? = null
+ @Volatile 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
+
+ // Cached system service and reusable event object (only accessed from monitorJob / Default dispatcher)
+ private val usageStatsManager by lazy { getSystemService(USAGE_STATS_SERVICE) as UsageStatsManager }
+ private val reusableEvent = UsageEvents.Event()
+
+ 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"
+
+ @Volatile 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
+ )
+
+ // Recover backup before monitoring starts. Avoid startup restore/apply race.
+ serviceScope.launch {
+ val backup = settingsRepository.getShutUpOriginalSettings()
+ if (backup.isNotEmpty()) {
+ Log.d(TAG, "Found pending ShutUp backup on startup — checking if restore is needed")
+ val now = System.currentTimeMillis()
+ val foregroundPackage = getForegroundPackage(now - 5000, now)
+ val foregroundShutUp = settingsRepository.loadShutUpConfigs().any {
+ it.isEnabled && it.packageName == foregroundPackage
+ }
+ if (!foregroundShutUp) {
+ Log.d(TAG, "No shut-up app running — restoring backup on startup")
+ ShutUpManager.restoreOriginalSettings(this@ShutUpForegroundService, settingsRepository)
+ }
+ }
+ 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) {
+ val previousPkg = lastPackageName
+ lastPackageName = currentPkg
+ onPackageChanged(previousPkg, 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,
+ settingsRepository,
+ reinforcement = true
+ )
+ }
+ }
+ }
+ } else {
+ lastQueryTime = now - 500
+ }
+ delay(400)
+ }
+ }
+ }
+
+ private fun getForegroundPackage(startTime: Long, endTime: Long): String? {
+ try {
+ val events = usageStatsManager.queryEvents(startTime, endTime)
+ var lastResumedPackage: String? = null
+ while (events.hasNextEvent()) {
+ events.getNextEvent(reusableEvent)
+ if (reusableEvent.eventType == UsageEvents.Event.ACTIVITY_RESUMED) {
+ lastResumedPackage = reusableEvent.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()
+
+ val newConfig = configs.find { it.packageName == newPkg && it.isEnabled }
+
+ // 1. Leaving a Shut-Up app. Keep the session snapshot when moving directly to another
+ // Shut-Up app; restoring between targets would briefly re-enable protected settings.
+ if (oldPkg != null && configs.any { it.packageName == oldPkg && it.isEnabled }) {
+ pendingRestoreJob?.cancel()
+ if (newConfig == null) {
+ activeTargetConfig = null
+ pendingRestorePackage = oldPkg
+ pendingRestoreJob = serviceScope.launch {
+ delay(settingsRepository.getShutUpRestoreDelay().coerceAtLeast(0) * 1000L)
+ val config = settingsRepository.loadShutUpConfigs().find { it.packageName == oldPkg }
+ if (config != null && config.isEnabled && lastPackageName != oldPkg && activeTargetConfig == null) {
+ ShutUpManager.restoreOriginalSettings(this@ShutUpForegroundService, settingsRepository)
+ if (config.attemptShizukuRestart) {
+ ShutUpManager.restartShizuku(this@ShutUpForegroundService)
+ }
+ if (config.autoArchive) {
+ showAutoFreezeNotification(config.packageName)
+ }
+ } else {
+ Log.d(TAG, "Skipping restore for $oldPkg — another target is active or package returned")
+ }
+ pendingRestorePackage = null
+ pendingRestoreJob = null
+ }
+ } else {
+ pendingRestorePackage = null
+ }
+ }
+
+ // 2. Entering a Shut-Up app
+ if (newConfig != null) {
+ activeTargetConfig = newConfig
+ 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, settingsRepository)
+ } 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
+ }
+
+ // Build PendingIntents once — they do not change between countdown ticks
+ val freezePendingIntent = PendingIntent.getService(
+ this@ShutUpForegroundService,
+ 101,
+ Intent(this@ShutUpForegroundService, ShutUpForegroundService::class.java).apply {
+ action = ACTION_FREEZE_NOW
+ putExtra(EXTRA_PACKAGE_NAME, packageName)
+ },
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+ val abortPendingIntent = PendingIntent.getService(
+ this@ShutUpForegroundService,
+ 102,
+ Intent(this@ShutUpForegroundService, ShutUpForegroundService::class.java).apply {
+ action = ACTION_ABORT_FREEZE
+ },
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+
+ freezeCountdownJob = serviceScope.launch {
+ var secondsRemaining = 5
+ while (secondsRemaining > 0) {
+ 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), freezePendingIntent)
+ .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()
+
+ val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
+ notificationManager.cancel(NOTIFICATION_FREEZE_ID)
+
+ // Best-effort synchronous restore if we were mid-restore cycle when the service was stopped.
+ // Only restore if no shut-up app is actively in the foreground (activeTargetConfig == null means
+ // we left the target app and were waiting for it to close before reverting).
+ val pkg = pendingRestorePackage
+ if (pkg != null && activeTargetConfig == null) {
+ try {
+ val configs = settingsRepository.loadShutUpConfigs()
+ val config = configs.find { it.packageName == pkg && it.isEnabled }
+ if (config != null && !ShutUpManager.isAppRunning(this, pkg)) {
+ runBlocking(Dispatchers.IO) {
+ ShutUpManager.restoreOriginalSettings(this@ShutUpForegroundService, settingsRepository)
+ }
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed sync restore on destroy", e)
+ }
+ }
+
+ serviceScope.cancel()
+ 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 b33687719..6dc2cbfc6 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
@@ -13,6 +13,7 @@ import android.accessibilityservice.AccessibilityService
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
+import android.media.AudioManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
@@ -21,15 +22,23 @@ import android.content.pm.PackageManager
import android.os.Handler
import android.os.Looper
import android.provider.Settings
+import android.content.res.Configuration
+import android.os.Build
import android.util.Log
import androidx.core.app.NotificationCompat
+import android.view.inputmethod.InputMethodManager
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.data.repository.SettingsRepository
import com.sameerasw.essentials.services.automation.executors.CombinedActionExecutor
import com.sameerasw.essentials.utils.FreezeManager
+import com.sameerasw.essentials.services.NotificationListener
import com.sameerasw.essentials.utils.StatusBarManager
+import com.sameerasw.essentials.utils.ShutUpManager
+import com.sameerasw.essentials.domain.model.ShutUpAppConfig
+import com.sameerasw.essentials.utils.RefreshRateUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -37,67 +46,68 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
-class AppFlowHandler(
- private val context: Context,
- private val service: AccessibilityService? = null
+class AppFlowHandler private constructor(
+ context: Context
) {
+ private val context = context.applicationContext
private val handler = Handler(Looper.getMainLooper())
- private val scope = CoroutineScope(Dispatchers.Main)
-
- private val authenticatedPackages = mutableSetOf()
- private val lastLeaveTimes = mutableMapOf()
- private val activeCountdowns = mutableMapOf()
-
- private val shutUpReceiver = object : BroadcastReceiver() {
- override fun onReceive(context: Context?, intent: Intent?) {
- when (intent?.action) {
- ACTION_FREEZE_NOW -> {
- val packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME) ?: return
- activeCountdowns[packageName]?.cancel()
- activeCountdowns.remove(packageName)
- context?.let { FreezeManager.freezeApp(it, packageName) }
- cancelNotification(packageName)
- }
-
- ACTION_ABORT_FREEZE -> {
- val packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME) ?: return
- activeCountdowns[packageName]?.cancel()
- activeCountdowns.remove(packageName)
- cancelNotification(packageName)
- }
-
- ACTION_RESTORE_NOW -> {
- cancelRestoreNotification()
- val autoPkg = intent.getStringExtra(EXTRA_AUTO_ARCHIVE_PACKAGE)
- val pkgName = intent.getStringExtra(EXTRA_PACKAGE_NAME)
- val settingsRepo = com.sameerasw.essentials.data.repository.SettingsRepository(
- context ?: return
- )
- val config = if (pkgName != null) {
- settingsRepo.loadShutUpConfigs().find { it.packageName == pkgName }
- } else null
- restoreShutUpSettings(settingsRepo, config, autoPkg, forceRestore = true)
- }
+ private var lastOrientation = context.resources.configuration.orientation
+ private val componentCallbacks = object : android.content.ComponentCallbacks2 {
+ override fun onConfigurationChanged(newConfig: Configuration) {
+ val newOrientation = newConfig.orientation
+ if (newOrientation != lastOrientation) {
+ lastOrientation = newOrientation
}
}
+ override fun onLowMemory() {}
+ override fun onTrimMemory(level: Int) {}
+ }
+
+ private val prefsChangeListener = android.content.SharedPreferences.OnSharedPreferenceChangeListener { _, _ -> }
+
+ private val mediaReceiver = object : android.content.BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {}
}
init {
- val filter = IntentFilter().apply {
- addAction(ACTION_FREEZE_NOW)
- addAction(ACTION_ABORT_FREEZE)
- addAction(ACTION_RESTORE_NOW)
- }
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
- context.registerReceiver(shutUpReceiver, filter, Context.RECEIVER_EXPORTED)
+ this.context.registerComponentCallbacks(componentCallbacks)
+ val filter = IntentFilter("com.sameerasw.essentials.MEDIA_PLAYBACK_CHANGED")
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ this.context.registerReceiver(mediaReceiver, filter, Context.RECEIVER_EXPORTED)
} else {
- context.registerReceiver(shutUpReceiver, filter)
+ this.context.registerReceiver(mediaReceiver, filter)
}
+ val prefs = this.context.getSharedPreferences(SettingsRepository.PREFS_NAME, Context.MODE_PRIVATE)
+ prefs.registerOnSharedPreferenceChangeListener(prefsChangeListener)
+ }
+
+ fun destroy() {
+ try {
+ val prefs = this.context.getSharedPreferences(SettingsRepository.PREFS_NAME, Context.MODE_PRIVATE)
+ prefs.unregisterOnSharedPreferenceChangeListener(prefsChangeListener)
+ } catch (_: Exception) {}
+ try {
+ context.unregisterComponentCallbacks(componentCallbacks)
+ } catch (_: Exception) {}
+ try {
+ context.unregisterReceiver(mediaReceiver)
+ } catch (_: Exception) {}
+ }
+ private val scope = CoroutineScope(Dispatchers.Main.immediate)
+
+ private val settingsRepository by lazy { SettingsRepository(context) }
+ private val prefs by lazy { context.getSharedPreferences(SettingsRepository.PREFS_NAME, Context.MODE_PRIVATE) }
+ private val notificationListenerComponent by lazy {
+ ComponentName(context, NotificationListener::class.java)
}
+ private val authenticatedPackages = mutableSetOf()
+ private val lastLeaveTimes = mutableMapOf()
+
// App Lock State
private var lockingPackage: String? = null
private var lastLockRequestTime: Long = 0
+ @Volatile
var currentPackage: String? = null
private set
private var currentUsageStatsPackage: String? = null
@@ -114,28 +124,83 @@ class AppFlowHandler(
private val ignoredSystemPackages = listOf(
"android",
"com.android.systemui",
- "com.google.android.inputmethod.latin"
+ "com.google.android.inputmethod.latin",
+ "com.google.android.gms"
)
+ private fun isIgnoredPackage(packageName: String): Boolean {
+ if (packageName == context.packageName) return true
+ if (ignoredSystemPackages.contains(packageName)) return true
+
+ val lowerPkg = packageName.lowercase()
+ if (lowerPkg.contains("systemui") ||
+ lowerPkg.contains("keyguard") ||
+ lowerPkg.contains("volume") ||
+ lowerPkg.contains("soundassistant") ||
+ lowerPkg.contains("dialer") ||
+ lowerPkg.contains("telecom") ||
+ lowerPkg.contains("phone") ||
+ lowerPkg.contains("incallui") ||
+ lowerPkg.contains("packageinstaller") ||
+ lowerPkg.contains("permissioncontroller")
+ ) {
+ return true
+ }
+
+ // Check active call state via AudioManager mode
+ val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
+ if (audioManager != null) {
+ val mode = audioManager.mode
+ if (mode == AudioManager.MODE_IN_CALL ||
+ mode == AudioManager.MODE_IN_COMMUNICATION ||
+ mode == AudioManager.MODE_RINGTONE
+ ) {
+ return true
+ }
+ }
+
+ return try {
+ val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
+ val ims = imm?.enabledInputMethodList
+ ims?.any { it.packageName == packageName } == true
+ } catch (_: Exception) {
+ false
+ }
+ }
+
fun onPackageChanged(packageName: String, isFromUsageStats: Boolean = false) {
- val prefs = context.getSharedPreferences("essentials_prefs", Context.MODE_PRIVATE)
- val useUsageAccess = prefs.getBoolean("use_usage_access", false)
+ val useUsageAccess = settingsRepository.getBoolean(SettingsRepository.KEY_USE_USAGE_ACCESS, false) &&
+ com.sameerasw.essentials.services.AppDetectionService.isRunning
+
+ Log.d("AppFlowHandler", "onPackageChanged: packageName=$packageName, isFromUsageStats=$isFromUsageStats, useUsageAccess=$useUsageAccess, currentPackage=$currentPackage")
+ // If the new foreground window belongs to a system overlay (status bar, quick settings,
+ // notifications), a keyboard (IME), a volume dialog, or a phone call, completely ignore it.
+ // We do NOT update currentPackage so that state-dependent features remain stable.
+ if (isIgnoredPackage(packageName)) {
+ Log.d("AppFlowHandler", "onPackageChanged: Ignoring system/IME/volume/call package $packageName")
+ return
+ }
val oldPackage = currentPackage
+ currentPackage = packageName
+ if (oldPackage != null && oldPackage != packageName) {
+ lastLeaveTimes[oldPackage] = System.currentTimeMillis()
+ }
+ if (packageName != context.packageName && packageName != lockingPackage) {
+ lockingPackage = null
+ }
+
if (isFromUsageStats == useUsageAccess) {
- currentPackage = packageName
- if (oldPackage != null && oldPackage != packageName) {
- lastLeaveTimes[oldPackage] = System.currentTimeMillis()
- checkShutUpRestore(oldPackage, packageName)
- }
- if (packageName != context.packageName && packageName != lockingPackage) {
- lockingPackage = null
- }
+ Log.d("AppFlowHandler", "onPackageChanged: Processing package change because isFromUsageStats matches useUsageAccess")
checkAppLock(packageName)
checkHighlightNightLight(packageName)
checkAppAutomations(packageName)
checkGestureBarAutomation(packageName)
}
+
+ // Accessibility events are the fastest automatic launch signal. The manager serializes
+ // this with the foreground-service fallback and periodic enforcement.
+ checkShutUp(packageName)
}
fun onAuthenticated(packageName: String) {
@@ -149,6 +214,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, settingsRepository)
+ }
+ }
+
private fun checkAppLock(packageName: String) {
val prefs = context.getSharedPreferences("essentials_prefs", Context.MODE_PRIVATE)
val isEnabled = prefs.getBoolean("app_lock_enabled", false)
@@ -224,7 +309,7 @@ class AppFlowHandler(
pendingNLRunnable?.let { handler.removeCallbacks(it) }
- if (ignoredSystemPackages.contains(packageName)) {
+ if (isIgnoredPackage(packageName)) {
Log.d("NightLight", "Ignoring system package $packageName")
return
}
@@ -296,6 +381,10 @@ class AppFlowHandler(
}
private fun checkAppAutomations(packageName: String) {
+ if (isIgnoredPackage(packageName)) {
+ Log.d("AppFlowHandler", "checkAppAutomations: Ignoring system/IME package $packageName")
+ return
+ }
scope.launch {
val automations = DIYRepository.automations.value
val appAutomations =
@@ -383,221 +472,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,
- wasShutUpConfig,
- if (wasShutUpConfig.autoArchive) wasShutUpConfig.packageName else null
- )
- }
- }
-
- private fun startAutoArchiveCountdown(packageName: String) {
- Log.d("AppFlowHandler", "startAutoArchiveCountdown: $packageName")
- // Cancel existing countdown for this app if any
- activeCountdowns[packageName]?.cancel()
- val appName = try {
- val appInfo = context.packageManager.getApplicationInfo(packageName, 0)
- context.packageManager.getApplicationLabel(appInfo).toString()
- } catch (e: Exception) {
- Log.e("AppFlowHandler", "Failed to get app name for $packageName", e)
- packageName
- }
-
- val job = scope.launch {
- Log.d("AppFlowHandler", "Countdown job started for $packageName")
- for (i in 10 downTo 1) {
- Log.d("AppFlowHandler", "Countdown for $packageName: $i")
- showCountdownNotification(packageName, appName, i)
- delay(1000)
- }
- // countdown finished
- Log.d("AppFlowHandler", "Countdown finished for $packageName, freezing...")
- val success = withContext(Dispatchers.IO) {
- FreezeManager.freezeApp(context, packageName)
- }
- Log.d("AppFlowHandler", "Freeze result for $packageName: $success")
- cancelNotification(packageName)
- activeCountdowns.remove(packageName)
- }
- activeCountdowns[packageName] = job
- }
-
- private fun showCountdownNotification(packageName: String, appName: String, secondsLeft: Int) {
- createNotificationChannel()
- val notificationManager =
- context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
-
- val freezeIntent = Intent(ACTION_FREEZE_NOW).apply {
- `package` = context.packageName
- putExtra(EXTRA_PACKAGE_NAME, packageName)
- }
- val freezePendingIntent = PendingIntent.getBroadcast(
- context,
- packageName.hashCode() + 1,
- freezeIntent,
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
- )
-
- val abortIntent = Intent(ACTION_ABORT_FREEZE).apply {
- `package` = context.packageName
- putExtra(EXTRA_PACKAGE_NAME, packageName)
- }
- val abortPendingIntent = PendingIntent.getBroadcast(
- context,
- packageName.hashCode() + 2,
- abortIntent,
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
- )
-
- val title =
- context.getString(com.sameerasw.essentials.R.string.shut_up_auto_archive_notif_title)
- val text = context.getString(
- com.sameerasw.essentials.R.string.shut_up_auto_archive_notif_text,
- appName,
- secondsLeft
- )
- val criticalText = secondsLeft.toString()
-
- val notification =
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
- val builder = android.app.Notification.Builder(context, "shutup_alerts_channel")
- .setSmallIcon(com.sameerasw.essentials.R.drawable.rounded_snowflake_24)
- .setContentTitle(title)
- .setContentText(text)
- .setOngoing(true)
- .setOnlyAlertOnce(true)
- .setCategory(android.app.Notification.CATEGORY_SERVICE)
- .setShowWhen(false)
- .setGroup("shutup_auto_archive")
- .setColorized(false)
-
- if (android.os.Build.VERSION.SDK_INT >= 31) {
- builder.setForegroundServiceBehavior(android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE)
- }
-
- builder.addAction(
- android.app.Notification.Action.Builder(
- android.graphics.drawable.Icon.createWithResource(
- context,
- com.sameerasw.essentials.R.drawable.rounded_snowflake_24
- ),
- context.getString(com.sameerasw.essentials.R.string.shut_up_auto_archive_action_freeze),
- freezePendingIntent
- ).build()
- )
- builder.addAction(
- android.app.Notification.Action.Builder(
- android.graphics.drawable.Icon.createWithResource(
- context,
- com.sameerasw.essentials.R.drawable.rounded_close_24
- ),
- context.getString(com.sameerasw.essentials.R.string.shut_up_auto_archive_action_abort),
- abortPendingIntent
- ).build()
- )
-
- // Live Update Status Chip
- try {
- val setRequestPromotedOngoing = builder.javaClass.getMethod(
- "setRequestPromotedOngoing",
- Boolean::class.javaPrimitiveType
- )
- setRequestPromotedOngoing.invoke(builder, true)
-
- val setShortCriticalText = builder.javaClass.getMethod(
- "setShortCriticalText",
- CharSequence::class.java
- )
- setShortCriticalText.invoke(builder, criticalText)
- } catch (_: Throwable) {
- }
-
- val extras = android.os.Bundle()
- extras.putBoolean("android.requestPromotedOngoing", true)
- extras.putString("android.shortCriticalText", criticalText)
- builder.addExtras(extras)
-
- builder.setProgress(10, secondsLeft, false)
-
- builder.build()
- } else {
- NotificationCompat.Builder(context, "shutup_alerts_channel")
- .setSmallIcon(com.sameerasw.essentials.R.drawable.rounded_snowflake_24)
- .setContentTitle(title)
- .setContentText(text)
- .setPriority(NotificationCompat.PRIORITY_MAX)
- .setCategory(NotificationCompat.CATEGORY_SERVICE)
- .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
- .setOnlyAlertOnce(true)
- .setOngoing(true)
- .setProgress(10, secondsLeft, false)
- .addAction(
- com.sameerasw.essentials.R.drawable.rounded_snowflake_24,
- context.getString(com.sameerasw.essentials.R.string.shut_up_auto_archive_action_freeze),
- freezePendingIntent
- )
- .addAction(
- com.sameerasw.essentials.R.drawable.rounded_close_24,
- context.getString(com.sameerasw.essentials.R.string.shut_up_auto_archive_action_abort),
- abortPendingIntent
- )
- .addExtras(android.os.Bundle().apply {
- putBoolean("android.requestPromotedOngoing", true)
- putString("android.shortCriticalText", criticalText)
- })
- .build()
- }
-
- Log.d("AppFlowHandler", "Showing notification for $packageName, secondsLeft=$secondsLeft")
- notificationManager.notify(packageName.hashCode(), notification)
- }
-
- private fun cancelNotification(packageName: String) {
- val notificationManager =
- context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
- notificationManager.cancel(packageName.hashCode())
- }
private fun createNotificationChannel() {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
@@ -613,241 +488,31 @@ class AppFlowHandler(
}
notificationManager.createNotificationChannel(channel)
- val alertChannel = android.app.NotificationChannel(
- "shutup_alerts_channel",
- "Shut-Up! Alerts",
- NotificationManager.IMPORTANCE_MAX
- ).apply {
- description = "Live update notifications for auto archiving"
- enableVibration(false)
- setSound(null, null)
- lockscreenVisibility = android.app.Notification.VISIBILITY_PUBLIC
- }
- notificationManager.createNotificationChannel(alertChannel)
-
- val restoreChannel = android.app.NotificationChannel(
- "shutup_restore_channel",
- "Shut-Up! Restore",
- NotificationManager.IMPORTANCE_MAX
- ).apply {
- description = "Notifications for restoring Shut-Up settings"
- enableVibration(false)
- setSound(null, null)
- lockscreenVisibility = android.app.Notification.VISIBILITY_PUBLIC
- }
- notificationManager.createNotificationChannel(restoreChannel)
- }
- }
-
- private fun showRestoreNotification(
- wasShutUpConfig: com.sameerasw.essentials.domain.model.ShutUpAppConfig?,
- autoArchivePackage: String?
- ) {
- createNotificationChannel()
- val notificationManager =
- context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
-
- val restoreIntent = Intent(ACTION_RESTORE_NOW).apply {
- `package` = context.packageName
- if (autoArchivePackage != null) {
- putExtra(EXTRA_AUTO_ARCHIVE_PACKAGE, autoArchivePackage)
- }
- if (wasShutUpConfig != null) {
- putExtra(EXTRA_PACKAGE_NAME, wasShutUpConfig.packageName)
- }
}
- val restorePendingIntent = PendingIntent.getBroadcast(
- context,
- 12345,
- restoreIntent,
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
- )
-
- val title = "Shut-Up active"
- val text = "Do you want to restore now?"
- val criticalText = "Restore Now"
-
- val notification =
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
- val builder = android.app.Notification.Builder(context, "shutup_restore_channel")
- .setSmallIcon(com.sameerasw.essentials.R.drawable.rounded_code_24)
- .setContentTitle(title)
- .setContentText(text)
- .setCategory(android.app.Notification.CATEGORY_SERVICE)
- .setVisibility(android.app.Notification.VISIBILITY_PUBLIC)
- .setOnlyAlertOnce(true)
- .setOngoing(true)
- .addAction(
- android.app.Notification.Action.Builder(
- android.graphics.drawable.Icon.createWithResource(
- context,
- com.sameerasw.essentials.R.drawable.rounded_code_24
- ),
- "Restore Now",
- restorePendingIntent
- ).build()
- )
-
- try {
- val setShortCriticalText = builder.javaClass.getMethod(
- "setShortCriticalText",
- CharSequence::class.java
- )
- setShortCriticalText.invoke(builder, criticalText)
- } catch (_: Throwable) {
- }
-
- val extras = android.os.Bundle()
- extras.putBoolean("android.requestPromotedOngoing", true)
- extras.putString("android.shortCriticalText", criticalText)
- builder.addExtras(extras)
- builder.build()
- } else {
- NotificationCompat.Builder(context, "shutup_restore_channel")
- .setSmallIcon(com.sameerasw.essentials.R.drawable.rounded_code_24)
- .setContentTitle(title)
- .setContentText(text)
- .setPriority(NotificationCompat.PRIORITY_MAX)
- .setCategory(NotificationCompat.CATEGORY_SERVICE)
- .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
- .setOnlyAlertOnce(true)
- .setOngoing(true)
- .addAction(
- com.sameerasw.essentials.R.drawable.rounded_code_24,
- "Restore Now",
- restorePendingIntent
- )
- .addExtras(android.os.Bundle().apply {
- putBoolean("android.requestPromotedOngoing", true)
- putString("android.shortCriticalText", criticalText)
- })
- .build()
- }
-
- notificationManager.notify(NOTIFICATION_ID_SHUTUP_RESTORE, notification)
}
- private fun cancelRestoreNotification() {
- val notificationManager =
- context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
- notificationManager.cancel(NOTIFICATION_ID_SHUTUP_RESTORE)
- }
-
- private fun restoreShutUpSettings(
- repository: com.sameerasw.essentials.data.repository.SettingsRepository,
- wasShutUpConfig: com.sameerasw.essentials.domain.model.ShutUpAppConfig?,
- autoArchivePackage: String? = null,
- forceRestore: Boolean = false
- ) {
- val originalSettings = repository.getShutUpOriginalSettings()
- if (originalSettings.isEmpty()) {
- if (autoArchivePackage != null) {
- startAutoArchiveCountdown(autoArchivePackage)
- }
- return
- }
-
- val mode = repository.getShutUpRestoreMode()
- if (mode == "Notify" && !forceRestore) {
- scope.launch {
- val delaySeconds = repository.getShutUpRestoreDelay()
- delay(delaySeconds * 1000L)
- showRestoreNotification(wasShutUpConfig, autoArchivePackage)
- }
- return
- }
-
- scope.launch {
- if (!forceRestore) {
- // Delay to ensure the app has fully settled before restoring system settings
- val delaySeconds = repository.getShutUpRestoreDelay()
- delay(delaySeconds * 1000L)
- }
-
- val canWriteSecure =
- com.sameerasw.essentials.utils.PermissionUtils.canWriteSecureSettings(context)
- val canWriteSystem = Settings.System.canWrite(context)
-
- originalSettings.forEach { (prefixedKey, value) ->
- try {
- val parts = prefixedKey.split(":", limit = 2)
- if (parts.size < 2) return@forEach
-
- val table = parts[0]
- val key = parts[1]
-
- when (table) {
- "global" -> {
- if (canWriteSecure) {
- Settings.Global.putString(context.contentResolver, key, value)
- }
- }
-
- "secure" -> {
- if (canWriteSecure) {
- Settings.Secure.putString(context.contentResolver, key, value)
- }
- }
-
- "system" -> {
- if (canWriteSystem) {
- Settings.System.putString(context.contentResolver, key, value)
- }
- }
- }
- } catch (e: Exception) {
- Log.e("AppFlowHandler", "Failed to restore setting $prefixedKey", e)
- }
- }
-
- // Clear original settings after restoration
- repository.saveShutUpOriginalSettings(emptyMap())
-
- // Wait a bit and Restart Shizuku as ADB might have been toggled back on
- if (wasShutUpConfig != null && wasShutUpConfig.disableWirelessDebugging && repository.isShutUpAttemptShizukuRestartEnabled()) {
- delay(1000)
- restartShizuku()
- }
-
- android.widget.Toast.makeText(
- context,
- context.getString(com.sameerasw.essentials.R.string.shut_up_toast_restored),
- android.widget.Toast.LENGTH_SHORT
- ).show()
-
- // Start auto-archive countdown AFTER everything is restored and Shizuku is starting
- if (autoArchivePackage != null) {
- startAutoArchiveCountdown(autoArchivePackage)
- }
- }
- }
- private fun restartShizuku() {
- val settingsRepository =
- com.sameerasw.essentials.data.repository.SettingsRepository(context)
- val token = settingsRepository.getShizukuAuthToken()
- if (token.isEmpty()) {
- Log.w("AppFlowHandler", "Shizuku auth token is missing, cannot restart Shizuku")
- return
- }
- try {
- val intent = Intent("moe.shizuku.privileged.api.START").apply {
- `package` = "moe.shizuku.privileged.api"
- putExtra("auth", token)
- addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES)
- }
- context.sendBroadcast(intent)
+ private fun isMediaPlaying(packageName: String): Boolean {
+ return try {
+ val msm = context.getSystemService(Context.MEDIA_SESSION_SERVICE) as? android.media.session.MediaSessionManager
+ val sessions = msm?.getActiveSessions(notificationListenerComponent)
+ sessions?.any {
+ it.packageName == packageName &&
+ it.playbackState?.state == android.media.session.PlaybackState.STATE_PLAYING
+ } ?: false
} catch (e: Exception) {
- Log.e("AppFlowHandler", "Failed to restart Shizuku", e)
+ false
}
}
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 ACTION_RESTORE_NOW = "com.sameerasw.essentials.ACTION_RESTORE_NOW"
- const val EXTRA_PACKAGE_NAME = "package_name"
- const val EXTRA_AUTO_ARCHIVE_PACKAGE = "auto_archive_package"
- const val NOTIFICATION_ID_SHUTUP_RESTORE = 9999
+ @Volatile
+ private var INSTANCE: AppFlowHandler? = null
+
+ fun getInstance(context: Context): AppFlowHandler {
+ return INSTANCE ?: synchronized(this) {
+ INSTANCE ?: AppFlowHandler(context.applicationContext).also { INSTANCE = it }
+ }
+ }
}
}
diff --git a/app/src/main/java/com/sameerasw/essentials/services/tiles/ScreenOffAccessibilityService.kt b/app/src/main/java/com/sameerasw/essentials/services/tiles/ScreenOffAccessibilityService.kt
index 363d44654..883fbb50e 100644
--- a/app/src/main/java/com/sameerasw/essentials/services/tiles/ScreenOffAccessibilityService.kt
+++ b/app/src/main/java/com/sameerasw/essentials/services/tiles/ScreenOffAccessibilityService.kt
@@ -224,7 +224,7 @@ class ScreenOffAccessibilityService : AccessibilityService(), SensorEventListene
flashlightHandler = FlashlightHandler(this, serviceScope)
notificationLightingHandler = NotificationLightingHandler(this)
buttonRemapHandler = ButtonRemapHandler(this, flashlightHandler)
- appFlowHandler = AppFlowHandler(this, this)
+ appFlowHandler = AppFlowHandler.getInstance(this)
ambientGlanceHandler = AmbientGlanceHandler(this)
aodForceTurnOffHandler = AodForceTurnOffHandler(this)
omniGestureOverlayHandler = OmniGestureOverlayHandler(this)
diff --git a/app/src/main/java/com/sameerasw/essentials/ui/activities/FeatureSettingsActivity.kt b/app/src/main/java/com/sameerasw/essentials/ui/activities/FeatureSettingsActivity.kt
index 8ebca97cc..606f629e1 100644
--- a/app/src/main/java/com/sameerasw/essentials/ui/activities/FeatureSettingsActivity.kt
+++ b/app/src/main/java/com/sameerasw/essentials/ui/activities/FeatureSettingsActivity.kt
@@ -234,6 +234,9 @@ class FeatureSettingsActivity : AppCompatActivity() {
val isNotificationListenerEnabled by viewModel.isNotificationListenerEnabled
val isReadPhoneStateEnabled by viewModel.isReadPhoneStateEnabled
val isShizukuPermissionGranted by viewModel.isShizukuPermissionGranted
+ val isWriteSettingsEnabled by viewModel.isWriteSettingsEnabled
+ val isUsageStatsPermissionGranted by viewModel.isUsageStatsPermissionGranted
+ val isPostNotificationsEnabled by viewModel.isPostNotificationsEnabled
var watchAdbWifiEnabled by remember {
mutableStateOf(prefs.getBoolean("watch_adb_wifi_enabled", false))
@@ -311,7 +314,10 @@ class FeatureSettingsActivity : AppCompatActivity() {
isNotificationLightingAccessibilityEnabled,
isNotificationListenerEnabled,
isReadPhoneStateEnabled,
- isShizukuPermissionGranted
+ isShizukuPermissionGranted,
+ isWriteSettingsEnabled,
+ isUsageStatsPermissionGranted,
+ isPostNotificationsEnabled
) {
val hasMissingPermissions = when (featureId) {
"Screen off widget" -> !isAccessibilityEnabled
@@ -332,6 +338,8 @@ class FeatureSettingsActivity : AppCompatActivity() {
"Screen refresh rate" -> !com.sameerasw.essentials.utils.ShellUtils.hasPermission(
context
)
+ "Shut-Up!" -> !isWriteSecureSettingsEnabled || !isWriteSettingsEnabled || !isUsageStatsPermissionGranted || !isPostNotificationsEnabled
+ "Per app refresh rate" -> (if (viewModel.isUseUsageAccess.value) !viewModel.isUsageStatsPermissionGranted.value else !isAccessibilityEnabled) || !isShizukuPermissionGranted
// 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
@@ -348,7 +356,6 @@ class FeatureSettingsActivity : AppCompatActivity() {
context
)
- "Shut-Up!" -> !isWriteSecureSettingsEnabled || !viewModel.isUsageStatsPermissionGranted.value
"Power and Battery" -> !isWriteSecureSettingsEnabled
"Networks" -> !isWriteSecureSettingsEnabled && !com.sameerasw.essentials.utils.ShellUtils.hasPermission(
context
@@ -525,7 +532,6 @@ class FeatureSettingsActivity : AppCompatActivity() {
modifier = Modifier.padding(top = 16.dp)
)
}
-
val children = FeatureRegistry.getFilteredFeatures(
context,
viewModel.isEnableUnsupportedFeatures.value
@@ -544,6 +550,7 @@ class FeatureSettingsActivity : AppCompatActivity() {
listOf(
"Text and animations",
"Screen refresh rate",
+ "Per app refresh rate",
"Navigation"
),
listOf(
@@ -687,7 +694,8 @@ class FeatureSettingsActivity : AppCompatActivity() {
context
)
- "Shut-Up!" -> !isWriteSecureSettingsEnabled || !viewModel.isUsageStatsPermissionGranted.value
+ "Shut-Up!" -> !isWriteSecureSettingsEnabled || !viewModel.isWriteSettingsEnabled.value || !viewModel.isUsageStatsPermissionGranted.value || !viewModel.isPostNotificationsEnabled.value
+ "Per app refresh rate" -> (if (viewModel.isUseUsageAccess.value) !viewModel.isUsageStatsPermissionGranted.value else !isAccessibilityEnabled) || !viewModel.isShizukuPermissionGranted.value
"Power and Battery" -> !isWriteSecureSettingsEnabled
"Networks" -> !isWriteSecureSettingsEnabled && !com.sameerasw.essentials.utils.ShellUtils.hasPermission(
context
@@ -1003,7 +1011,13 @@ class FeatureSettingsActivity : AppCompatActivity() {
highlightSetting = highlightSetting
)
}
-
+ "Shut-Up!" -> {
+ ShutUpSettingsUI(
+ viewModel = viewModel,
+ modifier = Modifier.padding(top = 16.dp),
+ highlightSetting = highlightSetting
+ )
+ }
"Always on Display" -> {
AlwaysOnDisplaySettingsUI(
viewModel = viewModel,
@@ -1043,12 +1057,11 @@ class FeatureSettingsActivity : AppCompatActivity() {
highlightSetting = highlightSetting
)
}
-
"Shut-Up!" -> {
ShutUpSettingsUI(
viewModel = viewModel,
modifier = Modifier.padding(top = 16.dp),
- highlightKey = highlightSetting
+ highlightSetting = highlightSetting
)
}
diff --git a/app/src/main/java/com/sameerasw/essentials/ui/activities/ShutUpShortcutActivity.kt b/app/src/main/java/com/sameerasw/essentials/ui/activities/ShutUpShortcutActivity.kt
index 860749caa..73a837a85 100644
--- a/app/src/main/java/com/sameerasw/essentials/ui/activities/ShutUpShortcutActivity.kt
+++ b/app/src/main/java/com/sameerasw/essentials/ui/activities/ShutUpShortcutActivity.kt
@@ -9,11 +9,10 @@
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
@@ -28,8 +27,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
@@ -86,7 +87,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,
@@ -105,146 +106,8 @@ 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"
- )
- }
-
- if (config.disableUsbDebugging || config.disableWirelessDebugging) {
- com.sameerasw.essentials.utils.ShizukuUtils.stopShizuku(this@ShutUpShortcutActivity)
- }
-
- // 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/ui/composables/SetupFeatures.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/SetupFeatures.kt
index bfa4f0fd7..8752640cc 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
@@ -88,6 +88,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.buttons.ListExpandToggleButton
@@ -403,47 +404,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) {
@@ -516,6 +477,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(
@@ -792,6 +831,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/core/cards/FeatureCard.kt b/app/src/main/java/com/sameerasw/essentials/ui/core/cards/FeatureCard.kt
index bfcf5cab2..7aa0a262f 100644
--- a/app/src/main/java/com/sameerasw/essentials/ui/core/cards/FeatureCard.kt
+++ b/app/src/main/java/com/sameerasw/essentials/ui/core/cards/FeatureCard.kt
@@ -124,10 +124,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/features/audio/ShutUpSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/features/audio/ShutUpSettingsUI.kt
index b437fdbd3..87e9a6c36 100644
--- a/app/src/main/java/com/sameerasw/essentials/ui/features/audio/ShutUpSettingsUI.kt
+++ b/app/src/main/java/com/sameerasw/essentials/ui/features/audio/ShutUpSettingsUI.kt
@@ -9,6 +9,13 @@
package com.sameerasw.essentials.ui.features.system
+import android.Manifest
+import android.content.Context
+import android.content.Intent
+import android.widget.Toast
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
@@ -18,14 +25,13 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
+import androidx.compose.material3.ToggleButton
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
@@ -38,8 +44,13 @@ import com.sameerasw.essentials.ui.core.cards.FeatureCard
import com.sameerasw.essentials.ui.core.containers.RoundedCardContainer
import com.sameerasw.essentials.ui.core.pickers.RestoreModePicker
import com.sameerasw.essentials.ui.core.sheets.AppSelectionSheet
+import com.sameerasw.essentials.ui.core.sheets.PermissionItem
+import com.sameerasw.essentials.ui.core.sheets.PermissionsBottomSheet
+import com.sameerasw.essentials.ui.core.sheets.SingleAppSelectionSheet
import com.sameerasw.essentials.ui.core.sheets.ShutUpPerAppSettingsSheet
import com.sameerasw.essentials.utils.AppUtil
+import com.sameerasw.essentials.utils.HapticUtil
+import com.sameerasw.essentials.utils.PermissionUtils
import com.sameerasw.essentials.viewmodels.MainViewModel
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@@ -47,20 +58,127 @@ import com.sameerasw.essentials.viewmodels.MainViewModel
fun ShutUpSettingsUI(
viewModel: MainViewModel,
modifier: Modifier = Modifier,
- highlightKey: String? = null
+ highlightSetting: String? = null
) {
val context = LocalContext.current
+ val view = LocalView.current
+
+ var showPermissionSheet by remember { mutableStateOf(false) }
var isAppSelectionSheetOpen by remember { mutableStateOf(false) }
- var selectedConfigForEditing by remember { mutableStateOf(null) }
+ var isEditSheetOpen by remember { mutableStateOf(false) }
+ var editingPackageName by remember { mutableStateOf("") }
+ var editingConfig by remember { mutableStateOf(null) }
+
+ // Permission states checked on composition and changes
+ var hasSecureSettings by remember { mutableStateOf(PermissionUtils.canWriteSecureSettings(context)) }
+ var hasWriteSettings by remember { mutableStateOf(PermissionUtils.canWriteSystemSettings(context)) }
+ var hasUsageStats by remember { mutableStateOf(PermissionUtils.hasUsageStatsPermission(context)) }
+ var hasNotifications by remember { mutableStateOf(PermissionUtils.isPostNotificationsEnabled(context)) }
- val configs by viewModel.shutUpConfigs
+ val updatePermissionStates = {
+ viewModel.check(context)
+ hasSecureSettings = PermissionUtils.canWriteSecureSettings(context)
+ hasWriteSettings = PermissionUtils.canWriteSystemSettings(context)
+ hasUsageStats = PermissionUtils.hasUsageStatsPermission(context)
+ hasNotifications = PermissionUtils.isPostNotificationsEnabled(context)
+ }
+
+ val lifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current
+ DisposableEffect(lifecycleOwner) {
+ val observer = androidx.lifecycle.LifecycleEventObserver { _, event ->
+ if (event == androidx.lifecycle.Lifecycle.Event.ON_RESUME) {
+ updatePermissionStates()
+ }
+ }
+ lifecycleOwner.lifecycle.addObserver(observer)
+ onDispose {
+ lifecycleOwner.lifecycle.removeObserver(observer)
+ }
+ }
+
+ LaunchedEffect(Unit) {
+ updatePermissionStates()
+ }
+
+ if (showPermissionSheet) {
+ val missingPermissions = mutableListOf().apply {
+ if (!hasSecureSettings) add("WRITE_SECURE_SETTINGS")
+ if (!hasWriteSettings) add("WRITE_SETTINGS")
+ if (!hasUsageStats) add("USAGE_STATS")
+ if (!hasNotifications) add("POST_NOTIFICATIONS")
+ }
+
+ if (missingPermissions.isNotEmpty()) {
+ PermissionsBottomSheet(
+ onDismissRequest = { showPermissionSheet = false },
+ featureTitle = R.string.feat_shut_up_title,
+ permissions = com.sameerasw.essentials.utils.PermissionUIHelper.getPermissionItems(
+ missingPermissions,
+ context,
+ viewModel,
+ context as? android.app.Activity
+ )
+ )
+ } else {
+ showPermissionSheet = false
+ }
+ }
Column(
modifier = modifier
.fillMaxWidth()
- .padding(16.dp),
- verticalArrangement = Arrangement.spacedBy(4.dp)
+ .padding(horizontal = 16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
) {
+ Text(
+ text = "Monitoring Service",
+ style = MaterialTheme.typography.titleMedium,
+ modifier = Modifier.padding(start = 16.dp),
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+
+ val onToggleShutUpService: (Boolean) -> Unit = { enabled ->
+ HapticUtil.performVirtualKeyHaptic(view)
+ if (enabled) {
+ // Recheck permissions
+ hasSecureSettings = PermissionUtils.canWriteSecureSettings(context)
+ hasWriteSettings = PermissionUtils.canWriteSystemSettings(context)
+ hasUsageStats = PermissionUtils.hasUsageStatsPermission(context)
+ hasNotifications = PermissionUtils.isPostNotificationsEnabled(context)
+
+ if (hasSecureSettings && hasWriteSettings && hasUsageStats && hasNotifications) {
+ viewModel.setShutUpServiceEnabled(true, context)
+ } else {
+ showPermissionSheet = true
+ }
+ } else {
+ viewModel.setShutUpServiceEnabled(false, context)
+ }
+ }
+
+ RoundedCardContainer(
+ modifier = Modifier,
+ spacing = 2.dp,
+ cornerRadius = 24.dp
+ ) {
+ FeatureCard(
+ title = "Enable Shut-Up! Service",
+ description = "Runs in the background and applies security rules on target app launch",
+ iconRes = R.drawable.rounded_security_24,
+ isEnabled = viewModel.isShutUpServiceEnabled.value,
+ showToggle = true,
+ hasMoreSettings = false,
+ onToggle = onToggleShutUpService,
+ onClick = { onToggleShutUpService(!viewModel.isShutUpServiceEnabled.value) }
+ )
+ }
+
+ Text(
+ text = "App Configurations",
+ style = MaterialTheme.typography.titleMedium,
+ modifier = Modifier.padding(start = 16.dp),
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
RoundedCardContainer(
modifier = Modifier,
@@ -89,113 +207,141 @@ fun ShutUpSettingsUI(
iconRes = R.drawable.rounded_app_registration_24,
isEnabled = true,
showToggle = false,
- hasMoreSettings = true,
+ hasMoreSettings = false,
onToggle = {},
- onClick = { isAppSelectionSheetOpen = true }
+ onClick = {
+ HapticUtil.performVirtualKeyHaptic(view)
+ isAppSelectionSheetOpen = true
+ }
)
}
-
- RoundedCardContainer(
- modifier = Modifier,
- spacing = 2.dp,
- cornerRadius = 24.dp
- ) {
- configs.forEach { config ->
- val appName = remember(config.packageName) {
- try {
- val appInfo =
- context.packageManager.getApplicationInfo(config.packageName, 0)
- context.packageManager.getApplicationLabel(appInfo).toString()
- } catch (e: Exception) {
- config.packageName
- }
- }
-
- val appIconPainter = remember(config.packageName) {
- try {
- val drawable = context.packageManager.getApplicationIcon(config.packageName)
- androidx.compose.ui.graphics.painter.BitmapPainter(
- AppUtil.drawableToBitmap(drawable).asImageBitmap()
- )
- } catch (e: Exception) {
- null
- }
- }
-
- FeatureCard(
- title = appName,
- description = config.packageName,
- isEnabled = true,
- onToggle = {},
- onClick = { selectedConfigForEditing = config },
- iconPainter = appIconPainter,
- showToggle = false,
- hasMoreSettings = true,
- customTrailingContent = {
- IconButton(
- onClick = {
- viewModel.createShutUpShortcut(context, config)
- }
- ) {
- Icon(
- painter = painterResource(id = R.drawable.rounded_add_24),
- contentDescription = stringResource(R.string.action_create_shortcut),
- tint = MaterialTheme.colorScheme.primary
- )
+ val configs by viewModel.shutUpConfigs
+ if (configs.isNotEmpty()) {
+ RoundedCardContainer(
+ modifier = Modifier,
+ spacing = 2.dp,
+ cornerRadius = 24.dp
+ ) {
+ configs.forEach { config ->
+ ShutUpAppItem(
+ config = config,
+ viewModel = viewModel,
+ onEditClick = { packageName, cfg ->
+ editingPackageName = packageName
+ editingConfig = cfg
+ isEditSheetOpen = true
}
- },
- additionalMenuItems = { onDismiss ->
- SegmentedDropdownMenuItem(
- text = { Text(stringResource(R.string.action_remove)) },
- onClick = {
- onDismiss()
- viewModel.removeShutUpConfig(config.packageName)
- },
- leadingIcon = {
- Icon(
- painter = painterResource(id = R.drawable.rounded_delete_24),
- contentDescription = null
- )
- }
- )
- }
- )
+ )
+ }
}
}
- Text(
- text = stringResource(R.string.shut_up_description),
- style = MaterialTheme.typography.bodyMedium,
- modifier = Modifier.padding(16.dp),
- color = MaterialTheme.colorScheme.onSurfaceVariant
- )
-
if (isAppSelectionSheetOpen) {
- AppSelectionSheet(
+ SingleAppSelectionSheet(
onDismissRequest = { isAppSelectionSheetOpen = false },
- onLoadApps = { ctx ->
- viewModel.shutUpConfigs.value.map { AppSelection(it.packageName, true) }
- },
- onSaveApps = { ctx, apps -> viewModel.saveShutUpSelectedApps(ctx, apps) }
+ onAppSelected = { app ->
+ isAppSelectionSheetOpen = false
+ editingPackageName = app.packageName
+ editingConfig = configs.find { it.packageName == app.packageName }
+ isEditSheetOpen = true
+ }
)
}
- if (selectedConfigForEditing != null) {
- val frozenApps = remember { viewModel.loadFreezeSelectedApps(context) }
- val isFrozen = remember(selectedConfigForEditing) {
- frozenApps.any { it.packageName == selectedConfigForEditing?.packageName }
+ if (isEditSheetOpen) {
+ val isFrozen = remember(editingPackageName) {
+ com.sameerasw.essentials.utils.FreezeManager.isAppFrozen(context, editingPackageName)
}
-
ShutUpPerAppSettingsSheet(
- onDismissRequest = { selectedConfigForEditing = null },
- config = configs.find { it.packageName == selectedConfigForEditing?.packageName }
- ?: selectedConfigForEditing!!,
- onConfigChanged = { viewModel.updateShutUpConfig(it) },
- onCreateShortcut = { viewModel.createShutUpShortcut(context, it) },
+ onDismissRequest = { isEditSheetOpen = false },
+ config = editingConfig ?: ShutUpAppConfig(packageName = editingPackageName),
+ onConfigChanged = { updatedConfig ->
+ viewModel.updateShutUpConfig(updatedConfig)
+ editingConfig = updatedConfig
+ },
+ onCreateShortcut = { config ->
+ viewModel.createShutUpShortcut(context, config)
+ },
isFrozen = isFrozen,
viewModel = viewModel
)
}
}
}
+
+@Composable
+private fun ShutUpAppItem(
+ config: ShutUpAppConfig,
+ viewModel: MainViewModel,
+ onEditClick: (String, ShutUpAppConfig) -> Unit
+) {
+ val context = LocalContext.current
+ val appName = remember(config.packageName) {
+ try {
+ val appInfo = context.packageManager.getApplicationInfo(config.packageName, 0)
+ context.packageManager.getApplicationLabel(appInfo).toString()
+ } catch (e: Exception) {
+ config.packageName
+ }
+ }
+
+ val appIconPainter = remember(config.packageName) {
+ try {
+ val drawable = context.packageManager.getApplicationIcon(config.packageName)
+ androidx.compose.ui.graphics.painter.BitmapPainter(
+ AppUtil.drawableToBitmap(drawable).asImageBitmap()
+ )
+ } catch (e: Exception) {
+ null
+ }
+ }
+
+ val enabledCount = config.settings.count { it.enabled }
+ val descText = "${enabledCount} settings configured" +
+ (if (config.autoArchive) " • Auto-Freeze" else "") +
+ (if (config.attemptShizukuRestart) " • Shizuku restart" else "")
+
+ FeatureCard(
+ title = appName,
+ description = descText,
+ isEnabled = config.isEnabled,
+ showToggle = true,
+ onToggle = { isChecked ->
+ viewModel.updateShutUpConfig(config.copy(isEnabled = isChecked))
+ },
+ onClick = {
+ onEditClick(config.packageName, config)
+ },
+ iconPainter = appIconPainter,
+ hasMoreSettings = true,
+ additionalMenuItems = { onDismiss ->
+ SegmentedDropdownMenuItem(
+ text = { Text("Create Shortcut") },
+ onClick = {
+ onDismiss()
+ viewModel.createShutUpShortcut(context, config)
+ },
+ leadingIcon = {
+ Icon(
+ painter = painterResource(id = R.drawable.rounded_link_24),
+ contentDescription = null
+ )
+ }
+ )
+ SegmentedDropdownMenuItem(
+ text = { Text(stringResource(R.string.action_remove)) },
+ onClick = {
+ onDismiss()
+ viewModel.removeShutUpConfig(config.packageName)
+ },
+ leadingIcon = {
+ Icon(
+ painter = painterResource(id = R.drawable.rounded_delete_24),
+ contentDescription = null
+ )
+ }
+ )
+ }
+ )
+}
diff --git a/app/src/main/java/com/sameerasw/essentials/ui/features/audio/sheets/ShutUpPerAppSettingsSheet.kt b/app/src/main/java/com/sameerasw/essentials/ui/features/audio/sheets/ShutUpPerAppSettingsSheet.kt
index f269c30ed..2de59b343 100644
--- a/app/src/main/java/com/sameerasw/essentials/ui/features/audio/sheets/ShutUpPerAppSettingsSheet.kt
+++ b/app/src/main/java/com/sameerasw/essentials/ui/features/audio/sheets/ShutUpPerAppSettingsSheet.kt
@@ -38,6 +38,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.copy
+import com.sameerasw.essentials.domain.model.disableAccessibility
+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.ui.core.cards.IconToggleItem
import com.sameerasw.essentials.ui.core.containers.RoundedCardContainer
import com.sameerasw.essentials.viewmodels.MainViewModel
@@ -57,7 +62,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(
@@ -67,7 +72,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)
}) {
@@ -140,14 +145,9 @@ fun ShutUpPerAppSettingsSheet(
title = stringResource(R.string.shut_up_attempt_shizuku_restart),
isChecked = isAttemptShizukuRestart,
onCheckedChange = {
- viewModel.setShutUpAttemptShizukuRestartEnabled(it)
- if (it && viewModel.shizukuAuthToken.value.isEmpty()) {
- android.widget.Toast.makeText(
- context,
- "Please enter the Shizuku auth token in Essentials settings",
- android.widget.Toast.LENGTH_LONG
- ).show()
- }
+ val newConfig = currentConfig.copy(attemptShizukuRestart = it)
+ currentConfig = newConfig
+ onConfigChanged(newConfig)
}
)
}
diff --git a/app/src/main/java/com/sameerasw/essentials/utils/ServiceUtils.kt b/app/src/main/java/com/sameerasw/essentials/utils/ServiceUtils.kt
index 2ce27e153..609ef94a2 100644
--- a/app/src/main/java/com/sameerasw/essentials/utils/ServiceUtils.kt
+++ b/app/src/main/java/com/sameerasw/essentials/utils/ServiceUtils.kt
@@ -34,7 +34,7 @@ object ServiceUtils {
*/
fun startRequiredServices(context: Context) {
val settingsRepository = SettingsRepository(context)
-
+ startShutUpServiceIfNeeded(context, settingsRepository)
startAppDetectionServiceIfNeeded(context, settingsRepository)
startBatteryNotificationServiceIfNeeded(context, settingsRepository)
schedulePeriodicAppUpdateCheck(context, settingsRepository)
@@ -99,6 +99,7 @@ object ServiceUtils {
}
}
+
fun schedulePeriodicAppUpdateCheck(
context: Context,
settingsRepository: SettingsRepository
@@ -123,4 +124,22 @@ 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..1390af3ff
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/utils/ShutUpManager.kt
@@ -0,0 +1,324 @@
+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.Dispatchers
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.withContext
+
+object ShutUpManager {
+ private const val TAG = "ShutUpManager"
+
+ // Every caller (shortcut, accessibility event, and foreground service) uses the same
+ // serialized setting transaction. This prevents an old restore from racing a new apply.
+ private val settingsMutex = Mutex()
+
+ 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 {
+ return safeWriteSettingInternal(context, type, key, value)
+ }
+
+ suspend fun safeWriteSettingSync(context: Context, type: String, key: String, value: String): Boolean = withContext(Dispatchers.IO) {
+ safeWriteSettingInternal(context, type, key, value)
+ }
+
+ private fun safeWriteSettingInternal(context: Context, type: String, key: String, value: String): Boolean {
+ val resolver = context.contentResolver
+ val resolverSuccess = try {
+ val result = 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, "Wrote setting via ContentResolver: [$type] $key = $value (success=$result)")
+ result
+ } 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
+ }
+
+ // Special handling for wireless debugging key: write to both global and secure tables
+ if (key == "adb_wifi_enabled") {
+ try {
+ Settings.Global.putString(resolver, key, value)
+ Settings.Secure.putString(resolver, key, value)
+ } catch (e: Exception) { }
+ }
+
+ var shellSuccess = false
+ if (ShellUtils.isAvailable(context) && ShellUtils.hasPermission(context)) {
+ try {
+ val shellType = type.lowercase()
+ ShellUtils.runCommand(context, "settings put $shellType $key $value")
+ shellSuccess = true
+ if (key == "adb_wifi_enabled") {
+ val otherType = if (shellType == "global") "secure" else "global"
+ ShellUtils.runCommand(context, "settings put $otherType $key $value")
+ }
+ Log.d(TAG, "Executed setting put via Shell: [$type] $key = $value (success=$shellSuccess)")
+ } catch (e: Exception) {
+ Log.w(TAG, "Failed to write setting via Shell: [$type] $key = $value", e)
+ }
+ }
+
+ return resolverSuccess || shellSuccess
+ }
+
+ 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,
+ repository: SettingsRepository? = null,
+ reinforcement: Boolean = false
+ ) = settingsMutex.withLock {
+ Log.d(TAG, "Applying ShutUp settings for ${config.packageName}")
+ withContext(Dispatchers.IO) {
+ val repo = repository ?: SettingsRepository(context)
+ val currentBackup = repo.getShutUpOriginalSettings()
+ val originalSettings = currentBackup.toMutableMap()
+
+ // Snapshot every original value before changing any setting. Re-enforcement never
+ // creates or changes the snapshot.
+ if (!reinforcement) {
+ 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)) {
+ originalSettings[prefixedKey] = safeReadSetting(context, resolvedType, setting.key) ?: ""
+ }
+ }
+ }
+
+ 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)) {
+ originalSettings[prefixedAccKey] = safeReadSetting(
+ context,
+ "SECURE",
+ Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES
+ ) ?: ""
+ }
+ }
+
+ if (originalSettings != currentBackup) {
+ repo.saveShutUpOriginalSettings(originalSettings)
+ }
+ }
+
+ config.settings.forEach { setting ->
+ if (setting.enabled) {
+ val resolvedType = if (setting.key == "accessibility_enabled") "SECURE" else setting.settingType
+ safeWriteSettingSync(context, resolvedType, setting.key, setting.valueOnLaunch)
+ }
+ }
+
+ // Special handling for accessibility services
+ val disableAccessibility = config.settings.any { it.key == "accessibility_enabled" && it.enabled }
+ if (disableAccessibility) {
+ safeWriteSettingSync(context, "SECURE", Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, "")
+ }
+ }
+ }
+
+ suspend fun revertShutUpSettings(context: Context, config: ShutUpAppConfig) = settingsMutex.withLock {
+ Log.d(TAG, "Reverting ShutUp settings for ${config.packageName}")
+ withContext(Dispatchers.IO) {
+ config.settings.forEach { setting ->
+ if (setting.enabled) {
+ val resolvedType = if (setting.key == "accessibility_enabled") "SECURE" else setting.settingType
+ safeWriteSettingSync(context, resolvedType, setting.key, setting.valueOnRevert)
+ }
+ }
+ }
+ }
+
+ suspend fun restoreOriginalSettings(context: Context, repository: SettingsRepository) {
+ settingsMutex.withLock {
+ val originalSettings = repository.getShutUpOriginalSettings()
+ if (originalSettings.isEmpty()) {
+ Log.d(TAG, "No original settings to restore (backup empty)")
+ return@withLock
+ }
+
+ Log.d(TAG, "Restoring original settings from backup (${originalSettings.size} entries)")
+ withContext(Dispatchers.IO) {
+ var restoreSucceeded = true
+ 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]
+ restoreSucceeded = safeWriteSettingSync(context, table, key, value) && restoreSucceeded
+ Log.d(TAG, "Restored $prefixedKey = $value")
+ } catch (e: Exception) {
+ restoreSucceeded = false
+ Log.e(TAG, "Failed to restore setting $prefixedKey", e)
+ }
+ }
+
+ if (restoreSucceeded) repository.saveShutUpOriginalSettings(emptyMap())
+ }
+
+ if (repository.getShutUpOriginalSettings().isEmpty()) 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 1500ms for developer/ADB services to stabilize before restarting Shizuku")
+ delay(1500)
+ Log.d(TAG, "Attempting Shizuku restart now")
+
+ val repository = SettingsRepository(context)
+ val savedToken = repository.getShizukuAuthToken()
+ val authTokens = if (savedToken.isNotBlank()) {
+ listOf(savedToken, "y95fuaRb9USHiIg724tvTHIs")
+ } else {
+ listOf("y95fuaRb9USHiIg724tvTHIs")
+ }
+
+ authTokens.forEach { token ->
+ // 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", token)
+ 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", token)
+ 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", token)
+ 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 shell/root is available, run Shizuku start script
+ if (ShellUtils.isAvailable(context) && ShellUtils.hasPermission(context)) {
+ Log.d(TAG, "Shell/Root is available, running Shizuku start script via Shell")
+ 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 = ShellUtils.runCommand(context, script)
+ Log.d(TAG, "Executed shell command: '$script', success: $success")
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed shell 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 bdd53d01c..c320cf190 100644
--- a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt
+++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt
@@ -48,6 +48,7 @@ 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.AppStandbyInfo
+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
@@ -141,6 +142,7 @@ class MainViewModel : ViewModel() {
val isBluetoothPermissionGranted = mutableStateOf(false)
val isUsageStatsPermissionGranted = mutableStateOf(false)
val appLanguage = mutableStateOf("en")
+ val isShutUpServiceEnabled = mutableStateOf(false)
val isBluetoothDevicesEnabled = mutableStateOf(false)
val isCallVibrationsEnabled = mutableStateOf(false)
@@ -211,9 +213,9 @@ class MainViewModel : ViewModel() {
val shutUpConfigs =
mutableStateOf>(emptyList())
val isShutUpLoading = mutableStateOf(false)
- val isShutUpAttemptShizukuRestart = mutableStateOf(true)
val shutUpRestoreDelay = mutableIntStateOf(10)
val shutUpRestoreMode = mutableStateOf("Auto")
+ val isShutUpAttemptShizukuRestart = mutableStateOf(true)
val shizukuAuthToken = mutableStateOf("")
val edgeLightingSweepSelectedShapes = mutableStateOf>(emptySet())
@@ -769,10 +771,7 @@ class MainViewModel : ViewModel() {
liveWallpaperCustomVideos.addAll(settingsRepository.getLiveWallpaperCustomVideos())
}
- SettingsRepository.KEY_SHUT_UP_ATTEMPT_SHIZUKU_RESTART -> {
- isShutUpAttemptShizukuRestart.value =
- settingsRepository.isShutUpAttemptShizukuRestartEnabled()
- }
+
SettingsRepository.KEY_SHUT_UP_RESTORE_DELAY -> {
shutUpRestoreDelay.intValue =
@@ -917,13 +916,15 @@ class MainViewModel : ViewModel() {
/**
* Updates ducking or mute configuration for a specific target package.
*
- * @param config [com.sameerasw.essentials.domain.model.ShutUpAppConfig] The updated ShutUpAppConfig object to store.
+ * @param config [ShutUpAppConfig] The updated ShutUpAppConfig object to store.
*/
- fun updateShutUpConfig(config: com.sameerasw.essentials.domain.model.ShutUpAppConfig) {
+ fun updateShutUpConfig(config: ShutUpAppConfig) {
settingsRepository.updateShutUpConfig(config)
loadShutUpConfigs()
}
+
+
/**
* Executes the remove shut up config operation.
*
@@ -985,7 +986,7 @@ class MainViewModel : ViewModel() {
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(
+ currentConfigs[it.packageName] ?: ShutUpAppConfig(
it.packageName
)
}
@@ -993,45 +994,58 @@ class MainViewModel : ViewModel() {
loadShutUpConfigs()
}
- fun createShutUpShortcut(
- context: Context,
- config: com.sameerasw.essentials.domain.model.ShutUpAppConfig
- ) {
- val appName = try {
- val appInfo = context.packageManager.getApplicationInfo(config.packageName, 0)
- context.packageManager.getApplicationLabel(appInfo).toString()
+ fun setShutUpServiceEnabled(enabled: Boolean, context: Context) {
+ isShutUpServiceEnabled.value = enabled
+ settingsRepository.setShutUpServiceEnabled(enabled)
+ val intent = Intent(context, com.sameerasw.essentials.services.ShutUpForegroundService::class.java)
+ if (enabled) {
+ androidx.core.content.ContextCompat.startForegroundService(context, intent)
+ } else {
+ context.stopService(intent)
+ }
+ }
+
+ fun createShutUpShortcut(context: Context, config: ShutUpAppConfig) {
+ if (!androidx.core.content.pm.ShortcutManagerCompat.isRequestPinShortcutSupported(context)) {
+ Toast.makeText(context, "Shortcut pinning not supported by launcher", Toast.LENGTH_SHORT).show()
+ return
+ }
+
+ val pm = context.packageManager
+ val appLabel = try {
+ val appInfo = pm.getApplicationInfo(config.packageName, 0)
+ pm.getApplicationLabel(appInfo).toString()
} catch (e: Exception) {
config.packageName
}
+ val shortLabel = "Shut-Up $appLabel"
+ val longLabel = "Launch $appLabel with Shut-Up"
- val intent =
- Intent(context, com.sameerasw.essentials.ShutUpShortcutActivity::class.java).apply {
- action = Intent.ACTION_MAIN
- putExtra("package_name", config.packageName)
- data = Uri.parse("shutup://${config.packageName}")
- }
+ val iconCompat = try {
+ val bitmap = com.sameerasw.essentials.utils.AppUtil.getShortcutIcon(context, config.packageName)
+ androidx.core.graphics.drawable.IconCompat.createWithBitmap(bitmap)
+ } catch (e: Exception) {
+ null
+ }
- if (androidx.core.content.pm.ShortcutManagerCompat.isRequestPinShortcutSupported(context)) {
- val appIcon = AppUtil.getShortcutIcon(context, config.packageName)
+ val shortcutIntent = Intent(context, com.sameerasw.essentials.ShutUpShortcutActivity::class.java).apply {
+ action = Intent.ACTION_VIEW
+ putExtra("package_name", config.packageName)
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ }
- val pinShortcutInfo =
- androidx.core.content.pm.ShortcutInfoCompat.Builder(context, config.packageName)
- .setShortLabel(appName)
- .setIcon(androidx.core.graphics.drawable.IconCompat.createWithBitmap(appIcon))
- .setIntent(intent)
- .build()
+ val shortcutInfo = androidx.core.content.pm.ShortcutInfoCompat.Builder(context, "shutup_${config.packageName}")
+ .setShortLabel(shortLabel)
+ .setLongLabel(longLabel)
+ .setIntent(shortcutIntent)
+ .apply {
+ if (iconCompat != null) {
+ setIcon(iconCompat)
+ }
+ }
+ .build()
- androidx.core.content.pm.ShortcutManagerCompat.requestPinShortcut(
- context,
- pinShortcutInfo,
- null
- )
- Toast.makeText(
- context,
- context.getString(R.string.shut_up_shortcut_created, appName),
- Toast.LENGTH_SHORT
- ).show()
- }
+ androidx.core.content.pm.ShortcutManagerCompat.requestPinShortcut(context, shortcutInfo, null)
}
/**
@@ -1091,8 +1105,7 @@ class MainViewModel : ViewModel() {
notificationLightingSystemMode.intValue =
settingsRepository.getNotificationLightingSystemMode()
- isShutUpAttemptShizukuRestart.value =
- settingsRepository.isShutUpAttemptShizukuRestartEnabled()
+
shutUpRestoreDelay.intValue =
settingsRepository.getShutUpRestoreDelay()
shutUpRestoreMode.value =
@@ -1155,6 +1168,8 @@ class MainViewModel : ViewModel() {
lockScreenClockSelectedColorId.value =
settingsRepository.getLockScreenClockSelectedColorId()
lockScreenClockSeedColor.intValue = settingsRepository.getLockScreenClockSeedColor()
+ isShutUpServiceEnabled.value = settingsRepository.isShutUpServiceEnabled()
+ isShutUpAttemptShizukuRestart.value = settingsRepository.isShutUpAttemptShizukuRestartEnabled()
loadShutUpConfigs()
recentSearches.value = settingsRepository.getRecentSearches()
loadCachedWallpaper()
@@ -6143,6 +6158,7 @@ class MainViewModel : ViewModel() {
* Executes the set pocket mode enabled operation.
*
* @param enabled [Boolean] Target enabled.
+ * @param context [Context] Target context.
*/
fun setPocketModeEnabled(enabled: Boolean) {
settingsRepository.putBoolean(SettingsRepository.KEY_POCKET_MODE_ENABLED, enabled)
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 6c699806d..332a104c4 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -962,6 +962,10 @@
Toggle Flashlight
Turn On Low Power Mode
Turn Off Low Power Mode
+ Turn On Cellular Data
+ Turn Off Cellular Data
+ Turn On Auto Brightness
+ Turn Off Auto Brightness
Dim Wallpaper
Screen Off
Media Play/Pause
@@ -980,6 +984,11 @@
Turn Off Hotspot
Toggle Hotspot
This action requires Shizuku or Root to adjust system wallpaper dimming.
+ Freeze Apps
+ Unfreeze Apps
+ This action requires Shizuku or Root to freeze specific applications.
+ This action requires Shizuku or Root to unfreeze specific applications.
+
Select Trigger
App
Automate based on open app
@@ -1928,6 +1937,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
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..13372aef5
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradlew b/gradlew
index b9bb139f7..1992bd5db 100755
--- a/gradlew
+++ b/gradlew
@@ -208,9 +208,12 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
- -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.