From e53fe00177ec364850c833d9b76866a97fba0d7e Mon Sep 17 00:00:00 2001 From: sameerasw Date: Sun, 10 May 2026 00:20:31 +0530 Subject: [PATCH 1/2] feat: implement Shut-Up! feature to dynamically restrict system settings for selected apps --- app/src/main/AndroidManifest.xml | 8 + .../essentials/FeatureSettingsActivity.kt | 11 ++ .../essentials/ShutUpShortcutActivity.kt | 161 ++++++++++++++++++ .../data/repository/SettingsRepository.kt | 50 +++++- .../domain/model/ShutUpAppConfig.kt | 10 ++ .../domain/registry/FeatureRegistry.kt | 15 ++ .../domain/registry/PermissionRegistry.kt | 5 + .../services/handlers/AppFlowHandler.kt | 63 +++++++ .../ui/components/cards/FeatureCard.kt | 35 +++- .../sheets/ShutUpPerAppSettingsSheet.kt | 112 ++++++++++++ .../ui/composables/SetupFeatures.kt | 44 +++++ .../composables/configs/ShutUpSettingsUI.kt | 153 +++++++++++++++++ .../com/sameerasw/essentials/utils/AppUtil.kt | 16 ++ .../essentials/viewmodels/MainViewModel.kt | 80 +++++++++ .../rounded_android_wifi_4_bar_plus_24.xml | 20 +++ .../res/drawable/rounded_domino_mask_24.xml | 20 +++ app/src/main/res/values/strings.xml | 16 ++ 17 files changed, 810 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/com/sameerasw/essentials/ShutUpShortcutActivity.kt create mode 100644 app/src/main/java/com/sameerasw/essentials/domain/model/ShutUpAppConfig.kt create mode 100644 app/src/main/java/com/sameerasw/essentials/ui/components/sheets/ShutUpPerAppSettingsSheet.kt create mode 100644 app/src/main/java/com/sameerasw/essentials/ui/composables/configs/ShutUpSettingsUI.kt create mode 100644 app/src/main/res/drawable/rounded_android_wifi_4_bar_plus_24.xml create mode 100644 app/src/main/res/drawable/rounded_domino_mask_24.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 53e9eb8be..8fed68fbb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -232,6 +232,14 @@ + + !viewModel.isWriteSettingsEnabled.value || !isWriteSecureSettingsEnabled "Always on Display" -> !isWriteSecureSettingsEnabled "Other customizations" -> !com.sameerasw.essentials.utils.ShellUtils.hasPermission(context) + "Shut-Up!" -> !isWriteSecureSettingsEnabled || !viewModel.isUsageStatsPermissionGranted.value else -> false } if (hasMissingPermissions) { @@ -412,6 +414,7 @@ class FeatureSettingsActivity : AppCompatActivity() { "Battery notification" -> !viewModel.isPostNotificationsEnabled.value || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !viewModel.isBluetoothPermissionGranted.value) "Text and animations" -> !viewModel.isWriteSettingsEnabled.value || !isWriteSecureSettingsEnabled "Screen refresh rate" -> !viewModel.isShizukuPermissionGranted.value + "Shut-Up!" -> !isWriteSecureSettingsEnabled || !viewModel.isUsageStatsPermissionGranted.value else -> false } @@ -678,6 +681,14 @@ 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 new file mode 100644 index 000000000..ca12affc1 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/ShutUpShortcutActivity.kt @@ -0,0 +1,161 @@ +package com.sameerasw.essentials + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.provider.Settings +import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.LoadingIndicator +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +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.PermissionUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class ShutUpShortcutActivity : ComponentActivity() { + + @OptIn(ExperimentalMaterial3ExpressiveApi::class) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + val packageName = intent.getStringExtra("package_name") + if (packageName == null) { + finish() + return + } + + setContent { + val viewModel: com.sameerasw.essentials.viewmodels.MainViewModel = + androidx.lifecycle.viewmodel.compose.viewModel() + val context = androidx.compose.ui.platform.LocalContext.current + androidx.compose.runtime.LaunchedEffect(Unit) { + viewModel.check(context) + } + val isPitchBlackThemeEnabled by viewModel.isPitchBlackThemeEnabled + EssentialsTheme(pitchBlackTheme = isPitchBlackThemeEnabled) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + LoadingIndicator(modifier = Modifier.scale(5f)) + } + } + } + + val settingsRepository = SettingsRepository(this) + val config = settingsRepository.loadShutUpConfigs().find { it.packageName == packageName } + + lifecycleScope.launch { + if (config != null && config.isEnabled) { + if (PermissionUtils.canWriteSecureSettings(this@ShutUpShortcutActivity)) { + applyShutUpSettings(config, settingsRepository) + withContext(Dispatchers.Main) { + Toast.makeText(this@ShutUpShortcutActivity, getString(R.string.shut_up_toast_active), Toast.LENGTH_SHORT).show() + } + } + } + + delay(800) + + launchApp(packageName) + finish() + } + } + + 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 -> + Settings.Secure.getString(contentResolver, key)?.let { originalSettings["secure:$key"] = it } + } + systemSettings.forEach { key -> + Settings.System.getString(contentResolver, key)?.let { originalSettings["system:$key"] = it } + } + globalSettings.forEach { key -> + Settings.Global.getString(contentResolver, 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 = Settings.Global.getString(contentResolver, 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 = Settings.Global.getString(contentResolver, "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 = Settings.Secure.getString(contentResolver, 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 fun launchApp(packageName: String) { + val intent = packageManager.getLaunchIntentForPackage(packageName) + if (intent != null) { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + startActivity(intent) + } else { + Toast.makeText(this, "Could not launch $packageName", Toast.LENGTH_SHORT).show() + } + } +} 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 75bec9ff7..36bc75def 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 @@ -222,6 +222,9 @@ class SettingsRepository(private val context: Context) { const val LIVE_WALLPAPER_DEFAULT_VIDEO = "my_video" const val LIVE_WALLPAPER_TRIGGER_UNLOCK = "unlock" const val LIVE_WALLPAPER_TRIGGER_SCREEN_ON = "screen_on" + + const val KEY_SHUT_UP_SELECTED_APPS = "shut_up_selected_apps" + const val KEY_SHUT_UP_ORIGINAL_SETTINGS = "shut_up_original_settings" } // Observe changes @@ -480,6 +483,50 @@ 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() val index = current.indexOfFirst { it.packageName == packageName } @@ -601,7 +648,8 @@ class SettingsRepository(private val context: Context) { p.all.forEach { (key, value) -> if (key == "freeze_auto_excluded_apps" || key.endsWith("_selected_apps")) { } else if (key.startsWith("mac_battery_") || key == "airsync_mac_connected" || - key == KEY_SNOOZE_DISCOVERED_CHANNELS || key == KEY_MAPS_DISCOVERED_CHANNELS + key == KEY_SNOOZE_DISCOVERED_CHANNELS || key == KEY_MAPS_DISCOVERED_CHANNELS || + key == KEY_SHUT_UP_ORIGINAL_SETTINGS ) { return@forEach } 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 new file mode 100644 index 000000000..270e2117d --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/domain/model/ShutUpAppConfig.kt @@ -0,0 +1,10 @@ +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 +) 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 a8f66c95b..6e6cea7fd 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 @@ -105,6 +105,21 @@ object FeatureRegistry { override fun isEnabled(viewModel: MainViewModel) = true override fun onToggle(viewModel: MainViewModel, context: Context, enabled: Boolean) {} }, + object : Feature( + id = "Shut-Up!", + title = R.string.feat_shut_up_title, + iconRes = R.drawable.rounded_domino_mask_24, + category = R.string.cat_system, + description = R.string.feat_shut_up_desc, + aboutDescription = R.string.shut_up_description, + permissionKeys = listOf("WRITE_SECURE_SETTINGS", "USAGE_STATS"), + showToggle = false, + hasMoreSettings = true, + parentFeatureId = "Security" + ) { + override fun isEnabled(viewModel: MainViewModel) = true + override fun onToggle(viewModel: MainViewModel, context: Context, enabled: Boolean) {} + }, object : Feature( id = "Notifications", title = R.string.feat_notifications_alerts_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 271c4bfb7..0feac26fb 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 @@ -91,4 +91,9 @@ fun initPermissionRegistry() { // Default browser permission PermissionRegistry.register("DEFAULT_BROWSER", R.string.feat_link_actions_title) + + // Shut-Up! feature + 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) } 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 b5d24fd7a..64f36ea9d 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 @@ -70,6 +70,7 @@ class AppFlowHandler( checkHighlightNightLight(packageName) checkAppAutomations(packageName) checkGestureBarAutomation(packageName) + checkShutUpRestore(oldPackage, packageName) } } @@ -316,4 +317,66 @@ class AppFlowHandler( val launchers = context.packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY) return launchers.any { it.activityInfo.packageName == packageName } } + + private fun checkShutUpRestore(oldPackage: String?, newPackage: String?) { + if (oldPackage == null || oldPackage == newPackage) return + + val settingsRepository = com.sameerasw.essentials.data.repository.SettingsRepository(context) + val shutUpConfigs = settingsRepository.loadShutUpConfigs() + + val wasShutUpApp = shutUpConfigs.any { it.packageName == oldPackage && it.isEnabled } + val isShutUpApp = shutUpConfigs.any { it.packageName == newPackage && it.isEnabled } + + if (wasShutUpApp && !isShutUpApp) { + restoreShutUpSettings(settingsRepository) + } + } + + private fun restoreShutUpSettings(repository: com.sameerasw.essentials.data.repository.SettingsRepository) { + val originalSettings = repository.getShutUpOriginalSettings() + if (originalSettings.isEmpty()) return + + scope.launch { + // Delay to ensure the app has fully settled before restoring system settings + kotlinx.coroutines.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()) + + android.widget.Toast.makeText(context, context.getString(com.sameerasw.essentials.R.string.shut_up_toast_restored), android.widget.Toast.LENGTH_SHORT).show() + } + } } 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 38ed098fe..b3e87d16f 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 @@ -62,7 +62,10 @@ fun FeatureCard( isBeta: Boolean = false, isPinned: Boolean = false, onPinToggle: (() -> Unit)? = null, - onHelpClick: (() -> Unit)? = null + onHelpClick: (() -> Unit)? = null, + additionalMenuItems: (@Composable (onDismiss: () -> Unit) -> Unit)? = null, + customTrailingContent: (@Composable () -> Unit)? = null, + iconPainter: androidx.compose.ui.graphics.painter.Painter? = null ) { val view = LocalView.current var showMenu by remember { mutableStateOf(false) } @@ -114,7 +117,7 @@ fun FeatureCard( modifier = modifier .alpha(alpha) .blur(blurRadius), - leadingContent = if (iconRes != null) { + leadingContent = if (iconPainter != null || iconRes != null) { { Box( modifier = Modifier @@ -125,12 +128,20 @@ fun FeatureCard( ), contentAlignment = Alignment.Center ) { - Icon( - painter = painterResource(id = iconRes), - contentDescription = resolvedTitle, - modifier = Modifier.size(24.dp), - tint = ColorUtil.getVibrantColorFor(resolvedTitle) - ) + if (iconPainter != null) { + androidx.compose.foundation.Image( + painter = iconPainter, + contentDescription = resolvedTitle, + modifier = Modifier.size(24.dp) + ) + } else if (iconRes != null) { + Icon( + painter = painterResource(id = iconRes), + contentDescription = resolvedTitle, + modifier = Modifier.size(24.dp), + tint = ColorUtil.getVibrantColorFor(resolvedTitle) + ) + } } } } else null, @@ -187,6 +198,10 @@ fun FeatureCard( } } } + + if (customTrailingContent != null) { + customTrailingContent() + } } }, colors = androidx.compose.material3.ListItemDefaults.colors( @@ -265,6 +280,10 @@ fun FeatureCard( } ) } + + if (additionalMenuItems != null) { + additionalMenuItems { showMenu = false } + } } } ) 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 new file mode 100644 index 000000000..629e7ce93 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/ShutUpPerAppSettingsSheet.kt @@ -0,0 +1,112 @@ +package com.sameerasw.essentials.ui.components.sheets + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.sameerasw.essentials.R +import com.sameerasw.essentials.domain.model.ShutUpAppConfig +import com.sameerasw.essentials.ui.components.cards.IconToggleItem +import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ShutUpPerAppSettingsSheet( + onDismissRequest: () -> Unit, + config: ShutUpAppConfig, + onConfigChanged: (ShutUpAppConfig) -> Unit, + onCreateShortcut: (ShutUpAppConfig) -> Unit +) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + var currentConfig by remember(config) { mutableStateOf(config) } + + ModalBottomSheet( + onDismissRequest = onDismissRequest, + sheetState = sheetState, + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = stringResource(R.string.shut_up_per_app_settings), + style = MaterialTheme.typography.headlineSmall, + modifier = Modifier.padding(horizontal = 16.dp) + ) + + RoundedCardContainer( + modifier = Modifier, + spacing = 2.dp, + cornerRadius = 24.dp + ) { + IconToggleItem( + iconRes = R.drawable.rounded_settings_24, + title = stringResource(R.string.shut_up_disable_dev_options), + isChecked = currentConfig.disableDevOptions, + onCheckedChange = { + val newConfig = currentConfig.copy(disableDevOptions = it) + currentConfig = newConfig + onConfigChanged(newConfig) + } + ) + IconToggleItem( + iconRes = R.drawable.rounded_adb_24, + title = stringResource(R.string.shut_up_disable_usb_debugging), + isChecked = currentConfig.disableUsbDebugging, + onCheckedChange = { + val newConfig = currentConfig.copy(disableUsbDebugging = it) + currentConfig = newConfig + onConfigChanged(newConfig) + } + ) + IconToggleItem( + iconRes = R.drawable.rounded_android_wifi_4_bar_plus_24, + title = stringResource(R.string.shut_up_disable_wireless_debugging), + isChecked = currentConfig.disableWirelessDebugging, + onCheckedChange = { + val newConfig = currentConfig.copy(disableWirelessDebugging = it) + currentConfig = newConfig + onConfigChanged(newConfig) + } + ) + IconToggleItem( + iconRes = R.drawable.rounded_settings_accessibility_24, + title = stringResource(R.string.shut_up_disable_accessibility), + isChecked = currentConfig.disableAccessibility, + onCheckedChange = { + val newConfig = currentConfig.copy(disableAccessibility = it) + currentConfig = newConfig + onConfigChanged(newConfig) + } + ) + } + + Button( + onClick = { + onCreateShortcut(currentConfig) + onDismissRequest() + }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + shape = MaterialTheme.shapes.extraLarge + ) { + Icon( + painter = androidx.compose.ui.res.painterResource(id = R.drawable.rounded_open_in_new_24), + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(text = stringResource(R.string.action_create_shortcut)) + } + + Spacer(modifier = Modifier.height(32.dp)) + } + } +} 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 e2a63641a..af4db5f95 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 @@ -92,6 +92,7 @@ fun SetupFeatures( ) { val isAccessibilityEnabled by viewModel.isAccessibilityEnabled val isWriteSecureSettingsEnabled by viewModel.isWriteSecureSettingsEnabled + val isWriteSettingsEnabled by viewModel.isWriteSettingsEnabled val isShizukuAvailable by viewModel.isShizukuAvailable val isShizukuPermissionGranted by viewModel.isShizukuPermissionGranted val isNotificationListenerEnabled by viewModel.isNotificationListenerEnabled @@ -202,6 +203,7 @@ fun SetupFeatures( showSheet, isAccessibilityEnabled, isWriteSecureSettingsEnabled, + isWriteSettingsEnabled, isShizukuAvailable, isShizukuPermissionGranted, isNotificationListenerEnabled, @@ -370,6 +372,48 @@ 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) { if (!isRootPermissionGranted) { 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 new file mode 100644 index 000000000..62acf706c --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/ShutUpSettingsUI.kt @@ -0,0 +1,153 @@ +package com.sameerasw.essentials.ui.composables.configs + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.platform.LocalContext +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.ShutUpPerAppSettingsSheet +import com.sameerasw.essentials.utils.AppUtil +import com.sameerasw.essentials.viewmodels.MainViewModel + +@Composable +fun ShutUpSettingsUI( + viewModel: MainViewModel, + modifier: Modifier = Modifier, + highlightKey: String? = null +) { + val context = LocalContext.current + var isAppSelectionSheetOpen by remember { mutableStateOf(false) } + var selectedConfigForEditing by remember { mutableStateOf(null) } + + val configs by viewModel.shutUpConfigs + + Column( + modifier = modifier + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + + RoundedCardContainer( + modifier = Modifier, + spacing = 2.dp, + 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 } + ) + } + + + RoundedCardContainer( + modifier = Modifier, + spacing = 2.dp, + cornerRadius = 24.dp + ) { + configs.forEach { config -> + val appName = remember(config.packageName) { + try { + val appInfo = context.packageManager.getApplicationInfo(config.packageName, 0) + context.packageManager.getApplicationLabel(appInfo).toString() + } catch (e: Exception) { + config.packageName + } + } + + val appIconPainter = remember(config.packageName) { + try { + val drawable = context.packageManager.getApplicationIcon(config.packageName) + androidx.compose.ui.graphics.painter.BitmapPainter( + AppUtil.drawableToBitmap(drawable).asImageBitmap() + ) + } catch (e: Exception) { + null + } + } + + FeatureCard( + title = appName, + description = config.packageName, + isEnabled = true, + onToggle = {}, + onClick = { selectedConfigForEditing = config }, + iconPainter = appIconPainter, + showToggle = false, + hasMoreSettings = true, + customTrailingContent = { + IconButton( + onClick = { + viewModel.createShutUpShortcut(context, config) + } + ) { + Icon( + painter = painterResource(id = R.drawable.rounded_add_24), + contentDescription = stringResource(R.string.action_create_shortcut), + tint = MaterialTheme.colorScheme.primary + ) + } + }, + additionalMenuItems = { onDismiss -> + SegmentedDropdownMenuItem( + text = { Text(stringResource(R.string.action_remove)) }, + onClick = { + onDismiss() + viewModel.removeShutUpConfig(config.packageName) + }, + leadingIcon = { + Icon( + painter = painterResource(id = R.drawable.rounded_delete_24), + contentDescription = null + ) + } + ) + } + ) + } + } + + Text( + text = stringResource(R.string.shut_up_description), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(16.dp), + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + if (isAppSelectionSheetOpen) { + AppSelectionSheet( + onDismissRequest = { isAppSelectionSheetOpen = false }, + onLoadApps = { ctx -> + viewModel.shutUpConfigs.value.map { AppSelection(it.packageName, true) } + }, + onSaveApps = { ctx, apps -> viewModel.saveShutUpSelectedApps(ctx, apps) } + ) + } + + if (selectedConfigForEditing != null) { + ShutUpPerAppSettingsSheet( + onDismissRequest = { selectedConfigForEditing = null }, + config = configs.find { it.packageName == selectedConfigForEditing?.packageName } ?: selectedConfigForEditing!!, + onConfigChanged = { viewModel.updateShutUpConfig(it) }, + onCreateShortcut = { viewModel.createShutUpShortcut(context, it) } + ) + } + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/utils/AppUtil.kt b/app/src/main/java/com/sameerasw/essentials/utils/AppUtil.kt index ce307c8b6..5d0b62928 100644 --- a/app/src/main/java/com/sameerasw/essentials/utils/AppUtil.kt +++ b/app/src/main/java/com/sameerasw/essentials/utils/AppUtil.kt @@ -197,6 +197,22 @@ object AppUtil { return bitmap } + fun drawableToBitmap(drawable: android.graphics.drawable.Drawable): Bitmap { + if (drawable is BitmapDrawable) { + return drawable.bitmap + } + + val bitmap = Bitmap.createBitmap( + drawable.intrinsicWidth.coerceAtLeast(1), + drawable.intrinsicHeight.coerceAtLeast(1), + Bitmap.Config.ARGB_8888 + ) + val canvas = Canvas(bitmap) + drawable.setBounds(0, 0, canvas.width, canvas.height) + drawable.draw(canvas) + return bitmap + } + fun getAppVersion(context: Context, packageName: String): String? { return try { val pInfo = context.packageManager.getPackageInfo(packageName, 0) 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 9dd0fe06d..2a549799a 100644 --- a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt +++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt @@ -21,6 +21,7 @@ import android.os.PowerManager import android.provider.CalendarContract import android.provider.Settings import android.view.inputmethod.InputMethodManager +import android.widget.Toast import androidx.appcompat.app.AppCompatDelegate import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf @@ -144,6 +145,9 @@ class MainViewModel : ViewModel() { val liveWallpaperPlaybackTrigger = mutableStateOf(SettingsRepository.LIVE_WALLPAPER_TRIGGER_UNLOCK) val liveWallpaperCustomVideos = mutableStateListOf() + val shutUpConfigs = mutableStateOf>(emptyList()) + val isShutUpLoading = mutableStateOf(false) + data class CalendarAccount( @@ -556,6 +560,64 @@ class MainViewModel : ViewModel() { AppCompatDelegate.setApplicationLocales(appLocale) } + fun loadShutUpConfigs() { + shutUpConfigs.value = settingsRepository.loadShutUpConfigs() + } + + fun updateShutUpConfig(config: com.sameerasw.essentials.domain.model.ShutUpAppConfig) { + settingsRepository.updateShutUpConfig(config) + loadShutUpConfigs() + } + + fun removeShutUpConfig(packageName: String) { + val current = shutUpConfigs.value.toMutableList() + current.removeAll { it.packageName == packageName } + settingsRepository.saveShutUpConfigs(current) + loadShutUpConfigs() + } + + 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) + } + 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() + } catch (e: Exception) { + config.packageName + } + + 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}") + } + + if (androidx.core.content.pm.ShortcutManagerCompat.isRequestPinShortcutSupported(context)) { + val appIcon = try { + val drawable = context.packageManager.getApplicationIcon(config.packageName) + AppUtil.drawableToBitmap(drawable) + } catch (e: Exception) { + null + } + + val pinShortcutInfo = androidx.core.content.pm.ShortcutInfoCompat.Builder(context, config.packageName) + .setShortLabel(appName) + .setIcon(if (appIcon != null) androidx.core.graphics.drawable.IconCompat.createWithBitmap(appIcon) else androidx.core.graphics.drawable.IconCompat.createWithResource(context, R.drawable.rounded_volume_off_24)) + .setIntent(intent) + .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() + } + } + fun check(context: Context) { appContext = context.applicationContext settingsRepository = SettingsRepository(context) @@ -580,6 +642,9 @@ class MainViewModel : ViewModel() { isCircleToSearchPreviewEnabled.value = settingsRepository.getBoolean(SettingsRepository.KEY_CIRCLE_TO_SEARCH_PREVIEW_ENABLED, false) isHideGestureBarOnLauncherEnabled.value = settingsRepository.getBoolean(SettingsRepository.KEY_HIDE_GESTURE_BAR_ON_LAUNCHER_ENABLED, false) notificationLightingSystemMode.intValue = settingsRepository.getNotificationLightingSystemMode() + + loadShutUpConfigs() + if (isHideGestureBarEnabled.value) { applyHideGestureBar(context, true) } @@ -2078,6 +2143,21 @@ class MainViewModel : ViewModel() { context.startActivity(intent) } + fun requestWriteSecureSettingsPermission(context: Context) { + val adbCommand = "adb shell pm grant ${context.packageName} android.permission.WRITE_SECURE_SETTINGS" + val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager + val clip = android.content.ClipData.newPlainText("adb_command", adbCommand) + clipboard.setPrimaryClip(clip) + } + + fun requestUsageStatsPermission(context: Context) { + PermissionUtils.openUsageStatsSettings(context) + } + + fun requestWriteSettingsPermission(context: Context) { + PermissionUtils.openWriteSettings(context) + } + fun showImePicker(context: Context) { val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager imm.showInputMethodPicker() diff --git a/app/src/main/res/drawable/rounded_android_wifi_4_bar_plus_24.xml b/app/src/main/res/drawable/rounded_android_wifi_4_bar_plus_24.xml new file mode 100644 index 000000000..bdb2df84e --- /dev/null +++ b/app/src/main/res/drawable/rounded_android_wifi_4_bar_plus_24.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/app/src/main/res/drawable/rounded_domino_mask_24.xml b/app/src/main/res/drawable/rounded_domino_mask_24.xml new file mode 100644 index 000000000..322ff2fd3 --- /dev/null +++ b/app/src/main/res/drawable/rounded_domino_mask_24.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ac2619e61..558c2a6ce 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1458,4 +1458,20 @@ Add video Custom video Set your favorite video as your home screen wallpaper. Choose between triggering the animation upon unlocking or simply whenever the screen turns on for a dynamic experience. + + + Shut-Up! + I know what I\'m doing + Shut-Up! bypasses app restrictions and checks for developer options, accessibility and debugging by dynamically hiding them when you launch sensitive apps. \n\nUse the custom shortcuts to ensure settings are hidden before the app launches. + Select Apps + Choose apps to apply Shut-Up! logic + Shut-Up! Settings + Shut-Up! Features + Disable Developer Options + Disable USB Debugging + Disable Wireless Debugging + Disable Accessibility Services + Shortcut created for %s + App got shut up + Shut up features returned From e3ade0d90de7a41449ad0be89dce107f54f5e6e2 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Sun, 10 May 2026 01:16:17 +0530 Subject: [PATCH 2/2] feat: add auto-freeze functionality for ShutUp apps with countdown notifications and configuration support. --- .../essentials/ShutUpShortcutActivity.kt | 7 + .../domain/model/ShutUpAppConfig.kt | 3 +- .../services/handlers/AppFlowHandler.kt | 283 +++++++++++++++++- .../sheets/ShutUpPerAppSettingsSheet.kt | 13 +- .../composables/configs/ShutUpSettingsUI.kt | 8 +- .../essentials/utils/FreezeManager.kt | 30 +- .../essentials/utils/ServiceUtils.kt | 5 +- app/src/main/res/values/strings.xml | 4 + 8 files changed, 342 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/sameerasw/essentials/ShutUpShortcutActivity.kt b/app/src/main/java/com/sameerasw/essentials/ShutUpShortcutActivity.kt index ca12affc1..9646236b0 100644 --- a/app/src/main/java/com/sameerasw/essentials/ShutUpShortcutActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/ShutUpShortcutActivity.kt @@ -61,6 +61,12 @@ class ShutUpShortcutActivity : ComponentActivity() { val config = settingsRepository.loadShutUpConfigs().find { it.packageName == packageName } lifecycleScope.launch { + // Unfreeze first while Shizuku/Root is still functional + if (com.sameerasw.essentials.utils.FreezeManager.isAppFrozen(this@ShutUpShortcutActivity, packageName)) { + com.sameerasw.essentials.utils.FreezeManager.unfreezeApp(this@ShutUpShortcutActivity, packageName) + delay(200) // Small extra delay for system to register unfreeze + } + if (config != null && config.isEnabled) { if (PermissionUtils.canWriteSecureSettings(this@ShutUpShortcutActivity)) { applyShutUpSettings(config, settingsRepository) @@ -70,6 +76,7 @@ class ShutUpShortcutActivity : ComponentActivity() { } } + // Delay to ensure system registers the settings changes delay(800) launchApp(packageName) 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 270e2117d..a9a0551b1 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 @@ -6,5 +6,6 @@ data class ShutUpAppConfig( val disableDevOptions: Boolean = true, val disableUsbDebugging: Boolean = true, val disableWirelessDebugging: Boolean = true, - val disableAccessibility: Boolean = false + val disableAccessibility: Boolean = false, + val autoArchive: Boolean = false ) 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 64f36ea9d..6d2862292 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 @@ -15,9 +15,19 @@ 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.StatusBarManager +import com.sameerasw.essentials.utils.FreezeManager +import com.sameerasw.essentials.domain.model.ShutUpAppConfig import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import android.app.NotificationManager +import android.app.PendingIntent +import androidx.core.app.NotificationCompat +import android.content.BroadcastReceiver +import android.content.IntentFilter class AppFlowHandler( private val context: Context, @@ -28,6 +38,38 @@ class AppFlowHandler( 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 @@ -319,22 +361,224 @@ class AppFlowHandler( } 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 wasShutUpApp = shutUpConfigs.any { it.packageName == oldPackage && it.isEnabled } - val isShutUpApp = shutUpConfigs.any { it.packageName == newPackage && it.isEnabled } + 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) } ?: false + + // 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) - if (wasShutUpApp && !isShutUpApp) { - restoreShutUpSettings(settingsRepository) + 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 restoreShutUpSettings(repository: com.sameerasw.essentials.data.repository.SettingsRepository) { + private fun createNotificationChannel() { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + val channel = android.app.NotificationChannel( + "app_detection_service_channel", + context.getString(com.sameerasw.essentials.R.string.app_detection_service_channel_name), + NotificationManager.IMPORTANCE_HIGH + ).apply { + description = "Channel for app detection alerts" + } + notificationManager.createNotificationChannel(channel) + + val alertChannel = android.app.NotificationChannel( + "shutup_alerts_channel", + "Shut-Up! Alerts", + NotificationManager.IMPORTANCE_MAX + ).apply { + description = "Live update notifications for auto archiving" + enableVibration(false) + setSound(null, null) + lockscreenVisibility = android.app.Notification.VISIBILITY_PUBLIC + } + notificationManager.createNotificationChannel(alertChannel) + } + } + + private fun restoreShutUpSettings(repository: com.sameerasw.essentials.data.repository.SettingsRepository, autoArchivePackage: String? = null) { val originalSettings = repository.getShutUpOriginalSettings() - if (originalSettings.isEmpty()) return + if (originalSettings.isEmpty()) { + if (autoArchivePackage != null) { + startAutoArchiveCountdown(autoArchivePackage) + } + return + } scope.launch { // Delay to ensure the app has fully settled before restoring system settings @@ -376,7 +620,34 @@ class AppFlowHandler( // Clear original settings after restoration repository.saveShutUpOriginalSettings(emptyMap()) + // Wait a bit and Restart Shizuku as ADB might have been toggled back on + kotlinx.coroutines.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/sheets/ShutUpPerAppSettingsSheet.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/ShutUpPerAppSettingsSheet.kt index 629e7ce93..f5c61c9a7 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 @@ -18,7 +18,8 @@ fun ShutUpPerAppSettingsSheet( onDismissRequest: () -> Unit, config: ShutUpAppConfig, onConfigChanged: (ShutUpAppConfig) -> Unit, - onCreateShortcut: (ShutUpAppConfig) -> Unit + onCreateShortcut: (ShutUpAppConfig) -> Unit, + isFrozen: Boolean ) { val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) var currentConfig by remember(config) { mutableStateOf(config) } @@ -85,6 +86,16 @@ fun ShutUpPerAppSettingsSheet( onConfigChanged(newConfig) } ) + IconToggleItem( + iconRes = R.drawable.rounded_snowflake_24, + title = stringResource(R.string.shut_up_auto_archive_notif_title), + isChecked = currentConfig.autoArchive, + onCheckedChange = { + val newConfig = currentConfig.copy(autoArchive = it) + currentConfig = newConfig + onConfigChanged(newConfig) + } + ) } Button( 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 62acf706c..2d02a6f6b 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 @@ -142,11 +142,17 @@ fun ShutUpSettingsUI( } if (selectedConfigForEditing != null) { + val frozenApps = remember { viewModel.loadFreezeSelectedApps(context) } + val isFrozen = remember(selectedConfigForEditing) { + frozenApps.any { it.packageName == selectedConfigForEditing?.packageName } + } + ShutUpPerAppSettingsSheet( onDismissRequest = { selectedConfigForEditing = null }, config = configs.find { it.packageName == selectedConfigForEditing?.packageName } ?: selectedConfigForEditing!!, onConfigChanged = { viewModel.updateShutUpConfig(it) }, - onCreateShortcut = { viewModel.createShutUpShortcut(context, it) } + onCreateShortcut = { viewModel.createShutUpShortcut(context, it) }, + isFrozen = isFrozen ) } } diff --git a/app/src/main/java/com/sameerasw/essentials/utils/FreezeManager.kt b/app/src/main/java/com/sameerasw/essentials/utils/FreezeManager.kt index fa2a4b3d4..01d15bfd5 100644 --- a/app/src/main/java/com/sameerasw/essentials/utils/FreezeManager.kt +++ b/app/src/main/java/com/sameerasw/essentials/utils/FreezeManager.kt @@ -4,6 +4,7 @@ import android.content.Context import android.os.Build import android.os.IBinder import android.os.PersistableBundle +import android.util.Log import org.lsposed.hiddenapibypass.HiddenApiBypass import rikka.shizuku.Shizuku import rikka.shizuku.ShizukuBinderWrapper @@ -376,13 +377,40 @@ object FreezeManager { private fun getSuspenderPackage(): String = if (Shizuku.getUid() == 0) "com.sameerasw.essentials" else "com.android.shell" - private fun getUserId(): Int = android.os.Process.myUserHandle().hashCode() + private fun getUserId(): Int { + return try { + val userHandle = android.os.Process.myUserHandle() + val method = userHandle.javaClass.getMethod("getIdentifier") + method.invoke(userHandle) as Int + } catch (_: Exception) { + 0 + } + } private fun setApplicationEnabledSetting( context: Context, packageName: String, newState: Int ): Boolean { + // 1. Try Shizuku first + if (ShizukuUtils.isShizukuAvailable() && ShizukuUtils.hasPermission()) { + try { + val pm = getService("package", "android.content.pm.IPackageManager\$Stub") + if (pm != null) { + val userId = getUserId() + Log.d("FreezeManager", "Shizuku: setting $packageName to $newState for user $userId") + HiddenApiBypass.invoke( + pm.javaClass, pm, "setApplicationEnabledSetting", + packageName, newState, 0, userId, "android" + ) + return true + } + } catch (e: Exception) { + Log.e("FreezeManager", "Shizuku call failed", e) + } + } + + // 2. Fallback to Shell (Root) if (!ShellUtils.hasPermission(context)) return false val cmd = when (newState) { 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 f81865c9e..60dcb689a 100644 --- a/app/src/main/java/com/sameerasw/essentials/utils/ServiceUtils.kt +++ b/app/src/main/java/com/sameerasw/essentials/utils/ServiceUtils.kt @@ -28,7 +28,10 @@ object ServiceUtils { it.isEnabled && it.type == Automation.Type.APP } - val shouldRun = isUseUsageAccess && (isAppLockEnabled || isDynamicNightLightEnabled || isHideGestureBarOnLauncherEnabled || hasAppAutomations) + val shutUpConfigs = settingsRepository.loadShutUpConfigs() + val hasShutUpApps = shutUpConfigs.any { it.isEnabled } + + val shouldRun = isUseUsageAccess && (isAppLockEnabled || isDynamicNightLightEnabled || isHideGestureBarOnLauncherEnabled || hasAppAutomations || hasShutUpApps) val intent = Intent(context, AppDetectionService::class.java) if (shouldRun) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 558c2a6ce..36d9e984e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1474,4 +1474,8 @@ Shortcut created for %s App got shut up Shut up features returned + Auto Freeze + %1$s will be archived in %2$d seconds + Freeze now + Abort