From bf7a3b3f75cb8f29eadf6189ccdbf744ec77683c Mon Sep 17 00:00:00 2001 From: Mudit200408 Date: Sun, 31 May 2026 18:28:31 +0530 Subject: [PATCH] shutup: Add foreground services and make it more robust --- app/src/main/AndroidManifest.xml | 29 +- .../essentials/FeatureSettingsActivity.kt | 30 +- .../essentials/ShutUpShortcutActivity.kt | 142 +------ .../data/repository/SettingsRepository.kt | 104 +++-- .../essentials/domain/model/AppSetting.kt | 10 + .../essentials/domain/model/Feature.kt | 3 +- .../domain/model/ShutUpAppConfig.kt | 64 ++- .../domain/registry/FeatureRegistry.kt | 24 +- .../domain/registry/PermissionRegistry.kt | 3 +- .../services/ShutUpForegroundService.kt | 317 +++++++++++++++ .../services/handlers/AppFlowHandler.kt | 380 ++---------------- .../ui/components/cards/FeatureCard.kt | 10 +- .../sheets/ShutUpPerAppSettingsSheet.kt | 13 +- .../ui/composables/SetupFeatures.kt | 183 +++++++-- .../composables/configs/ShutUpSettingsUI.kt | 338 +++++++++++----- .../essentials/utils/ServiceUtils.kt | 25 +- .../essentials/utils/ShutUpManager.kt | 316 +++++++++++++++ .../essentials/viewmodels/MainViewModel.kt | 116 +++--- app/src/main/res/values/strings.xml | 5 + 19 files changed, 1334 insertions(+), 778 deletions(-) create mode 100644 app/src/main/java/com/sameerasw/essentials/domain/model/AppSetting.kt create mode 100644 app/src/main/java/com/sameerasw/essentials/services/ShutUpForegroundService.kt create mode 100644 app/src/main/java/com/sameerasw/essentials/utils/ShutUpManager.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1c45524ce..040cd2a27 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -247,14 +247,6 @@ - - + + + + + + + + + + + diff --git a/app/src/main/java/com/sameerasw/essentials/FeatureSettingsActivity.kt b/app/src/main/java/com/sameerasw/essentials/FeatureSettingsActivity.kt index 2dbd2aea8..fb6451133 100644 --- a/app/src/main/java/com/sameerasw/essentials/FeatureSettingsActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/FeatureSettingsActivity.kt @@ -73,10 +73,10 @@ import com.sameerasw.essentials.ui.composables.configs.NotificationLightingSetti import com.sameerasw.essentials.ui.composables.configs.OtherCustomizationsSettingsUI import com.sameerasw.essentials.ui.composables.configs.QuickSettingsTilesSettingsUI import com.sameerasw.essentials.ui.composables.configs.RefreshRateSettingsUI +import com.sameerasw.essentials.ui.composables.configs.ShutUpSettingsUI import com.sameerasw.essentials.ui.composables.configs.RemoteLockSettingsUI import com.sameerasw.essentials.ui.composables.configs.ScreenLockedSecuritySettingsUI import com.sameerasw.essentials.ui.composables.configs.ScreenOffWidgetSettingsUI -import com.sameerasw.essentials.ui.composables.configs.ShutUpSettingsUI import com.sameerasw.essentials.ui.composables.configs.SnoozeNotificationsSettingsUI import com.sameerasw.essentials.ui.composables.configs.SoundModeTileSettingsUI import com.sameerasw.essentials.ui.composables.configs.StatusBarIconSettingsUI @@ -215,6 +215,9 @@ class FeatureSettingsActivity : AppCompatActivity() { val isNotificationListenerEnabled by viewModel.isNotificationListenerEnabled val isReadPhoneStateEnabled by viewModel.isReadPhoneStateEnabled val isShizukuPermissionGranted by viewModel.isShizukuPermissionGranted + val isWriteSettingsEnabled by viewModel.isWriteSettingsEnabled + val isUsageStatsPermissionGranted by viewModel.isUsageStatsPermissionGranted + val isPostNotificationsEnabled by viewModel.isPostNotificationsEnabled // FAB State for Notification Lighting var fabExpanded by remember { mutableStateOf(true) } @@ -246,7 +249,10 @@ class FeatureSettingsActivity : AppCompatActivity() { isNotificationLightingAccessibilityEnabled, isNotificationListenerEnabled, isReadPhoneStateEnabled, - isShizukuPermissionGranted + isShizukuPermissionGranted, + isWriteSettingsEnabled, + isUsageStatsPermissionGranted, + isPostNotificationsEnabled ) { val hasMissingPermissions = when (featureId) { "Screen off widget" -> !isAccessibilityEnabled @@ -264,6 +270,7 @@ class FeatureSettingsActivity : AppCompatActivity() { "Location reached" -> !viewModel.isLocationPermissionGranted.value || !viewModel.isBackgroundLocationPermissionGranted.value "Quick settings tiles" -> !viewModel.isWriteSettingsEnabled.value "Screen refresh rate" -> !viewModel.isShizukuPermissionGranted.value + "Shut-Up!" -> !isWriteSecureSettingsEnabled || !isWriteSettingsEnabled || !isUsageStatsPermissionGranted || !isPostNotificationsEnabled // Top level checks for other features (rarely hit if they are children, but safe to add) "Essentials On Display" -> !isAccessibilityEnabled || !isNotificationListenerEnabled "Call vibrations" -> !isReadPhoneStateEnabled || !isNotificationListenerEnabled @@ -280,7 +287,6 @@ class FeatureSettingsActivity : AppCompatActivity() { context ) - "Shut-Up!" -> !isWriteSecureSettingsEnabled || !viewModel.isUsageStatsPermissionGranted.value else -> false } if (hasMissingPermissions) { @@ -497,7 +503,7 @@ class FeatureSettingsActivity : AppCompatActivity() { "Text and animations" -> !viewModel.isWriteSettingsEnabled.value || !isWriteSecureSettingsEnabled "Lock screen clock" -> !isWriteSecureSettingsEnabled "Screen refresh rate" -> !viewModel.isShizukuPermissionGranted.value - "Shut-Up!" -> !isWriteSecureSettingsEnabled || !viewModel.isUsageStatsPermissionGranted.value + "Shut-Up!" -> !isWriteSecureSettingsEnabled || !isWriteSettingsEnabled || !isUsageStatsPermissionGranted || !isPostNotificationsEnabled else -> false } @@ -743,6 +749,14 @@ class FeatureSettingsActivity : AppCompatActivity() { ) } + "Shut-Up!" -> { + ShutUpSettingsUI( + viewModel = viewModel, + modifier = Modifier.padding(top = 16.dp), + highlightSetting = highlightSetting + ) + } + "Always on Display" -> { AlwaysOnDisplaySettingsUI( viewModel = viewModel, @@ -774,14 +788,6 @@ class FeatureSettingsActivity : AppCompatActivity() { highlightSetting = highlightSetting ) } - - "Shut-Up!" -> { - ShutUpSettingsUI( - viewModel = viewModel, - modifier = Modifier.padding(top = 16.dp), - highlightKey = highlightSetting - ) - } } } diff --git a/app/src/main/java/com/sameerasw/essentials/ShutUpShortcutActivity.kt b/app/src/main/java/com/sameerasw/essentials/ShutUpShortcutActivity.kt index 263bf0fb3..d26d70d1d 100644 --- a/app/src/main/java/com/sameerasw/essentials/ShutUpShortcutActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/ShutUpShortcutActivity.kt @@ -1,10 +1,9 @@ package com.sameerasw.essentials -import android.content.ContentResolver + import android.content.Intent import android.os.Bundle -import android.provider.Settings -import android.util.Log + import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent @@ -19,8 +18,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale import androidx.lifecycle.lifecycleScope import com.sameerasw.essentials.data.repository.SettingsRepository -import com.sameerasw.essentials.domain.model.ShutUpAppConfig + import com.sameerasw.essentials.ui.theme.EssentialsTheme + +import com.sameerasw.essentials.utils.ShutUpManager import com.sameerasw.essentials.utils.PermissionUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -77,7 +78,7 @@ class ShutUpShortcutActivity : ComponentActivity() { if (config != null && config.isEnabled) { if (PermissionUtils.canWriteSecureSettings(this@ShutUpShortcutActivity)) { - applyShutUpSettings(config, settingsRepository) + ShutUpManager.applyShutUpSettings(this@ShutUpShortcutActivity, config) withContext(Dispatchers.Main) { Toast.makeText( this@ShutUpShortcutActivity, @@ -96,137 +97,6 @@ class ShutUpShortcutActivity : ComponentActivity() { } } - private suspend fun applyShutUpSettings( - config: ShutUpAppConfig, - repository: SettingsRepository - ) { - withContext(Dispatchers.IO) { - val originalSettings = mutableMapOf() - - if (config.disableDevOptions) { - // Backup all relevant dev settings because disabling the main toggle might reset them - val secureSettings = listOf( - "anr_show_background", - "bugreport_in_power_menu", - "display_density_forced", - "mock_location", - "secure_overlay_settings", - "usb_audio_automatic_routing_disabled" - ) - val systemSettings = listOf("show_touches", "show_key_presses") - val globalSettings = listOf( - "adb_allowed_connection_time", - "adb_enabled", - "adb_wifi_enabled", - "always_finish_activities", - "animator_duration_scale", - "app_standby_enabled", - "cached_apps_freezer", - "default_install_location", - "development_settings_enabled", - "disable_window_blurs", - "enable_freeform_support", - "enable_non_resizable_multi_window", - "force_allow_on_external", - "force_desktop_mode_on_external_displays", - "force_resizable_activities", - "mobile_data_always_on", - "stay_on_while_plugged_in", - "usb_mass_storage_enabled", - "wait_for_debugger", - "wifi_display_certification_on", - "wifi_display_on", - "wifi_scan_always_enabled", - "window_animation_scale" - ) - - secureSettings.forEach { key -> - safeReadSetting(contentResolver, SettingsTable.SECURE, key) - ?.let { originalSettings["secure:$key"] = it } - } - systemSettings.forEach { key -> - safeReadSetting(contentResolver, SettingsTable.SYSTEM, key) - ?.let { originalSettings["system:$key"] = it } - } - globalSettings.forEach { key -> - safeReadSetting(contentResolver, SettingsTable.GLOBAL, key) - ?.let { originalSettings["global:$key"] = it } - } - - // Disable dev options - Settings.Global.putString( - contentResolver, - Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, - "0" - ) - } - - // Always explicitly disable USB debugging if requested, even if dev options were already disabled - // as some apps check this specific setting directly. - if (config.disableUsbDebugging) { - val current = - safeReadSetting(contentResolver, SettingsTable.GLOBAL, Settings.Global.ADB_ENABLED) - ?: "0" - if (current == "1") { - if (!originalSettings.containsKey("global:${Settings.Global.ADB_ENABLED}")) { - originalSettings["global:${Settings.Global.ADB_ENABLED}"] = "1" - } - Settings.Global.putString(contentResolver, Settings.Global.ADB_ENABLED, "0") - } - } - - if (config.disableWirelessDebugging) { - val current = - safeReadSetting(contentResolver, SettingsTable.GLOBAL, "adb_wifi_enabled") ?: "0" - if (current == "1") { - if (!originalSettings.containsKey("global:adb_wifi_enabled")) { - originalSettings["global:adb_wifi_enabled"] = "1" - } - Settings.Global.putString(contentResolver, "adb_wifi_enabled", "0") - } - } - - if (config.disableAccessibility) { - val current = safeReadSetting( - contentResolver, - SettingsTable.SECURE, - Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES - ) - if (!current.isNullOrEmpty()) { - originalSettings["secure:${Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES}"] = - current - Settings.Secure.putString( - contentResolver, - Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, - "" - ) - } - } - - if (originalSettings.isNotEmpty()) { - repository.saveShutUpOriginalSettings(originalSettings) - } - } - } - - private enum class SettingsTable { SYSTEM, SECURE, GLOBAL } - - // Android 12+ throws SecurityException reading @hide settings that aren't - // @Readable (e.g. show_key_presses). WRITE_SECURE_SETTINGS doesn't cover reads. - private fun safeReadSetting( - resolver: ContentResolver, - table: SettingsTable, - key: String - ): String? = try { - when (table) { - SettingsTable.SYSTEM -> Settings.System.getString(resolver, key) - SettingsTable.SECURE -> Settings.Secure.getString(resolver, key) - SettingsTable.GLOBAL -> Settings.Global.getString(resolver, key) - } - } catch (e: SecurityException) { - Log.w("ShutUpShortcut", "Skipping unreadable setting $table:$key", e) - null - } private fun launchApp(packageName: String) { val intent = packageManager.getLaunchIntentForPackage(packageName) diff --git a/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt b/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt index debe3a5f9..6ce2cbcde 100644 --- a/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt +++ b/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt @@ -13,6 +13,8 @@ import com.sameerasw.essentials.domain.model.NotificationLightingSweepPosition import com.sameerasw.essentials.domain.model.ScaleAnimationsProfile import com.sameerasw.essentials.domain.model.TrackedRepo import com.sameerasw.essentials.domain.model.github.GitHubUser +import com.sameerasw.essentials.domain.model.ShutUpAppConfig + import com.sameerasw.essentials.utils.RootUtils import com.sameerasw.essentials.utils.ShizukuUtils import kotlinx.coroutines.channels.awaitClose @@ -23,7 +25,7 @@ class SettingsRepository(private val context: Context) { private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) - private val gson = Gson() + private val gson = com.google.gson.GsonBuilder().create() init { migrateUsageAccessKey() @@ -233,10 +235,10 @@ class SettingsRepository(private val context: Context) { const val LIVE_WALLPAPER_TRIGGER_UNLOCK = "unlock" const val LIVE_WALLPAPER_TRIGGER_SCREEN_ON = "screen_on" + const val KEY_DISABLE_ROTATION_SUGGESTION = "disable_rotation_suggestion" + const val KEY_SHUT_UP_SELECTED_APPS = "shut_up_selected_apps" const val KEY_SHUT_UP_ORIGINAL_SETTINGS = "shut_up_original_settings" - const val KEY_SHUT_UP_ATTEMPT_SHIZUKU_RESTART = "shut_up_attempt_shizuku_restart" - const val KEY_DISABLE_ROTATION_SUGGESTION = "disable_rotation_suggestion" const val KEY_LOCK_SCREEN_CLOCK_WEIGHT = "lock_screen_clock_weight" const val KEY_LOCK_SCREEN_CLOCK_WIDTH = "lock_screen_clock_width" @@ -246,6 +248,8 @@ class SettingsRepository(private val context: Context) { const val KEY_LOCK_SCREEN_CLOCK_SELECTED_COLOR_ID = "lock_screen_clock_selected_color_id" const val KEY_LOCK_SCREEN_CLOCK_SEED_COLOR = "lock_screen_clock_seed_color" const val KEY_RECENT_SEARCHES = "recent_searches" + + const val KEY_SHUT_UP_SERVICE_ENABLED = "shutup_service_enabled" } // Observe changes @@ -505,52 +509,7 @@ class SettingsRepository(private val context: Context) { fun updateNotificationGlanceAppSelection(packageName: String, enabled: Boolean) = updateAppSelection(KEY_NOTIFICATION_GLANCE_SELECTED_APPS, packageName, enabled) - fun loadShutUpConfigs(): List { - val json = prefs.getString(KEY_SHUT_UP_SELECTED_APPS, null) - return if (json != null) { - try { - gson.fromJson( - json, - Array::class.java - ).toList() - } catch (e: Exception) { - emptyList() - } - } else { - emptyList() - } - } - - fun saveShutUpConfigs(configs: List) { - val json = gson.toJson(configs) - putString(KEY_SHUT_UP_SELECTED_APPS, json) - } - - fun updateShutUpConfig(config: com.sameerasw.essentials.domain.model.ShutUpAppConfig) { - val current = loadShutUpConfigs().toMutableList() - val index = current.indexOfFirst { it.packageName == config.packageName } - if (index != -1) { - current[index] = config - } else { - current.add(config) - } - saveShutUpConfigs(current) - } - fun saveShutUpOriginalSettings(settings: Map) { - val json = gson.toJson(settings) - putString(KEY_SHUT_UP_ORIGINAL_SETTINGS, json) - } - - fun getShutUpOriginalSettings(): Map { - val json = prefs.getString(KEY_SHUT_UP_ORIGINAL_SETTINGS, null) ?: return emptyMap() - return try { - @Suppress("UNCHECKED_CAST") - gson.fromJson(json, Map::class.java) as Map - } catch (e: Exception) { - emptyMap() - } - } private fun updateAppSelection(key: String, packageName: String, enabled: Boolean) { val current = loadAppSelection(key).toMutableList() @@ -854,11 +813,6 @@ class SettingsRepository(private val context: Context) { saveTrackedRepos(current) } - fun isShutUpAttemptShizukuRestartEnabled(): Boolean = - getBoolean(KEY_SHUT_UP_ATTEMPT_SHIZUKU_RESTART, true) - - fun setShutUpAttemptShizukuRestartEnabled(enabled: Boolean) = - putBoolean(KEY_SHUT_UP_ATTEMPT_SHIZUKU_RESTART, enabled) fun removeTrackedRepo(fullName: String) { val current = getTrackedRepos().toMutableList() @@ -1453,5 +1407,49 @@ class SettingsRepository(private val context: Context) { fun getLockScreenClockSeedColor(): Int = getInt(KEY_LOCK_SCREEN_CLOCK_SEED_COLOR, 0) fun setLockScreenClockSeedColor(value: Int) = putInt(KEY_LOCK_SCREEN_CLOCK_SEED_COLOR, value) + + fun loadShutUpConfigs(): List { + val json = prefs.getString(KEY_SHUT_UP_SELECTED_APPS, null) + return if (json != null) { + try { + gson.fromJson( + json, + Array::class.java + ).toList() + } catch (e: Exception) { + emptyList() + } + } else { + emptyList() + } + } + + fun saveShutUpConfigs(configs: List) { + val json = gson.toJson(configs) + putString(KEY_SHUT_UP_SELECTED_APPS, json) + } + + fun isShutUpServiceEnabled(): Boolean { + return prefs.getBoolean(KEY_SHUT_UP_SERVICE_ENABLED, false) + } + + fun setShutUpServiceEnabled(enabled: Boolean) { + putBoolean(KEY_SHUT_UP_SERVICE_ENABLED, enabled) + } + + fun saveShutUpOriginalSettings(settings: Map) { + val json = gson.toJson(settings) + putString(KEY_SHUT_UP_ORIGINAL_SETTINGS, json) + } + + fun getShutUpOriginalSettings(): Map { + val json = prefs.getString(KEY_SHUT_UP_ORIGINAL_SETTINGS, null) ?: return emptyMap() + return try { + @Suppress("UNCHECKED_CAST") + gson.fromJson(json, Map::class.java) as Map + } catch (e: Exception) { + emptyMap() + } + } } diff --git a/app/src/main/java/com/sameerasw/essentials/domain/model/AppSetting.kt b/app/src/main/java/com/sameerasw/essentials/domain/model/AppSetting.kt new file mode 100644 index 000000000..62ae8eee5 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/domain/model/AppSetting.kt @@ -0,0 +1,10 @@ +package com.sameerasw.essentials.domain.model + +data class AppSetting( + val enabled: Boolean = true, + val settingType: String, // "GLOBAL", "SECURE", "SYSTEM" + val key: String, + val valueOnLaunch: String, + val valueOnRevert: String, + val label: String +) diff --git a/app/src/main/java/com/sameerasw/essentials/domain/model/Feature.kt b/app/src/main/java/com/sameerasw/essentials/domain/model/Feature.kt index 1db1cb1e4..47ced8867 100644 --- a/app/src/main/java/com/sameerasw/essentials/domain/model/Feature.kt +++ b/app/src/main/java/com/sameerasw/essentials/domain/model/Feature.kt @@ -39,7 +39,8 @@ abstract class Feature( @StringRes val aboutDescription: Int? = null, @androidx.annotation.RawRes val animationRes: Int = 0 ) { - val requiresAuth: Boolean = category == com.sameerasw.essentials.R.string.cat_protection + open val requiresAuth: Boolean + get() = category == com.sameerasw.essentials.R.string.cat_protection abstract fun isEnabled(viewModel: MainViewModel): Boolean diff --git a/app/src/main/java/com/sameerasw/essentials/domain/model/ShutUpAppConfig.kt b/app/src/main/java/com/sameerasw/essentials/domain/model/ShutUpAppConfig.kt index a9a0551b1..27e019e63 100644 --- a/app/src/main/java/com/sameerasw/essentials/domain/model/ShutUpAppConfig.kt +++ b/app/src/main/java/com/sameerasw/essentials/domain/model/ShutUpAppConfig.kt @@ -3,9 +3,65 @@ package com.sameerasw.essentials.domain.model data class ShutUpAppConfig( val packageName: String, val isEnabled: Boolean = true, - val disableDevOptions: Boolean = true, - val disableUsbDebugging: Boolean = true, - val disableWirelessDebugging: Boolean = true, - val disableAccessibility: Boolean = false, + val settings: List = emptyList(), + val attemptShizukuRestart: Boolean = false, val autoArchive: Boolean = false ) + +val ShutUpAppConfig.disableDevOptions: Boolean + get() = settings.any { it.key == "development_settings_enabled" && it.enabled } + +val ShutUpAppConfig.disableUsbDebugging: Boolean + get() = settings.any { it.key == "adb_enabled" && it.enabled } + +val ShutUpAppConfig.disableWirelessDebugging: Boolean + get() = settings.any { it.key == "adb_wifi_enabled" && it.enabled } + +val ShutUpAppConfig.disableAccessibility: Boolean + get() = settings.any { it.key == "accessibility_enabled" && it.enabled } + +fun ShutUpAppConfig.copy( + packageName: String = this.packageName, + isEnabled: Boolean = this.isEnabled, + attemptShizukuRestart: Boolean = this.attemptShizukuRestart, + autoArchive: Boolean = this.autoArchive, + disableDevOptions: Boolean = this.disableDevOptions, + disableUsbDebugging: Boolean = this.disableUsbDebugging, + disableWirelessDebugging: Boolean = this.disableWirelessDebugging, + disableAccessibility: Boolean = this.disableAccessibility +): ShutUpAppConfig { + val newList = settings.toMutableList() + + fun updateKey(key: String, label: String, enabled: Boolean) { + val existing = newList.find { it.key == key } + if (existing != null) { + newList[newList.indexOf(existing)] = existing.copy(enabled = enabled) + } else if (enabled) { + val type = if (key == "accessibility_enabled") "SECURE" else "GLOBAL" + newList.add( + AppSetting( + label = label, + settingType = type, + key = key, + valueOnLaunch = "0", + valueOnRevert = "1", + enabled = true + ) + ) + } + } + + updateKey("development_settings_enabled", "Hide Developer Options", disableDevOptions) + updateKey("adb_enabled", "Hide USB Debugging", disableUsbDebugging) + updateKey("adb_wifi_enabled", "Hide Wireless Debugging", disableWirelessDebugging) + updateKey("accessibility_enabled", "Hide Accessibility Services", disableAccessibility) + + return ShutUpAppConfig( + packageName = packageName, + isEnabled = isEnabled, + settings = newList, + attemptShizukuRestart = attemptShizukuRestart, + autoArchive = autoArchive + ) +} + diff --git a/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt b/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt index 47dd20bfd..70ee12e8f 100644 --- a/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt +++ b/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt @@ -857,24 +857,36 @@ object FeatureRegistry { override fun onToggle(viewModel: MainViewModel, context: Context, enabled: Boolean) = viewModel.setAppLockEnabled(enabled, context) }, + object : Feature( id = "Shut-Up!", title = R.string.feat_shut_up_title, - iconRes = R.drawable.rounded_domino_mask_24, - category = R.string.cat_system, + iconRes = R.drawable.rounded_shield_lock_24, + category = R.string.cat_protection, description = R.string.feat_shut_up_desc, aboutDescription = R.string.shut_up_description, - permissionKeys = listOf("WRITE_SECURE_SETTINGS", "USAGE_STATS"), + permissionKeys = listOf("WRITE_SECURE_SETTINGS", "WRITE_SETTINGS", "USAGE_STATS", "POST_NOTIFICATIONS"), showToggle = false, hasMoreSettings = true, - isBeta = true, parentFeatureId = "Security", animationRes = R.raw.shutup_animation ) { - override fun isEnabled(viewModel: MainViewModel) = true - override fun onToggle(viewModel: MainViewModel, context: Context, enabled: Boolean) {} + override val requiresAuth: Boolean = false + + override fun isEnabled(viewModel: MainViewModel) = + viewModel.isShutUpServiceEnabled.value + + override fun isToggleEnabled(viewModel: MainViewModel, context: Context) = + viewModel.isWriteSecureSettingsEnabled.value && + viewModel.isWriteSettingsEnabled.value && + viewModel.isUsageStatsPermissionGranted.value && + viewModel.isPostNotificationsEnabled.value + + override fun onToggle(viewModel: MainViewModel, context: Context, enabled: Boolean) = + viewModel.setShutUpServiceEnabled(enabled, context) }, + object : Feature( id = "Location reached", title = R.string.feat_location_reached_title, diff --git a/app/src/main/java/com/sameerasw/essentials/domain/registry/PermissionRegistry.kt b/app/src/main/java/com/sameerasw/essentials/domain/registry/PermissionRegistry.kt index 15c9ce40f..ecf5fb3f2 100644 --- a/app/src/main/java/com/sameerasw/essentials/domain/registry/PermissionRegistry.kt +++ b/app/src/main/java/com/sameerasw/essentials/domain/registry/PermissionRegistry.kt @@ -93,8 +93,9 @@ fun initPermissionRegistry() { // Default browser permission PermissionRegistry.register("DEFAULT_BROWSER", R.string.feat_link_actions_title) - // Shut-Up! feature + // Shut-Up! permissions PermissionRegistry.register("WRITE_SECURE_SETTINGS", R.string.feat_shut_up_title) PermissionRegistry.register("WRITE_SETTINGS", R.string.feat_shut_up_title) PermissionRegistry.register("USAGE_STATS", R.string.feat_shut_up_title) + PermissionRegistry.register("POST_NOTIFICATIONS", R.string.feat_shut_up_title) } diff --git a/app/src/main/java/com/sameerasw/essentials/services/ShutUpForegroundService.kt b/app/src/main/java/com/sameerasw/essentials/services/ShutUpForegroundService.kt new file mode 100644 index 000000000..52acfb062 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/services/ShutUpForegroundService.kt @@ -0,0 +1,317 @@ +package com.sameerasw.essentials.services + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.app.usage.UsageEvents +import android.app.usage.UsageStatsManager +import android.content.Context +import android.content.Intent +import android.content.pm.ServiceInfo +import android.os.Build +import android.os.IBinder +import android.util.Log +import androidx.core.app.NotificationCompat +import com.sameerasw.essentials.R +import com.sameerasw.essentials.data.repository.SettingsRepository +import com.sameerasw.essentials.domain.model.ShutUpAppConfig +import com.sameerasw.essentials.utils.FreezeManager +import com.sameerasw.essentials.utils.ShutUpManager +import kotlinx.coroutines.* + +class ShutUpForegroundService : Service() { + + private val serviceScope = CoroutineScope(Dispatchers.Default + SupervisorJob()) + private lateinit var settingsRepository: SettingsRepository + private var monitorJob: Job? = null + private var lastPackageName: String? = null + private var lastQueryTime = System.currentTimeMillis() - 5000 + + private var pendingRestoreJob: Job? = null + private var pendingRestorePackage: String? = null + private var freezeCountdownJob: Job? = null + + // Active config for the currently monitored target app (used for periodic re-enforcement) + private var activeTargetConfig: com.sameerasw.essentials.domain.model.ShutUpAppConfig? = null + private var enforceTickCount = 0 + + companion object { + private const val TAG = "ShutUpForegroundService" + private const val CHANNEL_ID = "shutup_service_channel" + private const val NOTIFICATION_ID = 1002 + private const val NOTIFICATION_FREEZE_ID = 1003 + + const val ACTION_STOP_SERVICE = "ACTION_STOP_SERVICE" + const val ACTION_FREEZE_NOW = "ACTION_FREEZE_NOW" + const val ACTION_ABORT_FREEZE = "ACTION_ABORT_FREEZE" + const val EXTRA_PACKAGE_NAME = "package_name" + + var isRunning = false + } + + override fun onCreate() { + super.onCreate() + isRunning = true + settingsRepository = SettingsRepository(this) + createNotificationChannel() + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + when (intent?.action) { + ACTION_STOP_SERVICE -> { + stopForeground(true) + stopSelf() + return START_NOT_STICKY + } + ACTION_FREEZE_NOW -> { + val pkg = intent.getStringExtra(EXTRA_PACKAGE_NAME) + if (pkg != null) { + freezeCountdownJob?.cancel() + val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + notificationManager.cancel(NOTIFICATION_FREEZE_ID) + serviceScope.launch { + FreezeManager.freezeApp(this@ShutUpForegroundService, pkg) + } + } + return START_STICKY + } + ACTION_ABORT_FREEZE -> { + freezeCountdownJob?.cancel() + val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + notificationManager.cancel(NOTIFICATION_FREEZE_ID) + return START_STICKY + } + } + + startForeground( + NOTIFICATION_ID, + createServiceNotification(), + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE else 0 + ) + + startMonitoring() + return START_STICKY + } + + private fun startMonitoring() { + if (monitorJob != null) return + monitorJob = serviceScope.launch { + while (isActive) { + val now = System.currentTimeMillis() + val currentPkg = getForegroundPackage(lastQueryTime, now) + if (currentPkg != null) { + lastQueryTime = now + if (currentPkg != lastPackageName) { + onPackageChanged(lastPackageName, currentPkg) + lastPackageName = currentPkg + enforceTickCount = 0 + } else { + // Re-enforce settings every ~2s while target app stays in foreground + // This ensures settings stay hidden even if something re-enables them between opens + enforceTickCount++ + if (enforceTickCount % 5 == 0) { + activeTargetConfig?.let { config -> + Log.d(TAG, "Re-enforcing ShutUp settings for ${config.packageName}") + ShutUpManager.applyShutUpSettings(this@ShutUpForegroundService, config) + } + } + } + } else { + lastQueryTime = now - 500 + } + delay(400) + } + } + } + + private fun getForegroundPackage(startTime: Long, endTime: Long): String? { + val usageStatsManager = getSystemService(USAGE_STATS_SERVICE) as UsageStatsManager + try { + val events = usageStatsManager.queryEvents(startTime, endTime) + val event = UsageEvents.Event() + var lastResumedPackage: String? = null + while (events.hasNextEvent()) { + events.getNextEvent(event) + if (event.eventType == UsageEvents.Event.ACTIVITY_RESUMED) { + lastResumedPackage = event.packageName + } + } + if (lastResumedPackage != null) { + return lastResumedPackage + } + } catch (e: Exception) { + Log.e(TAG, "Failed to query usage events", e) + } + return null + } + + private suspend fun onPackageChanged(oldPkg: String?, newPkg: String?) { + if (newPkg == null || ShutUpManager.isPackageIgnored(newPkg)) return + + val configs = settingsRepository.loadShutUpConfigs() + + // 1. Leaving a Shut-Up app + if (oldPkg != null && configs.any { it.packageName == oldPkg && it.isEnabled }) { + if (newPkg != oldPkg) { + activeTargetConfig = null + pendingRestoreJob?.cancel() + pendingRestorePackage = oldPkg + pendingRestoreJob = serviceScope.launch { + var shouldContinue = true + while (shouldContinue) { + delay(3000) // 3 seconds debounce / check interval + val config = settingsRepository.loadShutUpConfigs().find { it.packageName == oldPkg } + if (config != null && config.isEnabled) { + if (!ShutUpManager.isAppRunning(this@ShutUpForegroundService, oldPkg)) { + if (activeTargetConfig != null) { + // A different Shut-Up app is now active; skip restoration to avoid + // re-enabling settings (USB debugging, dev options, etc.) while it is running + Log.d(TAG, "Skipping restore for $oldPkg — another Shut-Up app is active (${activeTargetConfig?.packageName})") + shouldContinue = false + } else { + ShutUpManager.revertShutUpSettings(this@ShutUpForegroundService, config) + ShutUpManager.restoreOriginalSettings(this@ShutUpForegroundService, settingsRepository) + if (config.attemptShizukuRestart) { + ShutUpManager.restartShizuku(this@ShutUpForegroundService) + } + if (config.autoArchive) { + showAutoFreezeNotification(config.packageName) + } + shouldContinue = false + } + } else { + Log.d(TAG, "$oldPkg is still running in the background, delaying revert settings...") + } + } else { + shouldContinue = false + } + } + pendingRestorePackage = null + } + } + } + + // 2. Entering a Shut-Up app + val newConfig = configs.find { it.packageName == newPkg } + if (newConfig != null && newConfig.isEnabled) { + activeTargetConfig = newConfig + if (pendingRestorePackage == newPkg) { + pendingRestoreJob?.cancel() + pendingRestorePackage = null + } + freezeCountdownJob?.cancel() + val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + notificationManager.cancel(NOTIFICATION_FREEZE_ID) + + // Apply inline — no extra coroutine spawn, runs directly in monitoring coroutine + ShutUpManager.applyShutUpSettings(this@ShutUpForegroundService, newConfig) + } else { + activeTargetConfig = null + } + } + + private fun showAutoFreezeNotification(packageName: String) { + freezeCountdownJob?.cancel() + val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + val appName = try { + val appInfo = packageManager.getApplicationInfo(packageName, 0) + packageManager.getApplicationLabel(appInfo).toString() + } catch (e: Exception) { + packageName + } + + freezeCountdownJob = serviceScope.launch { + var secondsRemaining = 5 + while (secondsRemaining > 0) { + val stopIntent = Intent(this@ShutUpForegroundService, ShutUpForegroundService::class.java).apply { + action = ACTION_FREEZE_NOW + putExtra(EXTRA_PACKAGE_NAME, packageName) + } + val stopPendingIntent = PendingIntent.getService( + this@ShutUpForegroundService, + 101, + stopIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + val abortIntent = Intent(this@ShutUpForegroundService, ShutUpForegroundService::class.java).apply { + action = ACTION_ABORT_FREEZE + } + val abortPendingIntent = PendingIntent.getService( + this@ShutUpForegroundService, + 102, + abortIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + val notification = NotificationCompat.Builder(this@ShutUpForegroundService, CHANNEL_ID) + .setContentTitle(getString(R.string.shut_up_auto_archive_notif_title)) + .setContentText(getString(R.string.shut_up_auto_archive_notif_text, appName, secondsRemaining)) + .setSmallIcon(R.drawable.rounded_snowflake_24) + .setOngoing(true) + .addAction(R.drawable.rounded_snowflake_24, getString(R.string.shut_up_auto_archive_action_freeze), stopPendingIntent) + .addAction(R.drawable.rounded_close_24, getString(R.string.shut_up_auto_archive_action_abort), abortPendingIntent) + .build() + + notificationManager.notify(NOTIFICATION_FREEZE_ID, notification) + delay(1000) + secondsRemaining-- + } + + FreezeManager.freezeApp(this@ShutUpForegroundService, packageName) + notificationManager.cancel(NOTIFICATION_FREEZE_ID) + } + } + + override fun onDestroy() { + isRunning = false + monitorJob?.cancel() + pendingRestoreJob?.cancel() + freezeCountdownJob?.cancel() + serviceScope.cancel() + + val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + notificationManager.cancel(NOTIFICATION_FREEZE_ID) + + super.onDestroy() + } + + override fun onBind(intent: Intent?): IBinder? = null + + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel( + CHANNEL_ID, + getString(R.string.shut_up_service_name), + NotificationManager.IMPORTANCE_LOW + ).apply { + description = getString(R.string.shut_up_service_desc) + } + val manager = getSystemService(NotificationManager::class.java) + manager.createNotificationChannel(channel) + } + } + + private fun createServiceNotification(): Notification { + val stopIntent = Intent(this, ShutUpForegroundService::class.java).apply { + action = ACTION_STOP_SERVICE + } + val stopPendingIntent = PendingIntent.getService( + this, + 201, + stopIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + return NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle(getString(R.string.shut_up_service_notification_title)) + .setContentText(getString(R.string.shut_up_service_notification_desc)) + .setSmallIcon(R.drawable.rounded_shield_lock_24) + .setOngoing(true) + .addAction(R.drawable.rounded_close_24, getString(R.string.action_stop), stopPendingIntent) + .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE) + .build() + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/services/handlers/AppFlowHandler.kt b/app/src/main/java/com/sameerasw/essentials/services/handlers/AppFlowHandler.kt index f554d622f..a27d913b1 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/handlers/AppFlowHandler.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/handlers/AppFlowHandler.kt @@ -2,25 +2,25 @@ package com.sameerasw.essentials.services.handlers import android.accessibilityservice.AccessibilityService import android.app.NotificationManager -import android.app.PendingIntent -import android.content.BroadcastReceiver + import android.content.ComponentName import android.content.Context import android.content.Intent -import android.content.IntentFilter import android.content.pm.PackageManager import android.os.Handler import android.os.Looper import android.provider.Settings import android.util.Log -import androidx.core.app.NotificationCompat + import com.google.gson.Gson import com.sameerasw.essentials.domain.diy.Automation import com.sameerasw.essentials.domain.diy.DIYRepository import com.sameerasw.essentials.domain.model.AppSelection import com.sameerasw.essentials.services.automation.executors.CombinedActionExecutor -import com.sameerasw.essentials.utils.FreezeManager + import com.sameerasw.essentials.utils.StatusBarManager +import com.sameerasw.essentials.utils.ShutUpManager +import com.sameerasw.essentials.domain.model.ShutUpAppConfig import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -33,43 +33,11 @@ class AppFlowHandler( private val service: AccessibilityService? = null ) { private val handler = Handler(Looper.getMainLooper()) - private val scope = CoroutineScope(Dispatchers.Main) + private val scope = CoroutineScope(Dispatchers.Main.immediate) private val authenticatedPackages = mutableSetOf() private val lastLeaveTimes = mutableMapOf() - private val activeCountdowns = mutableMapOf() - - private val shutUpReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - val packageName = intent?.getStringExtra(EXTRA_PACKAGE_NAME) ?: return - when (intent.action) { - ACTION_FREEZE_NOW -> { - activeCountdowns[packageName]?.cancel() - activeCountdowns.remove(packageName) - context?.let { FreezeManager.freezeApp(it, packageName) } - cancelNotification(packageName) - } - ACTION_ABORT_FREEZE -> { - activeCountdowns[packageName]?.cancel() - activeCountdowns.remove(packageName) - cancelNotification(packageName) - } - } - } - } - - init { - val filter = IntentFilter().apply { - addAction(ACTION_FREEZE_NOW) - addAction(ACTION_ABORT_FREEZE) - } - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) { - context.registerReceiver(shutUpReceiver, filter, Context.RECEIVER_EXPORTED) - } else { - context.registerReceiver(shutUpReceiver, filter) - } - } // App Lock State private var lockingPackage: String? = null @@ -96,6 +64,8 @@ class AppFlowHandler( val prefs = context.getSharedPreferences("essentials_prefs", Context.MODE_PRIVATE) val useUsageAccess = prefs.getBoolean("use_usage_access", false) + Log.d("AppFlowHandler", "onPackageChanged: packageName=$packageName, isFromUsageStats=$isFromUsageStats, useUsageAccess=$useUsageAccess, currentPackage=$currentPackage") + val oldPackage = currentPackage currentPackage = packageName @@ -108,11 +78,12 @@ class AppFlowHandler( } if (isFromUsageStats == useUsageAccess) { + Log.d("AppFlowHandler", "onPackageChanged: Processing package change because isFromUsageStats matches useUsageAccess") checkAppLock(packageName) checkHighlightNightLight(packageName) checkAppAutomations(packageName) checkGestureBarAutomation(packageName) - checkShutUpRestore(oldPackage, packageName) + checkShutUp(packageName) } } @@ -128,6 +99,26 @@ class AppFlowHandler( authenticatedPackages.clear() } + private fun checkShutUp(packageName: String) { + val prefs = context.getSharedPreferences("essentials_prefs", Context.MODE_PRIVATE) + val serviceEnabled = prefs.getBoolean("shutup_service_enabled", false) + if (!serviceEnabled) return + + val json = prefs.getString("shut_up_selected_apps", null) ?: return + val configs: List = try { + Gson().fromJson(json, Array::class.java).toList() + } catch (_: Exception) { + return + } + + val config = configs.find { it.packageName == packageName && it.isEnabled } ?: return + + scope.launch(Dispatchers.IO) { + Log.d("AppFlowHandler", "checkShutUp: Immediately applying ShutUp settings for $packageName via accessibility event") + ShutUpManager.applyShutUpSettings(context, config) + } + } + private fun checkAppLock(packageName: String) { val prefs = context.getSharedPreferences("essentials_prefs", Context.MODE_PRIVATE) val isEnabled = prefs.getBoolean("app_lock_enabled", false) @@ -362,220 +353,7 @@ class AppFlowHandler( return launchers.any { it.activityInfo.packageName == packageName } } - private fun checkShutUpRestore(oldPackage: String?, newPackage: String?) { - Log.d("AppFlowHandler", "checkShutUpRestore: old=$oldPackage, new=$newPackage") - if (oldPackage == null || oldPackage == newPackage) return - - val settingsRepository = - com.sameerasw.essentials.data.repository.SettingsRepository(context) - val shutUpConfigs = settingsRepository.loadShutUpConfigs() - - val wasShutUpConfig = shutUpConfigs.find { it.packageName == oldPackage && it.isEnabled } - - // Check if it was already frozen to avoid duplicate triggers (e.g. on screen off) - val isAlreadyFrozen = oldPackage.let { FreezeManager.isAppFrozen(context, it) } - - // We consider the new app a Shut-Up app if it's in the list OR if it's the shortcut activity - val isNewAppShutUp = shutUpConfigs.any { it.packageName == newPackage && it.isEnabled } || - newPackage == "com.sameerasw.essentials.ShutUpShortcutActivity" - - Log.d( - "AppFlowHandler", - "checkShutUpRestore: wasShutUpConfig=${wasShutUpConfig != null}, isNewAppShutUp=$isNewAppShutUp, isAlreadyFrozen=$isAlreadyFrozen" - ) - - // If it's already frozen, we've already handled it - if (isAlreadyFrozen) return - - // If we are entering a Shut-Up app, cancel ANY pending countdowns for other apps - if (isNewAppShutUp) { - if (activeCountdowns.isNotEmpty()) { - Log.d( - "AppFlowHandler", - "checkShutUpRestore: Entering Shut-Up app, cancelling all pending countdowns" - ) - activeCountdowns.values.forEach { it.cancel() } - activeCountdowns.keys.forEach { cancelNotification(it) } - activeCountdowns.clear() - } - } - - if (wasShutUpConfig != null && !isNewAppShutUp) { - Log.d("AppFlowHandler", "checkShutUpRestore: Triggering restoration for $oldPackage") - restoreShutUpSettings( - settingsRepository, - if (wasShutUpConfig.autoArchive) wasShutUpConfig.packageName else null - ) - } - } - - private fun startAutoArchiveCountdown(packageName: String) { - Log.d("AppFlowHandler", "startAutoArchiveCountdown: $packageName") - // Cancel existing countdown for this app if any - activeCountdowns[packageName]?.cancel() - val appName = try { - val appInfo = context.packageManager.getApplicationInfo(packageName, 0) - context.packageManager.getApplicationLabel(appInfo).toString() - } catch (e: Exception) { - Log.e("AppFlowHandler", "Failed to get app name for $packageName", e) - packageName - } - - val job = scope.launch { - Log.d("AppFlowHandler", "Countdown job started for $packageName") - for (i in 10 downTo 1) { - Log.d("AppFlowHandler", "Countdown for $packageName: $i") - showCountdownNotification(packageName, appName, i) - delay(1000) - } - // countdown finished - Log.d("AppFlowHandler", "Countdown finished for $packageName, freezing...") - val success = withContext(Dispatchers.IO) { - FreezeManager.freezeApp(context, packageName) - } - Log.d("AppFlowHandler", "Freeze result for $packageName: $success") - cancelNotification(packageName) - activeCountdowns.remove(packageName) - } - activeCountdowns[packageName] = job - } - - private fun showCountdownNotification(packageName: String, appName: String, secondsLeft: Int) { - createNotificationChannel() - val notificationManager = - context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - - val freezeIntent = Intent(ACTION_FREEZE_NOW).apply { - `package` = context.packageName - putExtra(EXTRA_PACKAGE_NAME, packageName) - } - val freezePendingIntent = PendingIntent.getBroadcast( - context, - packageName.hashCode() + 1, - freezeIntent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - - val abortIntent = Intent(ACTION_ABORT_FREEZE).apply { - `package` = context.packageName - putExtra(EXTRA_PACKAGE_NAME, packageName) - } - val abortPendingIntent = PendingIntent.getBroadcast( - context, - packageName.hashCode() + 2, - abortIntent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - - val title = - context.getString(com.sameerasw.essentials.R.string.shut_up_auto_archive_notif_title) - val text = context.getString( - com.sameerasw.essentials.R.string.shut_up_auto_archive_notif_text, - appName, - secondsLeft - ) - val criticalText = secondsLeft.toString() - - val notification = - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { - val builder = android.app.Notification.Builder(context, "shutup_alerts_channel") - .setSmallIcon(com.sameerasw.essentials.R.drawable.rounded_snowflake_24) - .setContentTitle(title) - .setContentText(text) - .setOngoing(true) - .setOnlyAlertOnce(true) - .setCategory(android.app.Notification.CATEGORY_SERVICE) - .setShowWhen(false) - .setGroup("shutup_auto_archive") - .setColorized(false) - - if (android.os.Build.VERSION.SDK_INT >= 31) { - builder.setForegroundServiceBehavior(android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE) - } - - builder.addAction( - android.app.Notification.Action.Builder( - android.graphics.drawable.Icon.createWithResource( - context, - com.sameerasw.essentials.R.drawable.rounded_snowflake_24 - ), - context.getString(com.sameerasw.essentials.R.string.shut_up_auto_archive_action_freeze), - freezePendingIntent - ).build() - ) - builder.addAction( - android.app.Notification.Action.Builder( - android.graphics.drawable.Icon.createWithResource( - context, - com.sameerasw.essentials.R.drawable.rounded_close_24 - ), - context.getString(com.sameerasw.essentials.R.string.shut_up_auto_archive_action_abort), - abortPendingIntent - ).build() - ) - - // Live Update Status Chip - try { - val setRequestPromotedOngoing = builder.javaClass.getMethod( - "setRequestPromotedOngoing", - Boolean::class.javaPrimitiveType - ) - setRequestPromotedOngoing.invoke(builder, true) - - val setShortCriticalText = builder.javaClass.getMethod( - "setShortCriticalText", - CharSequence::class.java - ) - setShortCriticalText.invoke(builder, criticalText) - } catch (_: Throwable) { - } - - val extras = android.os.Bundle() - extras.putBoolean("android.requestPromotedOngoing", true) - extras.putString("android.shortCriticalText", criticalText) - builder.addExtras(extras) - - builder.setProgress(10, secondsLeft, false) - - builder.build() - } else { - NotificationCompat.Builder(context, "shutup_alerts_channel") - .setSmallIcon(com.sameerasw.essentials.R.drawable.rounded_snowflake_24) - .setContentTitle(title) - .setContentText(text) - .setPriority(NotificationCompat.PRIORITY_MAX) - .setCategory(NotificationCompat.CATEGORY_SERVICE) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .setOnlyAlertOnce(true) - .setOngoing(true) - .setProgress(10, secondsLeft, false) - .addAction( - com.sameerasw.essentials.R.drawable.rounded_snowflake_24, - context.getString(com.sameerasw.essentials.R.string.shut_up_auto_archive_action_freeze), - freezePendingIntent - ) - .addAction( - com.sameerasw.essentials.R.drawable.rounded_close_24, - context.getString(com.sameerasw.essentials.R.string.shut_up_auto_archive_action_abort), - abortPendingIntent - ) - .addExtras(android.os.Bundle().apply { - putBoolean("android.requestPromotedOngoing", true) - putString("android.shortCriticalText", criticalText) - }) - .build() - } - - Log.d("AppFlowHandler", "Showing notification for $packageName, secondsLeft=$secondsLeft") - notificationManager.notify(packageName.hashCode(), notification) - } - - private fun cancelNotification(packageName: String) { - val notificationManager = - context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - notificationManager.cancel(packageName.hashCode()) - } private fun createNotificationChannel() { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { @@ -590,109 +368,11 @@ class AppFlowHandler( description = "Channel for app detection alerts" } notificationManager.createNotificationChannel(channel) - - val alertChannel = android.app.NotificationChannel( - "shutup_alerts_channel", - "Shut-Up! Alerts", - NotificationManager.IMPORTANCE_MAX - ).apply { - description = "Live update notifications for auto archiving" - enableVibration(false) - setSound(null, null) - lockscreenVisibility = android.app.Notification.VISIBILITY_PUBLIC - } - notificationManager.createNotificationChannel(alertChannel) } } - private fun restoreShutUpSettings( - repository: com.sameerasw.essentials.data.repository.SettingsRepository, - autoArchivePackage: String? = null - ) { - val originalSettings = repository.getShutUpOriginalSettings() - if (originalSettings.isEmpty()) { - if (autoArchivePackage != null) { - startAutoArchiveCountdown(autoArchivePackage) - } - return - } - scope.launch { - // Delay to ensure the app has fully settled before restoring system settings - delay(2000) - - val canWriteSecure = - com.sameerasw.essentials.utils.PermissionUtils.canWriteSecureSettings(context) - val canWriteSystem = Settings.System.canWrite(context) - - originalSettings.forEach { (prefixedKey, value) -> - try { - val parts = prefixedKey.split(":", limit = 2) - if (parts.size < 2) return@forEach - - val table = parts[0] - val key = parts[1] - - when (table) { - "global" -> { - if (canWriteSecure) { - Settings.Global.putString(context.contentResolver, key, value) - } - } - - "secure" -> { - if (canWriteSecure) { - Settings.Secure.putString(context.contentResolver, key, value) - } - } - - "system" -> { - if (canWriteSystem) { - Settings.System.putString(context.contentResolver, key, value) - } - } - } - } catch (e: Exception) { - Log.e("AppFlowHandler", "Failed to restore setting $prefixedKey", e) - } - } - - // Clear original settings after restoration - repository.saveShutUpOriginalSettings(emptyMap()) - - // Wait a bit and Restart Shizuku as ADB might have been toggled back on - delay(1000) - restartShizuku() - - android.widget.Toast.makeText( - context, - context.getString(com.sameerasw.essentials.R.string.shut_up_toast_restored), - android.widget.Toast.LENGTH_SHORT - ).show() - - // Start auto-archive countdown AFTER everything is restored and Shizuku is starting - if (autoArchivePackage != null) { - startAutoArchiveCountdown(autoArchivePackage) - } - } - } - - private fun restartShizuku() { - try { - val intent = Intent("moe.shizuku.privileged.api.START").apply { - `package` = "moe.shizuku.privileged.api" - putExtra("auth", "y95fuaRb9USHiIg724tvTHIs") - addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES) - } - context.sendBroadcast(intent) - } catch (e: Exception) { - Log.e("AppFlowHandler", "Failed to restart Shizuku", e) - } - } companion object { - const val ACTION_FREEZE_NOW = "com.sameerasw.essentials.ACTION_FREEZE_NOW" - const val ACTION_ABORT_FREEZE = "com.sameerasw.essentials.ACTION_ABORT_FREEZE" - const val EXTRA_PACKAGE_NAME = "package_name" } } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/cards/FeatureCard.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/cards/FeatureCard.kt index 159f215d2..dcbd6d79b 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/components/cards/FeatureCard.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/cards/FeatureCard.kt @@ -106,10 +106,12 @@ fun FeatureCard( HapticUtil.performVirtualKeyHaptic(view) onClick() }, - onLongClick = { - HapticUtil.performVirtualKeyHaptic(view) - showMenu = true - }, + onLongClick = if (onPinToggle != null || onHelpClick != null || additionalMenuItems != null) { + { + HapticUtil.performVirtualKeyHaptic(view) + showMenu = true + } + } else null, verticalAlignment = Alignment.CenterVertically, modifier = modifier .alpha(alpha) diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/ShutUpPerAppSettingsSheet.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/ShutUpPerAppSettingsSheet.kt index 5d858091f..3cf0eb18a 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/ShutUpPerAppSettingsSheet.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/ShutUpPerAppSettingsSheet.kt @@ -30,6 +30,11 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.sameerasw.essentials.R import com.sameerasw.essentials.domain.model.ShutUpAppConfig +import com.sameerasw.essentials.domain.model.disableDevOptions +import com.sameerasw.essentials.domain.model.disableUsbDebugging +import com.sameerasw.essentials.domain.model.disableWirelessDebugging +import com.sameerasw.essentials.domain.model.disableAccessibility +import com.sameerasw.essentials.domain.model.copy import com.sameerasw.essentials.ui.components.cards.IconToggleItem import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer import com.sameerasw.essentials.viewmodels.MainViewModel @@ -48,7 +53,7 @@ fun ShutUpPerAppSettingsSheet( var currentConfig by remember(config) { mutableStateOf(config) } var showShizukuRestartWarning by remember { mutableStateOf(false) } - val isAttemptShizukuRestart by viewModel.isShutUpAttemptShizukuRestart + val isAttemptShizukuRestart = currentConfig.attemptShizukuRestart if (showShizukuRestartWarning) { AlertDialog( @@ -58,7 +63,7 @@ fun ShutUpPerAppSettingsSheet( confirmButton = { TextButton(onClick = { showShizukuRestartWarning = false - val newConfig = currentConfig.copy(autoArchive = true) + val newConfig = currentConfig.copy(autoArchive = true, attemptShizukuRestart = true) currentConfig = newConfig onConfigChanged(newConfig) }) { @@ -132,7 +137,9 @@ fun ShutUpPerAppSettingsSheet( title = stringResource(R.string.shut_up_attempt_shizuku_restart), isChecked = isAttemptShizukuRestart, onCheckedChange = { - viewModel.setShutUpAttemptShizukuRestartEnabled(it) + val newConfig = currentConfig.copy(attemptShizukuRestart = it) + currentConfig = newConfig + onConfigChanged(newConfig) } ) } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/SetupFeatures.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/SetupFeatures.kt index bb363cb86..de371c75e 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/composables/SetupFeatures.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/SetupFeatures.kt @@ -77,6 +77,7 @@ import com.sameerasw.essentials.FeatureSettingsActivity import com.sameerasw.essentials.R import com.sameerasw.essentials.domain.registry.FeatureRegistry import com.sameerasw.essentials.domain.registry.PermissionRegistry +import com.sameerasw.essentials.utils.PermissionUtils import com.sameerasw.essentials.ui.activities.YourAndroidActivity import com.sameerasw.essentials.ui.components.FavoriteCarousel import com.sameerasw.essentials.ui.components.cards.FeatureCard @@ -383,47 +384,7 @@ fun SetupFeatures( } } - R.string.feat_shut_up_title -> { - if (!isWriteSecureSettingsEnabled) { - missing.add( - PermissionItem( - iconRes = R.drawable.rounded_security_24, - title = R.string.perm_write_secure_title, - description = R.string.perm_write_secure_desc_common, - dependentFeatures = PermissionRegistry.getFeatures("WRITE_SECURE_SETTINGS"), - actionLabel = R.string.perm_action_grant, - action = { viewModel.requestWriteSecureSettingsPermission(context) }, - isGranted = isWriteSecureSettingsEnabled - ) - ) - } - if (!isWriteSettingsEnabled) { - missing.add( - PermissionItem( - iconRes = R.drawable.rounded_settings_24, - title = R.string.perm_write_settings_title, - description = R.string.perm_write_settings_desc, - dependentFeatures = PermissionRegistry.getFeatures("WRITE_SETTINGS"), - actionLabel = R.string.perm_action_grant, - action = { viewModel.requestWriteSettingsPermission(context) }, - isGranted = isWriteSettingsEnabled - ) - ) - } - if (!viewModel.isUsageStatsPermissionGranted.value) { - missing.add( - PermissionItem( - iconRes = R.drawable.rounded_app_registration_24, - title = R.string.perm_usage_stats_title, - description = R.string.perm_usage_stats_desc, - dependentFeatures = PermissionRegistry.getFeatures("USAGE_STATS"), - actionLabel = R.string.perm_action_grant, - action = { viewModel.requestUsageStatsPermission(context) }, - isGranted = viewModel.isUsageStatsPermissionGranted.value - ) - ) - } - } + R.string.feat_screen_locked_security_title -> { if (isRootEnabled) { @@ -496,6 +457,84 @@ fun SetupFeatures( } } + R.string.feat_shut_up_title -> { + if (!isWriteSecureSettingsEnabled) { + missing.add( + PermissionItem( + iconRes = R.drawable.rounded_security_24, + title = R.string.perm_write_secure_title, + description = R.string.perm_write_secure_desc_common, + dependentFeatures = PermissionRegistry.getFeatures("WRITE_SECURE_SETTINGS"), + actionLabel = R.string.perm_action_copy_adb, + action = { + val adbCommand = + "adb shell pm grant com.sameerasw.essentials android.permission.WRITE_SECURE_SETTINGS" + val clipboard = + context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData.newPlainText("adb_command", adbCommand) + clipboard.setPrimaryClip(clip) + }, + secondaryActionLabel = R.string.perm_action_check, + secondaryAction = { + viewModel.isWriteSecureSettingsEnabled.value = + PermissionUtils.canWriteSecureSettings(context) + }, + isGranted = isWriteSecureSettingsEnabled + ) + ) + } + if (!isWriteSettingsEnabled) { + missing.add( + PermissionItem( + iconRes = R.drawable.rounded_settings_24, + title = R.string.perm_write_settings_title, + description = R.string.perm_write_settings_desc, + dependentFeatures = PermissionRegistry.getFeatures("WRITE_SETTINGS"), + actionLabel = R.string.perm_action_enable, + action = { + PermissionUtils.openWriteSettings(context) + }, + isGranted = isWriteSettingsEnabled + ) + ) + } + if (!viewModel.isUsageStatsPermissionGranted.value) { + missing.add( + PermissionItem( + iconRes = R.drawable.rounded_data_usage_24, + title = R.string.perm_usage_stats_title, + description = R.string.perm_usage_stats_desc, + dependentFeatures = PermissionRegistry.getFeatures("USAGE_STATS"), + actionLabel = R.string.perm_action_grant, + action = { + com.sameerasw.essentials.utils.PermissionUtils.openUsageStatsSettings(context) + }, + isGranted = viewModel.isUsageStatsPermissionGranted.value + ) + ) + } + if (!viewModel.isPostNotificationsEnabled.value) { + missing.add( + PermissionItem( + iconRes = R.drawable.rounded_notifications_unread_24, + title = R.string.permission_post_notifications_title, + description = R.string.permission_post_notifications_desc, + dependentFeatures = PermissionRegistry.getFeatures("POST_NOTIFICATIONS"), + actionLabel = R.string.perm_action_grant, + action = { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) { + (context as? Activity)?.requestPermissions( + arrayOf(android.Manifest.permission.POST_NOTIFICATIONS), + 1 + ) + } + }, + isGranted = viewModel.isPostNotificationsEnabled.value + ) + ) + } + } + R.string.feat_call_vibrations_title -> { if (!viewModel.isReadPhoneStateEnabled.value) { missing.add( @@ -772,6 +811,68 @@ fun SetupFeatures( ) ) + R.string.feat_shut_up_title -> listOf( + PermissionItem( + iconRes = R.drawable.rounded_security_24, + title = R.string.perm_write_secure_title, + description = R.string.perm_write_secure_desc_common, + dependentFeatures = PermissionRegistry.getFeatures("WRITE_SECURE_SETTINGS"), + actionLabel = R.string.perm_action_copy_adb, + action = { + val adbCommand = + "adb shell pm grant com.sameerasw.essentials android.permission.WRITE_SECURE_SETTINGS" + val clipboard = + context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData.newPlainText("adb_command", adbCommand) + clipboard.setPrimaryClip(clip) + }, + secondaryActionLabel = R.string.perm_action_check, + secondaryAction = { + viewModel.isWriteSecureSettingsEnabled.value = + PermissionUtils.canWriteSecureSettings(context) + }, + isGranted = isWriteSecureSettingsEnabled + ), + PermissionItem( + iconRes = R.drawable.rounded_settings_24, + title = R.string.perm_write_settings_title, + description = R.string.perm_write_settings_desc, + dependentFeatures = PermissionRegistry.getFeatures("WRITE_SETTINGS"), + actionLabel = R.string.perm_action_enable, + action = { + PermissionUtils.openWriteSettings(context) + }, + isGranted = isWriteSettingsEnabled + ), + PermissionItem( + iconRes = R.drawable.rounded_data_usage_24, + title = R.string.perm_usage_stats_title, + description = R.string.perm_usage_stats_desc, + dependentFeatures = PermissionRegistry.getFeatures("USAGE_STATS"), + actionLabel = R.string.perm_action_grant, + action = { + com.sameerasw.essentials.utils.PermissionUtils.openUsageStatsSettings(context) + }, + isGranted = viewModel.isUsageStatsPermissionGranted.value + ), + PermissionItem( + iconRes = R.drawable.rounded_notifications_unread_24, + title = R.string.permission_post_notifications_title, + description = R.string.permission_post_notifications_desc, + dependentFeatures = PermissionRegistry.getFeatures("POST_NOTIFICATIONS"), + actionLabel = R.string.perm_action_grant, + action = { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) { + (context as? Activity)?.requestPermissions( + arrayOf(android.Manifest.permission.POST_NOTIFICATIONS), + 1 + ) + } + }, + isGranted = viewModel.isPostNotificationsEnabled.value + ) + ) + R.string.feat_call_vibrations_title -> listOf( PermissionItem( iconRes = R.drawable.rounded_mobile_24, diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/ShutUpSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/ShutUpSettingsUI.kt index cc22146e9..4b9bc7561 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/ShutUpSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/ShutUpSettingsUI.kt @@ -1,53 +1,157 @@ package com.sameerasw.essentials.ui.composables.configs +import android.Manifest +import android.content.Context +import android.content.Intent +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.sameerasw.essentials.R -import com.sameerasw.essentials.domain.model.AppSelection import com.sameerasw.essentials.domain.model.ShutUpAppConfig import com.sameerasw.essentials.ui.components.cards.FeatureCard import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer import com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenuItem -import com.sameerasw.essentials.ui.components.sheets.AppSelectionSheet +import com.sameerasw.essentials.ui.components.sheets.PermissionItem +import com.sameerasw.essentials.ui.components.sheets.PermissionsBottomSheet import com.sameerasw.essentials.ui.components.sheets.ShutUpPerAppSettingsSheet +import com.sameerasw.essentials.ui.components.sheets.SingleAppSelectionSheet import com.sameerasw.essentials.utils.AppUtil +import com.sameerasw.essentials.utils.HapticUtil +import com.sameerasw.essentials.utils.PermissionUtils import com.sameerasw.essentials.viewmodels.MainViewModel @Composable fun ShutUpSettingsUI( viewModel: MainViewModel, modifier: Modifier = Modifier, - highlightKey: String? = null + highlightSetting: String? = null ) { val context = LocalContext.current + val view = LocalView.current + + var showPermissionSheet by remember { mutableStateOf(false) } var isAppSelectionSheetOpen by remember { mutableStateOf(false) } - var selectedConfigForEditing by remember { mutableStateOf(null) } + var isEditSheetOpen by remember { mutableStateOf(false) } + var editingPackageName by remember { mutableStateOf("") } + var editingConfig by remember { mutableStateOf(null) } + + // Permission states checked on composition and changes + var hasSecureSettings by remember { mutableStateOf(PermissionUtils.canWriteSecureSettings(context)) } + var hasWriteSettings by remember { mutableStateOf(PermissionUtils.canWriteSystemSettings(context)) } + var hasUsageStats by remember { mutableStateOf(PermissionUtils.hasUsageStatsPermission(context)) } + var hasNotifications by remember { mutableStateOf(PermissionUtils.isPostNotificationsEnabled(context)) } + + val hasAllPermissions = hasSecureSettings && hasWriteSettings && hasUsageStats && hasNotifications + + // System Permission Launcher for Post Notifications + val requestNotificationLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted -> + hasNotifications = isGranted + } + + LaunchedEffect(Unit) { + hasSecureSettings = PermissionUtils.canWriteSecureSettings(context) + hasWriteSettings = PermissionUtils.canWriteSystemSettings(context) + hasUsageStats = PermissionUtils.hasUsageStatsPermission(context) + hasNotifications = PermissionUtils.isPostNotificationsEnabled(context) + } - val configs by viewModel.shutUpConfigs + if (showPermissionSheet) { + PermissionsBottomSheet( + onDismissRequest = { showPermissionSheet = false }, + featureTitle = R.string.feat_shut_up_title, + permissions = listOf( + PermissionItem( + iconRes = R.drawable.rounded_admin_panel_settings_24, + title = "Write Secure Settings", + description = "Required to toggle USB and Wireless Debugging settings", + isGranted = hasSecureSettings, + action = { + // Secure settings cannot be requested directly by activity intent. + // We show a toast instructions. + Toast.makeText(context, "Requires ADB or Root to grant WRITE_SECURE_SETTINGS", Toast.LENGTH_LONG).show() + } + ), + PermissionItem( + iconRes = R.drawable.rounded_settings_24, + title = "Write System Settings", + description = "Required to toggle standard settings", + isGranted = hasWriteSettings, + action = { + PermissionUtils.openWriteSettings(context) + } + ), + PermissionItem( + iconRes = R.drawable.rounded_data_usage_24, + title = "Usage Access", + description = "Required to detect when target apps are launched or exited", + isGranted = hasUsageStats, + action = { + com.sameerasw.essentials.utils.PermissionUtils.openUsageStatsSettings(context) + } + ), + PermissionItem( + iconRes = R.drawable.rounded_notifications_unread_24, + title = "Post Notifications", + description = "Required to display background service status and auto-freeze countdowns", + isGranted = hasNotifications, + action = { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) { + requestNotificationLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + } + } + ) + ) + ) + } Column( modifier = modifier .fillMaxWidth() - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(4.dp) + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) ) { + Text( + text = "Monitoring Service", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(start = 16.dp), + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + val onToggleShutUpService: (Boolean) -> Unit = { enabled -> + HapticUtil.performVirtualKeyHaptic(view) + if (enabled) { + // Recheck permissions + hasSecureSettings = PermissionUtils.canWriteSecureSettings(context) + hasWriteSettings = PermissionUtils.canWriteSystemSettings(context) + hasUsageStats = PermissionUtils.hasUsageStatsPermission(context) + hasNotifications = PermissionUtils.isPostNotificationsEnabled(context) + + if (hasSecureSettings && hasWriteSettings && hasUsageStats && hasNotifications) { + viewModel.setShutUpServiceEnabled(true, context) + } else { + showPermissionSheet = true + } + } else { + viewModel.setShutUpServiceEnabled(false, context) + } + } RoundedCardContainer( modifier = Modifier, @@ -55,115 +159,151 @@ fun ShutUpSettingsUI( cornerRadius = 24.dp ) { FeatureCard( - title = stringResource(R.string.shut_up_select_apps_title), - description = stringResource(R.string.shut_up_select_apps_desc), - iconRes = R.drawable.rounded_app_registration_24, - isEnabled = true, - showToggle = false, - hasMoreSettings = true, - onToggle = {}, - onClick = { isAppSelectionSheetOpen = true } + title = "Enable Shut-Up! Service", + description = "Runs in the background and applies security rules on target app launch", + iconRes = R.drawable.rounded_security_24, + isEnabled = viewModel.isShutUpServiceEnabled.value, + showToggle = true, + hasMoreSettings = false, + onToggle = onToggleShutUpService, + onClick = { onToggleShutUpService(!viewModel.isShutUpServiceEnabled.value) } ) } + Text( + text = "App Configurations", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(start = 16.dp), + color = MaterialTheme.colorScheme.onSurfaceVariant + ) RoundedCardContainer( modifier = Modifier, spacing = 2.dp, cornerRadius = 24.dp ) { - configs.forEach { config -> - val appName = remember(config.packageName) { - try { - val appInfo = - context.packageManager.getApplicationInfo(config.packageName, 0) - context.packageManager.getApplicationLabel(appInfo).toString() - } catch (e: Exception) { - config.packageName - } + FeatureCard( + title = stringResource(R.string.shut_up_select_apps_title), + description = stringResource(R.string.shut_up_select_apps_desc), + iconRes = R.drawable.rounded_app_registration_24, + isEnabled = true, + showToggle = false, + hasMoreSettings = false, + onToggle = {}, + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + isAppSelectionSheetOpen = true } + ) + } - val appIconPainter = remember(config.packageName) { - try { - val drawable = context.packageManager.getApplicationIcon(config.packageName) - androidx.compose.ui.graphics.painter.BitmapPainter( - AppUtil.drawableToBitmap(drawable).asImageBitmap() - ) - } catch (e: Exception) { - null + val configs by viewModel.shutUpConfigs + if (configs.isNotEmpty()) { + RoundedCardContainer( + modifier = Modifier, + spacing = 2.dp, + cornerRadius = 24.dp + ) { + configs.forEach { config -> + val appName = remember(config.packageName) { + try { + val appInfo = context.packageManager.getApplicationInfo(config.packageName, 0) + context.packageManager.getApplicationLabel(appInfo).toString() + } catch (e: Exception) { + config.packageName + } } - } - FeatureCard( - title = appName, - description = config.packageName, - isEnabled = true, - onToggle = {}, - onClick = { selectedConfigForEditing = config }, - iconPainter = appIconPainter, - showToggle = false, - hasMoreSettings = true, - customTrailingContent = { - IconButton( - onClick = { - viewModel.createShutUpShortcut(context, config) - } - ) { - Icon( - painter = painterResource(id = R.drawable.rounded_add_24), - contentDescription = stringResource(R.string.action_create_shortcut), - tint = MaterialTheme.colorScheme.primary + val appIconPainter = remember(config.packageName) { + try { + val drawable = context.packageManager.getApplicationIcon(config.packageName) + androidx.compose.ui.graphics.painter.BitmapPainter( + AppUtil.drawableToBitmap(drawable).asImageBitmap() ) + } catch (e: Exception) { + null } - }, - additionalMenuItems = { onDismiss -> - SegmentedDropdownMenuItem( - text = { Text(stringResource(R.string.action_remove)) }, - onClick = { - onDismiss() - viewModel.removeShutUpConfig(config.packageName) - }, - leadingIcon = { - Icon( - painter = painterResource(id = R.drawable.rounded_delete_24), - contentDescription = null - ) - } - ) } - ) + + val enabledCount = config.settings.count { it.enabled } + val descText = "${enabledCount} settings configured" + + (if (config.autoArchive) " • Auto-Freeze" else "") + + (if (config.attemptShizukuRestart) " • Shizuku restart" else "") + + FeatureCard( + title = appName, + description = descText, + isEnabled = config.isEnabled, + showToggle = true, + onToggle = { isChecked -> + viewModel.updateShutUpConfig(config.copy(isEnabled = isChecked)) + }, + onClick = { + editingPackageName = config.packageName + editingConfig = config + isEditSheetOpen = true + }, + iconPainter = appIconPainter, + hasMoreSettings = true, + additionalMenuItems = { onDismiss -> + SegmentedDropdownMenuItem( + text = { Text("Create Shortcut") }, + onClick = { + onDismiss() + viewModel.createShutUpShortcut(context, config) + }, + leadingIcon = { + Icon( + painter = painterResource(id = R.drawable.rounded_link_24), + contentDescription = null + ) + } + ) + SegmentedDropdownMenuItem( + text = { Text(stringResource(R.string.action_remove)) }, + onClick = { + onDismiss() + viewModel.removeShutUpConfig(config.packageName) + }, + leadingIcon = { + Icon( + painter = painterResource(id = R.drawable.rounded_delete_24), + contentDescription = null + ) + } + ) + } + ) + } } } - Text( - text = stringResource(R.string.shut_up_description), - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(16.dp), - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - if (isAppSelectionSheetOpen) { - AppSelectionSheet( + SingleAppSelectionSheet( onDismissRequest = { isAppSelectionSheetOpen = false }, - onLoadApps = { ctx -> - viewModel.shutUpConfigs.value.map { AppSelection(it.packageName, true) } - }, - onSaveApps = { ctx, apps -> viewModel.saveShutUpSelectedApps(ctx, apps) } + onAppSelected = { app -> + isAppSelectionSheetOpen = false + editingPackageName = app.packageName + editingConfig = configs.find { it.packageName == app.packageName } + isEditSheetOpen = true + } ) } - if (selectedConfigForEditing != null) { - val frozenApps = remember { viewModel.loadFreezeSelectedApps(context) } - val isFrozen = remember(selectedConfigForEditing) { - frozenApps.any { it.packageName == selectedConfigForEditing?.packageName } + if (isEditSheetOpen) { + val isFrozen = remember(editingPackageName) { + com.sameerasw.essentials.utils.FreezeManager.isAppFrozen(context, editingPackageName) } - ShutUpPerAppSettingsSheet( - onDismissRequest = { selectedConfigForEditing = null }, - config = configs.find { it.packageName == selectedConfigForEditing?.packageName } - ?: selectedConfigForEditing!!, - onConfigChanged = { viewModel.updateShutUpConfig(it) }, - onCreateShortcut = { viewModel.createShutUpShortcut(context, it) }, + onDismissRequest = { isEditSheetOpen = false }, + config = editingConfig ?: ShutUpAppConfig(packageName = editingPackageName), + onConfigChanged = { updatedConfig -> + viewModel.updateShutUpConfig(updatedConfig) + editingConfig = updatedConfig + }, + onCreateShortcut = { config -> + viewModel.createShutUpShortcut(context, config) + }, isFrozen = isFrozen, viewModel = viewModel ) diff --git a/app/src/main/java/com/sameerasw/essentials/utils/ServiceUtils.kt b/app/src/main/java/com/sameerasw/essentials/utils/ServiceUtils.kt index 088f7375a..efb7208a1 100644 --- a/app/src/main/java/com/sameerasw/essentials/utils/ServiceUtils.kt +++ b/app/src/main/java/com/sameerasw/essentials/utils/ServiceUtils.kt @@ -16,6 +16,7 @@ object ServiceUtils { startAppDetectionServiceIfNeeded(context, settingsRepository) startBatteryNotificationServiceIfNeeded(context, settingsRepository) + startShutUpServiceIfNeeded(context, settingsRepository) } private fun startAppDetectionServiceIfNeeded( @@ -35,11 +36,8 @@ object ServiceUtils { it.isEnabled && it.type == Automation.Type.APP } - val shutUpConfigs = settingsRepository.loadShutUpConfigs() - val hasShutUpApps = shutUpConfigs.any { it.isEnabled } - val shouldRun = - isUseUsageAccess && (isAppLockEnabled || isDynamicNightLightEnabled || isHideGestureBarOnLauncherEnabled || hasAppAutomations || hasShutUpApps) + isUseUsageAccess && (isAppLockEnabled || isDynamicNightLightEnabled || isHideGestureBarOnLauncherEnabled || hasAppAutomations) val intent = Intent(context, AppDetectionService::class.java) if (shouldRun) { @@ -76,4 +74,23 @@ object ServiceUtils { } } } + + private fun startShutUpServiceIfNeeded( + context: Context, + settingsRepository: SettingsRepository + ) { + val isShutUpEnabled = settingsRepository.isShutUpServiceEnabled() + val intent = Intent(context, com.sameerasw.essentials.services.ShutUpForegroundService::class.java) + if (isShutUpEnabled) { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.startForegroundService(intent) + } else { + context.startService(intent) + } + } catch (e: Exception) { + e.printStackTrace() + } + } + } } diff --git a/app/src/main/java/com/sameerasw/essentials/utils/ShutUpManager.kt b/app/src/main/java/com/sameerasw/essentials/utils/ShutUpManager.kt new file mode 100644 index 000000000..57f58237a --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/utils/ShutUpManager.kt @@ -0,0 +1,316 @@ +package com.sameerasw.essentials.utils + +import android.content.Context +import android.content.Intent +import android.provider.Settings +import android.util.Log +import android.widget.Toast +import com.sameerasw.essentials.R +import com.sameerasw.essentials.data.repository.SettingsRepository +import com.sameerasw.essentials.domain.model.ShutUpAppConfig +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.util.concurrent.CopyOnWriteArrayList + +object ShutUpManager { + private const val TAG = "ShutUpManager" + + // Shared scope for async shell commands. Kept separate so we can cancel pending jobs. + private val shellScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + private val pendingShellJobs = CopyOnWriteArrayList() + + private fun cancelPendingShellJobs() { + pendingShellJobs.forEach { it.cancel() } + pendingShellJobs.clear() + Log.d(TAG, "Cancelled all pending shell jobs") + } + + private val ignoredSystemPackages = listOf( + "android", + "com.android.systemui", + "com.google.android.inputmethod.latin", + "com.google.android.gms" + ) + + fun isPackageIgnored(packageName: String): Boolean { + return ignoredSystemPackages.contains(packageName) || + packageName.startsWith("com.android.inputmethod") || + packageName.startsWith("com.google.android.inputmethod") || + packageName.contains("autofill") + } + + fun isAppRunning(context: Context, packageName: String): Boolean { + if (ShellUtils.isAvailable(context) && ShellUtils.hasPermission(context)) { + try { + val output = ShellUtils.runCommandWithOutput(context, "pidof $packageName") + if (!output.isNullOrBlank()) { + return true + } + } catch (e: Exception) { + Log.w(TAG, "pidof check failed for $packageName", e) + } + try { + val output = ShellUtils.runCommandWithOutput(context, "pgrep -f $packageName") + if (!output.isNullOrBlank()) { + return true + } + } catch (e: Exception) { + Log.w(TAG, "pgrep check failed for $packageName", e) + } + } + + try { + val am = context.getSystemService(Context.ACTIVITY_SERVICE) as android.app.ActivityManager + val processes = am.runningAppProcesses + if (processes != null) { + for (process in processes) { + if (process.processName == packageName) { + return true + } + } + } + } catch (e: Exception) { + Log.w(TAG, "ActivityManager check failed for $packageName", e) + } + + return false + } + + fun safeWriteSetting(context: Context, type: String, key: String, value: String): Boolean { + val resolver = context.contentResolver + val resolverSuccess = try { + when (type.uppercase()) { + "GLOBAL" -> Settings.Global.putString(resolver, key, value) + "SECURE" -> Settings.Secure.putString(resolver, key, value) + "SYSTEM" -> Settings.System.putString(resolver, key, value) + else -> false + } + Log.d(TAG, "Successfully wrote setting via ContentResolver: [$type] $key = $value") + true + } catch (e: SecurityException) { + Log.e(TAG, "SecurityException writing setting via ContentResolver: [$type] $key = $value", e) + false + } catch (e: Exception) { + Log.e(TAG, "Error writing setting via ContentResolver: [$type] $key = $value", e) + false + } + + // Run shell command asynchronously in background to shut down/enable active ports/daemons without blocking + if (ShellUtils.isAvailable(context) && ShellUtils.hasPermission(context)) { + val job = shellScope.launch { + try { + val shellType = type.lowercase() + ShellUtils.runCommand(context, "settings put $shellType $key $value") + Log.d(TAG, "Successfully executed setting put via Shell (async): [$type] $key = $value") + } catch (e: Exception) { + Log.w(TAG, "Failed to write setting via Shell (async): [$type] $key = $value", e) + } + } + pendingShellJobs.add(job) + // Prune completed jobs to avoid list growing forever + pendingShellJobs.removeAll { it.isCompleted || it.isCancelled } + } + + return resolverSuccess + } + + fun safeReadSetting(context: Context, type: String, key: String): String? { + val resolver = context.contentResolver + return try { + when (type.uppercase()) { + "GLOBAL" -> Settings.Global.getString(resolver, key) + "SECURE" -> Settings.Secure.getString(resolver, key) + "SYSTEM" -> Settings.System.getString(resolver, key) + else -> null + } + } catch (e: Exception) { + null + } + } + + suspend fun applyShutUpSettings(context: Context, config: ShutUpAppConfig) { + Log.d(TAG, "Applying ShutUp settings for ${config.packageName}") + // Cancel any stale revert shell commands before applying to prevent race conditions + cancelPendingShellJobs() + withContext(Dispatchers.IO) { + val repository = SettingsRepository(context) + val currentBackup = repository.getShutUpOriginalSettings() + val originalSettings = currentBackup.toMutableMap() + + config.settings.forEach { setting -> + if (setting.enabled) { + val resolvedType = if (setting.key == "accessibility_enabled") "SECURE" else setting.settingType + val prefixedKey = "${resolvedType.lowercase()}:${setting.key}" + if (!originalSettings.containsKey(prefixedKey)) { + val currentVal = safeReadSetting(context, resolvedType, setting.key) + if (currentVal != null) { + originalSettings[prefixedKey] = currentVal + } + } + safeWriteSetting(context, resolvedType, setting.key, setting.valueOnLaunch) + } + } + + // Special handling for accessibility services + val disableAccessibility = config.settings.any { it.key == "accessibility_enabled" && it.enabled } + if (disableAccessibility) { + val prefixedAccKey = "secure:${Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES}" + if (!originalSettings.containsKey(prefixedAccKey)) { + val currentAccServices = safeReadSetting(context, "SECURE", Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES) + if (!currentAccServices.isNullOrEmpty()) { + originalSettings[prefixedAccKey] = currentAccServices + safeWriteSetting(context, "SECURE", Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, "") + } + } + } + + if (originalSettings.isNotEmpty() && originalSettings != currentBackup) { + repository.saveShutUpOriginalSettings(originalSettings) + } + } + } + + suspend fun revertShutUpSettings(context: Context, config: ShutUpAppConfig) { + Log.d(TAG, "Reverting ShutUp settings for ${config.packageName}") + // Cancel any stale apply shell commands before reverting to prevent race conditions + cancelPendingShellJobs() + withContext(Dispatchers.IO) { + config.settings.forEach { setting -> + if (setting.enabled) { + val resolvedType = if (setting.key == "accessibility_enabled") "SECURE" else setting.settingType + safeWriteSetting(context, resolvedType, setting.key, setting.valueOnRevert) + } + } + } + } + + suspend fun restoreOriginalSettings(context: Context, repository: SettingsRepository) { + val originalSettings = repository.getShutUpOriginalSettings() + if (originalSettings.isEmpty()) { + Log.d(TAG, "No original settings to restore (backup empty)") + return + } + + Log.d(TAG, "Restoring original settings from backup (${originalSettings.size} entries)") + withContext(Dispatchers.IO) { + var restoredAccessibilityServices = false + originalSettings.forEach { (prefixedKey, value) -> + try { + val parts = prefixedKey.split(":", limit = 2) + if (parts.size < 2) return@forEach + val table = parts[0] + val key = parts[1] + when (table) { + "global" -> Settings.Global.putString(context.contentResolver, key, value) + "secure" -> { + Settings.Secure.putString(context.contentResolver, key, value) + if (key == Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES) { + restoredAccessibilityServices = true + } + } + "system" -> Settings.System.putString(context.contentResolver, key, value) + } + Log.d(TAG, "Restored $prefixedKey = $value") + } catch (e: Exception) { + Log.e(TAG, "Failed to restore setting $prefixedKey", e) + } + } + + // Re-enable accessibility master switch when the services list was restored. + // Clearing ENABLED_ACCESSIBILITY_SERVICES also disables the master switch, + // so we must explicitly turn it back on. + if (restoredAccessibilityServices) { + try { + Settings.Secure.putString( + context.contentResolver, + Settings.Secure.ACCESSIBILITY_ENABLED, + "1" + ) + Log.d(TAG, "Re-enabled ACCESSIBILITY_ENABLED master switch") + } catch (e: Exception) { + Log.e(TAG, "Failed to re-enable ACCESSIBILITY_ENABLED", e) + } + } + + // Clear backup so AppFlowHandler doesn't double-restore + repository.saveShutUpOriginalSettings(emptyMap()) + } + + withContext(Dispatchers.Main) { + Toast.makeText( + context, + context.getString(R.string.shut_up_toast_restored), + Toast.LENGTH_SHORT + ).show() + } + } + + suspend fun restartShizuku(context: Context) { + Log.d(TAG, "Waiting 800ms for developer/ADB services to stabilize before restarting Shizuku") + delay(800) + Log.d(TAG, "Attempting Shizuku restart now") + // Try explicit ManualStartReceiver broadcast + try { + val intent = Intent("moe.shizuku.privileged.api.START").apply { + setClassName("moe.shizuku.privileged.api", "moe.shizuku.manager.receiver.ManualStartReceiver") + putExtra("auth", "y95fuaRb9USHiIg724tvTHIs") + addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES) + } + context.sendBroadcast(intent) + Log.d(TAG, "Sent explicit ManualStartReceiver broadcast") + } catch (e: Exception) { + Log.e(TAG, "Failed explicit ManualStartReceiver broadcast", e) + } + + // Try explicit BootReceiver broadcast + try { + val intent = Intent("moe.shizuku.privileged.api.START").apply { + setClassName("moe.shizuku.privileged.api", "moe.shizuku.manager.receiver.BootReceiver") + putExtra("auth", "y95fuaRb9USHiIg724tvTHIs") + addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES) + } + context.sendBroadcast(intent) + Log.d(TAG, "Sent explicit BootReceiver broadcast") + } catch (e: Exception) { + Log.e(TAG, "Failed explicit BootReceiver broadcast", e) + } + + // Try legacy/implicit broadcast + try { + val intent = Intent("moe.shizuku.privileged.api.START").apply { + setPackage("moe.shizuku.privileged.api") + putExtra("auth", "y95fuaRb9USHiIg724tvTHIs") + addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES) + } + context.sendBroadcast(intent) + Log.d(TAG, "Sent legacy implicit broadcast") + } catch (e: Exception) { + Log.e(TAG, "Failed legacy implicit broadcast", e) + } + + // If root is enabled, run the start script directly via root shell + if (ShellUtils.isRootEnabled(context)) { + Log.d(TAG, "Root is enabled, running Shizuku start script via root") + withContext(Dispatchers.IO) { + val scripts = listOf( + "sh /data/data/moe.shizuku.privileged.api/start.sh", + "sh /sdcard/Android/data/moe.shizuku.privileged.api/files/start.sh", + "sh /storage/emulated/0/Android/data/moe.shizuku.privileged.api/files/start.sh" + ) + scripts.forEach { script -> + try { + val success = RootUtils.runCommand(script) + Log.d(TAG, "Executed root command: '$script', success: $success") + } catch (e: Exception) { + Log.e(TAG, "Failed root command: '$script'", e) + } + } + } + } + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt index 492ac93af..3106d4663 100644 --- a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt +++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt @@ -37,6 +37,7 @@ import com.sameerasw.essentials.data.repository.UpdateRepository import com.sameerasw.essentials.domain.HapticFeedbackType import com.sameerasw.essentials.domain.MapsState import com.sameerasw.essentials.domain.model.AppSelection +import com.sameerasw.essentials.domain.model.ShutUpAppConfig import com.sameerasw.essentials.domain.model.DnsPreset import com.sameerasw.essentials.domain.model.NotificationApp import com.sameerasw.essentials.domain.model.NotificationLightingColorMode @@ -119,6 +120,8 @@ class MainViewModel : ViewModel() { val isBluetoothPermissionGranted = mutableStateOf(false) val isUsageStatsPermissionGranted = mutableStateOf(false) val appLanguage = mutableStateOf("en") + val isShutUpServiceEnabled = mutableStateOf(false) + val shutUpConfigs = mutableStateOf>(emptyList()) val isBluetoothDevicesEnabled = mutableStateOf(false) val isCallVibrationsEnabled = mutableStateOf(false) @@ -156,10 +159,7 @@ class MainViewModel : ViewModel() { mutableStateOf(SettingsRepository.LIVE_WALLPAPER_TRIGGER_UNLOCK) val liveWallpaperCustomVideos = mutableStateListOf() - val shutUpConfigs = - mutableStateOf>(emptyList()) - val isShutUpLoading = mutableStateOf(false) - val isShutUpAttemptShizukuRestart = mutableStateOf(true) + data class CalendarAccount( @@ -628,10 +628,7 @@ class MainViewModel : ViewModel() { liveWallpaperCustomVideos.addAll(settingsRepository.getLiveWallpaperCustomVideos()) } - SettingsRepository.KEY_SHUT_UP_ATTEMPT_SHIZUKU_RESTART -> { - isShutUpAttemptShizukuRestart.value = - settingsRepository.isShutUpAttemptShizukuRestartEnabled() - } + SettingsRepository.KEY_DISABLE_ROTATION_SUGGESTION -> { isDisableRotationSuggestionEnabled.value = @@ -658,12 +655,21 @@ class MainViewModel : ViewModel() { AppCompatDelegate.setApplicationLocales(appLocale) } + + fun loadShutUpConfigs() { shutUpConfigs.value = settingsRepository.loadShutUpConfigs() } - fun updateShutUpConfig(config: com.sameerasw.essentials.domain.model.ShutUpAppConfig) { - settingsRepository.updateShutUpConfig(config) + fun updateShutUpConfig(config: ShutUpAppConfig) { + val current = shutUpConfigs.value.toMutableList() + val index = current.indexOfFirst { it.packageName == config.packageName } + if (index != -1) { + current[index] = config + } else { + current.add(config) + } + settingsRepository.saveShutUpConfigs(current) loadShutUpConfigs() } @@ -674,63 +680,62 @@ class MainViewModel : ViewModel() { loadShutUpConfigs() } - fun setShutUpAttemptShizukuRestartEnabled(enabled: Boolean) { - isShutUpAttemptShizukuRestart.value = enabled - settingsRepository.setShutUpAttemptShizukuRestartEnabled(enabled) + fun setShutUpServiceEnabled(enabled: Boolean, context: Context) { + isShutUpServiceEnabled.value = enabled + settingsRepository.setShutUpServiceEnabled(enabled) + val intent = Intent(context, com.sameerasw.essentials.services.ShutUpForegroundService::class.java) + if (enabled) { + ContextCompat.startForegroundService(context, intent) + } else { + context.stopService(intent) + } } - fun saveShutUpSelectedApps(context: Context, apps: List) { - val currentConfigs = settingsRepository.loadShutUpConfigs().associateBy { it.packageName } - val newConfigs = apps.filter { it.isEnabled }.map { - currentConfigs[it.packageName] ?: com.sameerasw.essentials.domain.model.ShutUpAppConfig( - it.packageName - ) + fun createShutUpShortcut(context: Context, config: ShutUpAppConfig) { + if (!androidx.core.content.pm.ShortcutManagerCompat.isRequestPinShortcutSupported(context)) { + Toast.makeText(context, "Shortcut pinning not supported by launcher", Toast.LENGTH_SHORT).show() + return } - settingsRepository.saveShutUpConfigs(newConfigs) - loadShutUpConfigs() - } - fun createShutUpShortcut( - context: Context, - config: com.sameerasw.essentials.domain.model.ShutUpAppConfig - ) { - val appName = try { - val appInfo = context.packageManager.getApplicationInfo(config.packageName, 0) - context.packageManager.getApplicationLabel(appInfo).toString() + val pm = context.packageManager + val appLabel = try { + val appInfo = pm.getApplicationInfo(config.packageName, 0) + pm.getApplicationLabel(appInfo).toString() } catch (e: Exception) { config.packageName } + val shortLabel = "Shut-Up $appLabel" + val longLabel = "Launch $appLabel with Shut-Up" - val intent = - Intent(context, com.sameerasw.essentials.ShutUpShortcutActivity::class.java).apply { - action = Intent.ACTION_MAIN - putExtra("package_name", config.packageName) - data = Uri.parse("shutup://${config.packageName}") - } + val iconCompat = try { + val bitmap = com.sameerasw.essentials.utils.AppUtil.getShortcutIcon(context, config.packageName) + androidx.core.graphics.drawable.IconCompat.createWithBitmap(bitmap) + } catch (e: Exception) { + null + } - if (androidx.core.content.pm.ShortcutManagerCompat.isRequestPinShortcutSupported(context)) { - val appIcon = AppUtil.getShortcutIcon(context, config.packageName) + val shortcutIntent = Intent(context, com.sameerasw.essentials.ShutUpShortcutActivity::class.java).apply { + action = Intent.ACTION_VIEW + putExtra("package_name", config.packageName) + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } - val pinShortcutInfo = - androidx.core.content.pm.ShortcutInfoCompat.Builder(context, config.packageName) - .setShortLabel(appName) - .setIcon(androidx.core.graphics.drawable.IconCompat.createWithBitmap(appIcon)) - .setIntent(intent) - .build() + val shortcutInfo = androidx.core.content.pm.ShortcutInfoCompat.Builder(context, "shutup_${config.packageName}") + .setShortLabel(shortLabel) + .setLongLabel(longLabel) + .setIntent(shortcutIntent) + .apply { + if (iconCompat != null) { + setIcon(iconCompat) + } + } + .build() - androidx.core.content.pm.ShortcutManagerCompat.requestPinShortcut( - context, - pinShortcutInfo, - null - ) - Toast.makeText( - context, - context.getString(R.string.shut_up_shortcut_created, appName), - Toast.LENGTH_SHORT - ).show() - } + androidx.core.content.pm.ShortcutManagerCompat.requestPinShortcut(context, shortcutInfo, null) } + + fun check(context: Context) { appContext = context.applicationContext settingsRepository = SettingsRepository(context) @@ -776,8 +781,6 @@ class MainViewModel : ViewModel() { notificationLightingSystemMode.intValue = settingsRepository.getNotificationLightingSystemMode() - isShutUpAttemptShizukuRestart.value = - settingsRepository.isShutUpAttemptShizukuRestartEnabled() isDisableRotationSuggestionEnabled.value = settingsRepository.getBoolean(SettingsRepository.KEY_DISABLE_ROTATION_SUGGESTION, false) lockScreenClockId.value = readCurrentLockScreenClockId(context) @@ -789,6 +792,7 @@ class MainViewModel : ViewModel() { lockScreenClockSelectedColorId.value = settingsRepository.getLockScreenClockSelectedColorId() lockScreenClockSeedColor.intValue = settingsRepository.getLockScreenClockSeedColor() + isShutUpServiceEnabled.value = settingsRepository.isShutUpServiceEnabled() loadShutUpConfigs() recentSearches.value = settingsRepository.getRecentSearches() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2d79cf8a2..4589e99f1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1506,6 +1506,11 @@ %1$s will be archived in %2$d seconds Freeze now Abort + Shut-Up! Service + Monitors launched apps to disable developer settings + Shut-Up! is active + Monitoring app launch and exit + Lock screen clock Customize lock screen clock on Pixels