diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8a00795..b6084a8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools" android:installLocation="internalOnly"> + @@ -26,9 +27,12 @@ tools:ignore="ForegroundServicesPolicy" /> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/dev/pranav/applock/BatteryProtection.kt b/app/src/main/java/dev/pranav/applock/BatteryProtection.kt new file mode 100644 index 0000000..6cba48b --- /dev/null +++ b/app/src/main/java/dev/pranav/applock/BatteryProtection.kt @@ -0,0 +1,22 @@ +package dev.pranav.applock.core.security + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.PowerManager +import android.provider.Settings + +fun enforceBatteryExemption(context: Context) { + + val pm = context.getSystemService(PowerManager::class.java) + + if (pm.isIgnoringBatteryOptimizations(context.packageName)) return + + val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) + + intent.data = Uri.parse("package:${context.packageName}") + + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + + context.startActivity(intent) +} diff --git a/app/src/main/java/dev/pranav/applock/RestrictSettingsState.kt b/app/src/main/java/dev/pranav/applock/RestrictSettingsState.kt new file mode 100644 index 0000000..9a753df --- /dev/null +++ b/app/src/main/java/dev/pranav/applock/RestrictSettingsState.kt @@ -0,0 +1,27 @@ +package dev.pranav.applock.core.security + +data class RestrictSettingsState( + + val blockOverlaySettings: Boolean = false, + + val blockUsageAccessSettings: Boolean = false, + + val blockAccessibilitySettings: Boolean = false, + + val blockDeviceAdminSettings: Boolean = false, + + val requireBatteryExemption: Boolean = false +) + +enum class RestrictSetting { + + OVERLAY, + + USAGE, + + ACCESSIBILITY, + + DEVICE_ADMIN, + + BATTERY +} diff --git a/app/src/main/java/dev/pranav/applock/core/broadcast/SettingsIntentInterceptor.kt b/app/src/main/java/dev/pranav/applock/core/broadcast/SettingsIntentInterceptor.kt new file mode 100644 index 0000000..0ae8569 --- /dev/null +++ b/app/src/main/java/dev/pranav/applock/core/broadcast/SettingsIntentInterceptor.kt @@ -0,0 +1,113 @@ +package dev.pranav.applock.core.broadcast + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import dev.pranav.applock.core.utils.LogUtils +import dev.pranav.applock.core.utils.appLockRepository +import dev.pranav.applock.core.utils.systemSettingsRestrictionManager + +/** + * COMPLETE FILE - Copy to app/src/main/java/dev/pranav/applock/core/broadcast/ + * + * Broadcast receiver that intercepts attempts to open restricted system settings pages. + * Register in AndroidManifest.xml with the provided intent filters. + */ +class SettingsIntentInterceptor : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent?) { + if (intent == null) { + return + } + + try { + val repository = context.appLockRepository() + + if (!repository.isAntiUninstallEnabled()) { + return + } + + val restrictionManager = context.systemSettingsRestrictionManager() + + if (restrictionManager.isIntentRestricted(intent)) { + blockAndShowLockScreen(context, intent) + abortBroadcast() + } + } catch (e: Exception) { + LogUtils.logError("Error in SettingsIntentInterceptor", e) + } + } + + /** + * Block the settings intent and show lock screen instead. + */ + private fun blockAndShowLockScreen(context: Context, intent: Intent) { + try { + val action = intent.action ?: "Unknown" + LogUtils.logSecurityEvent("Blocked attempt to access settings: $action") + + val lockScreenIntent = Intent(context, PasswordOverlayActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_CLEAR_TOP or + Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS + putExtra("isRestrictedSettings", true) + putExtra("restrictedAction", intent.action) + } + context.startActivity(lockScreenIntent) + + val homeIntent = Intent(Intent.ACTION_MAIN).apply { + addCategory(Intent.CATEGORY_HOME) + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + context.startActivity(homeIntent) + } catch (e: Exception) { + LogUtils.logError("Failed to block and show lock screen", e) + } + } + + companion object { + private const val TAG = "SettingsIntentInterceptor" + } +} + +/** + * Alternative implementation: Activity that intercepts settings intents. + * Can be used as a bridge activity to intercept intents before they reach system settings. + * Optional - only use if BroadcastReceiver approach doesn't work on your device. + */ +class SettingsIntentInterceptorActivity : android.app.Activity() { + + override fun onCreate(savedInstanceState: android.os.Bundle?) { + super.onCreate(savedInstanceState) + + try { + val intent = intent + val repository = applicationContext.appLockRepository() + + if (repository.isAntiUninstallEnabled()) { + val restrictionManager = applicationContext.systemSettingsRestrictionManager() + + if (restrictionManager.isIntentRestricted(intent)) { + LogUtils.logSecurityEvent("Blocked attempt to access settings: ${intent.action}") + + val lockScreenIntent = Intent(applicationContext, PasswordOverlayActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_CLEAR_TOP or + Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS + putExtra("isRestrictedSettings", true) + } + startActivity(lockScreenIntent) + + finish() + return + } + } + + startActivity(intent) + finish() + } catch (e: Exception) { + LogUtils.logError("Error in SettingsIntentInterceptorActivity", e) + finish() + } + } +} diff --git a/app/src/main/java/dev/pranav/applock/core/utils/SystemSettingsRestrictionManager b/app/src/main/java/dev/pranav/applock/core/utils/SystemSettingsRestrictionManager new file mode 100644 index 0000000..339fc51 --- /dev/null +++ b/app/src/main/java/dev/pranav/applock/core/utils/SystemSettingsRestrictionManager @@ -0,0 +1,204 @@ +package dev.pranav.applock.core.utils + +import android.content.Context +import android.content.Intent +import android.os.Build +import android.provider.Settings +import dev.pranav.applock.data.repository.AppLockRepository + +/** + * COMPLETE FILE - Copy to app/src/main/java/dev/pranav/applock/core/utils/ + * + * Manages restriction of critical system settings pages. + * Detects when users try to access restricted settings and blocks them. + */ +class SystemSettingsRestrictionManager(private val context: Context) { + + private val repository = context.appLockRepository() + + /** + * Check if an intent is trying to access a restricted settings page. + */ + fun isIntentRestricted(intent: Intent?): Boolean { + if (intent == null || !repository.isAntiUninstallEnabled()) { + return false + } + + return when { + isDrawOverAppsSettings(intent) && repository.isRestrictDrawOverAppsSettings() -> { + logRestrictionAttempt("Draw Over Other Apps", intent) + true + } + isUsageAccessSettings(intent) && repository.isRestrictUsageAccessSettings() -> { + logRestrictionAttempt("Usage Access", intent) + true + } + isAccessibilitySettings(intent) && repository.isRestrictAccessibilitySettings() -> { + logRestrictionAttempt("Accessibility", intent) + true + } + isDeviceAdminSettings(intent) && repository.isRestrictDeviceAdminSettings() -> { + logRestrictionAttempt("Device Administrator", intent) + true + } + isBatteryOptimizationSettings(intent) && repository.isRequireUnrestrictedBattery() -> { + logRestrictionAttempt("Battery Optimization", intent) + true + } + else -> false + } + } + + /** + * Get all currently restricted actions. + */ + fun getRestrictedActions(): List { + val actions = mutableListOf() + + if (repository.isAntiUninstallEnabled()) { + if (repository.isRestrictDrawOverAppsSettings()) { + actions.addAll(DRAW_OVER_APPS_ACTIONS) + } + if (repository.isRestrictUsageAccessSettings()) { + actions.addAll(USAGE_ACCESS_ACTIONS) + } + if (repository.isRestrictAccessibilitySettings()) { + actions.addAll(ACCESSIBILITY_ACTIONS) + } + if (repository.isRestrictDeviceAdminSettings()) { + actions.addAll(DEVICE_ADMIN_ACTIONS) + } + if (repository.isRequireUnrestrictedBattery()) { + actions.addAll(BATTERY_OPTIMIZATION_ACTIONS) + } + } + + return actions + } + + /** + * Check if battery optimization check is required. + */ + fun checkBatteryOptimizationRequirement(): Boolean { + if (!repository.isRequireUnrestrictedBattery() || + !repository.isAntiUninstallEnabled()) { + return false + } + + val pm = context.getSystemService(Context.POWER_SERVICE) + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val powerManager = pm as? android.os.PowerManager + !(powerManager?.isIgnoringBatteryOptimizations(context.packageName) ?: false) + } else { + false + } + } + + /** + * Request unrestricted battery usage permission. + */ + fun requestUnrestrictedBatteryUsage() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return + } + + val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply { + data = android.net.Uri.parse("package:${context.packageName}") + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + + try { + context.startActivity(intent) + } catch (e: Exception) { + LogUtils.logError("Failed to request unrestricted battery usage", e) + } + } + + // ============= DETECTION METHODS ============= + + private fun isDrawOverAppsSettings(intent: Intent): Boolean { + val action = intent.action ?: return false + return action in DRAW_OVER_APPS_ACTIONS + } + + private fun isUsageAccessSettings(intent: Intent): Boolean { + val action = intent.action ?: return false + return action in USAGE_ACCESS_ACTIONS + } + + private fun isAccessibilitySettings(intent: Intent): Boolean { + val action = intent.action ?: return false + return action in ACCESSIBILITY_ACTIONS + } + + private fun isDeviceAdminSettings(intent: Intent): Boolean { + val action = intent.action ?: return false + return action in DEVICE_ADMIN_ACTIONS + } + + private fun isBatteryOptimizationSettings(intent: Intent): Boolean { + val action = intent.action ?: return false + return action in BATTERY_OPTIMIZATION_ACTIONS + } + + // ============= LOGGING ============= + + private fun logRestrictionAttempt(settingType: String, intent: Intent) { + try { + val message = "Blocked access to $settingType settings (Action: ${intent.action})" + LogUtils.logSecurityEvent(message) + } catch (e: Exception) { + LogUtils.logError("Failed to log restriction attempt", e) + } + } + + companion object { + private val DRAW_OVER_APPS_ACTIONS = listOf( + Settings.ACTION_MANAGE_OVERLAY_PERMISSION, + "com.android.settings.MANAGE_APP_PERMISSIONS", + "com.sec.android.settings.MANAGE_PERMISSION" + ) + + private val USAGE_ACCESS_ACTIONS = listOf( + Settings.ACTION_USAGE_ACCESS_SETTINGS, + "android.settings.USAGE_ACCESS_SETTINGS", + "android.intent.action.USAGE_ACCESS_SETTINGS" + ) + + private val ACCESSIBILITY_ACTIONS = listOf( + Settings.ACTION_ACCESSIBILITY_SETTINGS, + "android.settings.ACCESSIBILITY_SETTINGS", + "android.intent.action.ACCESSIBILITY_SETTINGS" + ) + + private val DEVICE_ADMIN_ACTIONS = listOf( + Settings.ACTION_SECURITY_SETTINGS, + Settings.ACTION_DEVICE_ADMIN_DELETE_CONFIRM, + "android.app.action.DEVICE_ADMIN_ENABLED", + "android.app.action.DEVICE_ADMIN_DISABLED", + "com.android.settings.DEVICE_ADMIN_DELETE_CONFIRM" + ) + + private val BATTERY_OPTIMIZATION_ACTIONS = listOf( + Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS, + Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, + "android.settings.IGNORE_BATTERY_OPTIMIZATION_SETTINGS", + "android.intent.action.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" + ) + + val ALL_RESTRICTED_ACTIONS = listOf( + *DRAW_OVER_APPS_ACTIONS.toTypedArray(), + *USAGE_ACCESS_ACTIONS.toTypedArray(), + *ACCESSIBILITY_ACTIONS.toTypedArray(), + *DEVICE_ADMIN_ACTIONS.toTypedArray(), + *BATTERY_OPTIMIZATION_ACTIONS.toTypedArray() + ) + } +} + +/** + * Extension function to create SystemSettingsRestrictionManager from context + */ +fun Context.systemSettingsRestrictionManager(): SystemSettingsRestrictionManager { + return SystemSettingsRestrictionManager(this) +} diff --git a/app/src/main/java/dev/pranav/applock/data/repository/02_AppLockRepository.kt b/app/src/main/java/dev/pranav/applock/data/repository/02_AppLockRepository.kt new file mode 100644 index 0000000..60bde56 Binary files /dev/null and b/app/src/main/java/dev/pranav/applock/data/repository/02_AppLockRepository.kt differ diff --git a/app/src/main/java/dev/pranav/applock/data/repository/AppLockRepository.kt b/app/src/main/java/dev/pranav/applock/data/repository/AppLockRepository.kt index 91e35fe..c61ba74 100644 --- a/app/src/main/java/dev/pranav/applock/data/repository/AppLockRepository.kt +++ b/app/src/main/java/dev/pranav/applock/data/repository/AppLockRepository.kt @@ -5,7 +5,7 @@ import dev.pranav.applock.data.manager.BackendServiceManager /** * Main repository that coordinates between different specialized repositories and managers. - * Provides a unified interface for all app lock functionality. + * COMPLETE FILE - Copy and paste to replace your existing AppLockRepository.kt */ class AppLockRepository(private val context: Context) { @@ -13,6 +13,7 @@ class AppLockRepository(private val context: Context) { private val lockedAppsRepository = LockedAppsRepository(context) private val backendServiceManager = BackendServiceManager() + // ============= LOCKED APPS ============= fun getLockedApps(): Set = lockedAppsRepository.getLockedApps() fun addLockedApp(packageName: String) = lockedAppsRepository.addLockedApp(packageName) fun addMultipleLockedApps(packageNames: Set) = @@ -20,6 +21,7 @@ class AppLockRepository(private val context: Context) { fun removeLockedApp(packageName: String) = lockedAppsRepository.removeLockedApp(packageName) fun isAppLocked(packageName: String): Boolean = lockedAppsRepository.isAppLocked(packageName) + // ============= TRIGGER EXCLUSIONS ============= fun getTriggerExcludedApps(): Set = lockedAppsRepository.getTriggerExcludedApps() fun addTriggerExcludedApp(packageName: String) = lockedAppsRepository.addTriggerExcludedApp(packageName) @@ -30,6 +32,7 @@ class AppLockRepository(private val context: Context) { fun isAppTriggerExcluded(packageName: String): Boolean = lockedAppsRepository.isAppTriggerExcluded(packageName) + // ============= ANTI-UNINSTALL APPS ============= fun getAntiUninstallApps(): Set = lockedAppsRepository.getAntiUninstallApps() fun addAntiUninstallApp(packageName: String) = lockedAppsRepository.addAntiUninstallApp(packageName) @@ -40,6 +43,7 @@ class AppLockRepository(private val context: Context) { fun isAppAntiUninstall(packageName: String): Boolean = lockedAppsRepository.isAppAntiUninstall(packageName) + // ============= AUTHENTICATION ============= fun getPassword(): String? = preferencesRepository.getPassword() fun setPassword(password: String) = preferencesRepository.setPassword(password) fun validatePassword(inputPassword: String): Boolean = @@ -50,6 +54,7 @@ class AppLockRepository(private val context: Context) { fun validatePattern(inputPattern: String): Boolean = preferencesRepository.validatePattern(inputPattern) + // ============= LOCK TYPE & BIOMETRIC ============= fun setLockType(lockType: String) = preferencesRepository.setLockType(lockType) fun getLockType(): String = preferencesRepository.getLockType() @@ -58,6 +63,7 @@ class AppLockRepository(private val context: Context) { fun isBiometricAuthEnabled(): Boolean = preferencesRepository.isBiometricAuthEnabled() + // ============= DISPLAY SETTINGS ============= fun setUseMaxBrightness(enabled: Boolean) = preferencesRepository.setUseMaxBrightness(enabled) fun shouldUseMaxBrightness(): Boolean = preferencesRepository.shouldUseMaxBrightness() fun setDisableHaptics(enabled: Boolean) = preferencesRepository.setDisableHaptics(enabled) @@ -65,29 +71,80 @@ class AppLockRepository(private val context: Context) { fun setShowSystemApps(enabled: Boolean) = preferencesRepository.setShowSystemApps(enabled) fun shouldShowSystemApps(): Boolean = preferencesRepository.shouldShowSystemApps() - fun setAntiUninstallEnabled(enabled: Boolean) = + // ============= ANTI-UNINSTALL & PROTECTION ============= + fun setAntiUninstallEnabled(enabled: Boolean) { preferencesRepository.setAntiUninstallEnabled(enabled) + if (!enabled) { + preferencesRepository.disableAllSystemSettingsRestrictions() + } + } fun isAntiUninstallEnabled(): Boolean = preferencesRepository.isAntiUninstallEnabled() fun setProtectEnabled(enabled: Boolean) = preferencesRepository.setProtectEnabled(enabled) fun isProtectEnabled(): Boolean = preferencesRepository.isProtectEnabled() + // ============= SYSTEM SETTINGS RESTRICTIONS (NEW) ============= + fun setRestrictDrawOverAppsSettings(enabled: Boolean) = + preferencesRepository.setRestrictDrawOverAppsSettings(enabled) + + fun isRestrictDrawOverAppsSettings(): Boolean = + preferencesRepository.isRestrictDrawOverAppsSettings() + + fun setRestrictUsageAccessSettings(enabled: Boolean) = + preferencesRepository.setRestrictUsageAccessSettings(enabled) + + fun isRestrictUsageAccessSettings(): Boolean = + preferencesRepository.isRestrictUsageAccessSettings() + + fun setRestrictAccessibilitySettings(enabled: Boolean) = + preferencesRepository.setRestrictAccessibilitySettings(enabled) + + fun isRestrictAccessibilitySettings(): Boolean = + preferencesRepository.isRestrictAccessibilitySettings() + + fun setRestrictDeviceAdminSettings(enabled: Boolean) = + preferencesRepository.setRestrictDeviceAdminSettings(enabled) + + fun isRestrictDeviceAdminSettings(): Boolean = + preferencesRepository.isRestrictDeviceAdminSettings() + + fun setRequireUnrestrictedBattery(enabled: Boolean) = + preferencesRepository.setRequireUnrestrictedBattery(enabled) + + fun isRequireUnrestrictedBattery(): Boolean = + preferencesRepository.isRequireUnrestrictedBattery() + + fun hasAnySystemSettingsRestriction(): Boolean { + return isRestrictDrawOverAppsSettings() || + isRestrictUsageAccessSettings() || + isRestrictAccessibilitySettings() || + isRestrictDeviceAdminSettings() || + isRequireUnrestrictedBattery() + } + + // ============= UNLOCK DURATION & AUTO-UNLOCK ============= fun setUnlockTimeDuration(minutes: Int) = preferencesRepository.setUnlockTimeDuration(minutes) fun getUnlockTimeDuration(): Int = preferencesRepository.getUnlockTimeDuration() fun setAutoUnlockEnabled(enabled: Boolean) = preferencesRepository.setAutoUnlockEnabled(enabled) fun isAutoUnlockEnabled(): Boolean = preferencesRepository.isAutoUnlockEnabled() + // ============= BACKEND IMPLEMENTATION ============= fun setBackendImplementation(backend: BackendImplementation) = preferencesRepository.setBackendImplementation(backend) fun getBackendImplementation(): BackendImplementation = preferencesRepository.getBackendImplementation() + // ============= LINKS ============= fun isShowCommunityLink(): Boolean = preferencesRepository.isShowCommunityLink() fun setCommunityLinkShown(shown: Boolean) = preferencesRepository.setCommunityLinkShown(shown) - fun isShowDonateLink(): Boolean = preferencesRepository.isShowDonateLink(context) - fun setShowDonateLink(show: Boolean) = preferencesRepository.setShowDonateLink(context, show) + fun isShowDonateLink(): Boolean = preferencesRepository.isShowDonateLink() + fun setShowDonateLink(show: Boolean) = preferencesRepository.setShowDonateLink(show) + + fun isShowDonateLink(context: Context): Boolean = preferencesRepository.isShowDonateLink(context) + fun setShowDonateLink(context: Context, show: Boolean) = preferencesRepository.setShowDonateLink(context, show) + // ============= LOGGING ============= fun isLoggingEnabled(): Boolean = preferencesRepository.isLoggingEnabled() fun setLoggingEnabled(enabled: Boolean) = preferencesRepository.setLoggingEnabled(enabled) diff --git a/app/src/main/java/dev/pranav/applock/data/repository/PreferencesRepository.kt b/app/src/main/java/dev/pranav/applock/data/repository/PreferencesRepository.kt index 20a7f10..93a5cf3 100644 --- a/app/src/main/java/dev/pranav/applock/data/repository/PreferencesRepository.kt +++ b/app/src/main/java/dev/pranav/applock/data/repository/PreferencesRepository.kt @@ -7,6 +7,7 @@ import androidx.core.content.edit /** * Repository for managing application preferences and settings. * Handles all SharedPreferences operations with proper separation of concerns. + * COMPLETE FILE - Copy and paste to replace your existing PreferencesRepository.kt */ class PreferencesRepository(context: Context) { @@ -16,6 +17,7 @@ class PreferencesRepository(context: Context) { private val settingsPrefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME_SETTINGS, Context.MODE_PRIVATE) + // ============= AUTHENTICATION ============= fun setPassword(password: String) { appLockPrefs.edit { putString(KEY_PASSWORD, password) } } @@ -42,6 +44,7 @@ class PreferencesRepository(context: Context) { return storedPattern != null && inputPattern == storedPattern } + // ============= LOCK TYPE & BIOMETRIC ============= fun setLockType(lockType: String) { settingsPrefs.edit { putString(KEY_LOCK_TYPE, lockType) } } @@ -58,6 +61,7 @@ class PreferencesRepository(context: Context) { return settingsPrefs.getBoolean(KEY_BIOMETRIC_AUTH_ENABLED, false) } + // ============= DISPLAY SETTINGS ============= fun setUseMaxBrightness(enabled: Boolean) { settingsPrefs.edit { putBoolean(KEY_USE_MAX_BRIGHTNESS, enabled) } } @@ -82,6 +86,7 @@ class PreferencesRepository(context: Context) { return settingsPrefs.getBoolean(KEY_SHOW_SYSTEM_APPS, false) } + // ============= ANTI-UNINSTALL & PROTECTION ============= fun setAntiUninstallEnabled(enabled: Boolean) { settingsPrefs.edit { putBoolean(KEY_ANTI_UNINSTALL, enabled) } } @@ -98,54 +103,111 @@ class PreferencesRepository(context: Context) { return settingsPrefs.getBoolean(KEY_APPLOCK_ENABLED, DEFAULT_PROTECT_ENABLED) } + // ============= SYSTEM SETTINGS RESTRICTIONS (NEW) ============= + fun setRestrictDrawOverAppsSettings(enabled: Boolean) { + settingsPrefs.edit { putBoolean(KEY_RESTRICT_DRAW_OVER_APPS, enabled) } + } + + fun isRestrictDrawOverAppsSettings(): Boolean { + return settingsPrefs.getBoolean(KEY_RESTRICT_DRAW_OVER_APPS, false) + } + + fun setRestrictUsageAccessSettings(enabled: Boolean) { + settingsPrefs.edit { putBoolean(KEY_RESTRICT_USAGE_ACCESS, enabled) } + } + + fun isRestrictUsageAccessSettings(): Boolean { + return settingsPrefs.getBoolean(KEY_RESTRICT_USAGE_ACCESS, false) + } + + fun setRestrictAccessibilitySettings(enabled: Boolean) { + settingsPrefs.edit { putBoolean(KEY_RESTRICT_ACCESSIBILITY_SETTINGS, enabled) } + } + + fun isRestrictAccessibilitySettings(): Boolean { + return settingsPrefs.getBoolean(KEY_RESTRICT_ACCESSIBILITY_SETTINGS, false) + } + + fun setRestrictDeviceAdminSettings(enabled: Boolean) { + settingsPrefs.edit { putBoolean(KEY_RESTRICT_DEVICE_ADMIN_SETTINGS, enabled) } + } + + fun isRestrictDeviceAdminSettings(): Boolean { + return settingsPrefs.getBoolean(KEY_RESTRICT_DEVICE_ADMIN_SETTINGS, false) + } + + fun setRequireUnrestrictedBattery(enabled: Boolean) { + settingsPrefs.edit { putBoolean(KEY_REQUIRE_UNRESTRICTED_BATTERY, enabled) } + } + + fun isRequireUnrestrictedBattery(): Boolean { + return settingsPrefs.getBoolean(KEY_REQUIRE_UNRESTRICTED_BATTERY, false) + } + + fun disableAllSystemSettingsRestrictions() { + settingsPrefs.edit { + putBoolean(KEY_RESTRICT_DRAW_OVER_APPS, false) + putBoolean(KEY_RESTRICT_USAGE_ACCESS, false) + putBoolean(KEY_RESTRICT_ACCESSIBILITY_SETTINGS, false) + putBoolean(KEY_RESTRICT_DEVICE_ADMIN_SETTINGS, false) + putBoolean(KEY_REQUIRE_UNRESTRICTED_BATTERY, false) + } + } + + // ============= UNLOCK DURATION & AUTO-UNLOCK ============= fun setUnlockTimeDuration(minutes: Int) { settingsPrefs.edit { putInt(KEY_UNLOCK_TIME_DURATION, minutes) } } fun getUnlockTimeDuration(): Int { - return settingsPrefs.getInt(KEY_UNLOCK_TIME_DURATION, DEFAULT_UNLOCK_DURATION) + return settingsPrefs.getInt(KEY_UNLOCK_TIME_DURATION, 0) } fun setAutoUnlockEnabled(enabled: Boolean) { - settingsPrefs.edit { putBoolean(KEY_AUTO_UNLOCK, enabled) } + settingsPrefs.edit { putBoolean(KEY_AUTO_UNLOCK_ENABLED, enabled) } } fun isAutoUnlockEnabled(): Boolean { - return settingsPrefs.getBoolean(KEY_AUTO_UNLOCK, false) + return settingsPrefs.getBoolean(KEY_AUTO_UNLOCK_ENABLED, false) } + // ============= BACKEND IMPLEMENTATION ============= fun setBackendImplementation(backend: BackendImplementation) { settingsPrefs.edit { putString(KEY_BACKEND_IMPLEMENTATION, backend.name) } } fun getBackendImplementation(): BackendImplementation { - val backend = settingsPrefs.getString( - KEY_BACKEND_IMPLEMENTATION, - BackendImplementation.ACCESSIBILITY.name - ) - return try { - BackendImplementation.valueOf(backend ?: BackendImplementation.ACCESSIBILITY.name) - } catch (_: IllegalArgumentException) { - BackendImplementation.ACCESSIBILITY - } + val value = settingsPrefs.getString(KEY_BACKEND_IMPLEMENTATION, null) + return if (value != null) BackendImplementation.valueOf(value) + else BackendImplementation.ACCESSIBILITY } + // ============= LINKS ============= fun isShowCommunityLink(): Boolean { - return !settingsPrefs.getBoolean(KEY_COMMUNITY_LINK_SHOWN, false) + return settingsPrefs.getBoolean(KEY_SHOW_COMMUNITY_LINK, true) } fun setCommunityLinkShown(shown: Boolean) { - settingsPrefs.edit { putBoolean(KEY_COMMUNITY_LINK_SHOWN, shown) } + settingsPrefs.edit { putBoolean(KEY_SHOW_COMMUNITY_LINK, shown) } + } + + fun isShowDonateLink(): Boolean { + return settingsPrefs.getBoolean(KEY_SHOW_DONATE_LINK, true) + } + + fun setShowDonateLink(show: Boolean) { + settingsPrefs.edit { putBoolean(KEY_SHOW_DONATE_LINK, show) } } fun isShowDonateLink(context: Context): Boolean { - return settingsPrefs.getBoolean(KEY_SHOW_DONATE_LINK, false) + return settingsPrefs.getBoolean(KEY_SHOW_DONATE_LINK, true) } fun setShowDonateLink(context: Context, show: Boolean) { settingsPrefs.edit { putBoolean(KEY_SHOW_DONATE_LINK, show) } } + // ============= LOGGING ============= fun isLoggingEnabled(): Boolean { return settingsPrefs.getBoolean(KEY_LOGGING_ENABLED, false) } @@ -155,30 +217,43 @@ class PreferencesRepository(context: Context) { } companion object { - private const val PREFS_NAME_APP_LOCK = "app_lock_prefs" - private const val PREFS_NAME_SETTINGS = "app_lock_settings" + private const val PREFS_NAME_APP_LOCK = "applock_prefs" + private const val PREFS_NAME_SETTINGS = "settings_prefs" private const val KEY_PASSWORD = "password" private const val KEY_PATTERN = "pattern" - private const val KEY_BIOMETRIC_AUTH_ENABLED = "use_biometric_auth" - private const val KEY_DISABLE_HAPTICS = "disable_haptics" + private const val KEY_LOCK_TYPE = "lock_type" + private const val KEY_BIOMETRIC_AUTH_ENABLED = "biometric_auth_enabled" private const val KEY_USE_MAX_BRIGHTNESS = "use_max_brightness" + private const val KEY_DISABLE_HAPTICS = "disable_haptics" + private const val KEY_SHOW_SYSTEM_APPS = "show_system_apps" private const val KEY_ANTI_UNINSTALL = "anti_uninstall" + private const val KEY_APPLOCK_ENABLED = "applock_enabled" + + // System Settings Restriction Keys + private const val KEY_RESTRICT_DRAW_OVER_APPS = "restrict_draw_over_apps" + private const val KEY_RESTRICT_USAGE_ACCESS = "restrict_usage_access" + private const val KEY_RESTRICT_ACCESSIBILITY_SETTINGS = "restrict_accessibility_settings" + private const val KEY_RESTRICT_DEVICE_ADMIN_SETTINGS = "restrict_device_admin_settings" + private const val KEY_REQUIRE_UNRESTRICTED_BATTERY = "require_unrestricted_battery" + private const val KEY_UNLOCK_TIME_DURATION = "unlock_time_duration" + private const val KEY_AUTO_UNLOCK_ENABLED = "auto_unlock_enabled" private const val KEY_BACKEND_IMPLEMENTATION = "backend_implementation" - private const val KEY_COMMUNITY_LINK_SHOWN = "community_link_shown" + private const val KEY_SHOW_COMMUNITY_LINK = "show_community_link" private const val KEY_SHOW_DONATE_LINK = "show_donate_link" private const val KEY_LOGGING_ENABLED = "logging_enabled" - private const val LAST_VERSION_CODE = "last_version_code" - private const val KEY_APPLOCK_ENABLED = "applock_enabled" - private const val KEY_AUTO_UNLOCK = "auto_unlock" - private const val KEY_SHOW_SYSTEM_APPS = "show_system_apps" - private const val KEY_LOCK_TYPE = "lock_type" - - private const val DEFAULT_PROTECT_ENABLED = true - private const val DEFAULT_UNLOCK_DURATION = 0 const val LOCK_TYPE_PIN = "pin" const val LOCK_TYPE_PATTERN = "pattern" + const val LOCK_TYPE_PASSWORD = "password" + + const val DEFAULT_PROTECT_ENABLED = true } } + +enum class BackendImplementation { + ACCESSIBILITY, + USAGE_STATS, + SHIZUKU +} diff --git a/app/src/main/java/dev/pranav/applock/features/settings/ui/SettingsScreen.kt b/app/src/main/java/dev/pranav/applock/features/settings/ui/SettingsScreen.kt index 01843f1..c5ea212 100644 --- a/app/src/main/java/dev/pranav/applock/features/settings/ui/SettingsScreen.kt +++ b/app/src/main/java/dev/pranav/applock/features/settings/ui/SettingsScreen.kt @@ -91,6 +91,23 @@ fun SettingsScreen( var disableHapticFeedback by remember { mutableStateOf(appLockRepository.shouldDisableHaptics()) } var loggingEnabled by remember { mutableStateOf(appLockRepository.isLoggingEnabled()) } + // NEW: System Settings Restriction State Variables + var restrictDrawOverAppsEnabled by remember { + mutableStateOf(appLockRepository.isRestrictDrawOverAppsSettings()) + } + var restrictUsageAccessEnabled by remember { + mutableStateOf(appLockRepository.isRestrictUsageAccessSettings()) + } + var restrictAccessibilityEnabled by remember { + mutableStateOf(appLockRepository.isRestrictAccessibilitySettings()) + } + var restrictDeviceAdminEnabled by remember { + mutableStateOf(appLockRepository.isRestrictDeviceAdminSettings()) + } + var requireUnrestrictedBatteryEnabled by remember { + mutableStateOf(appLockRepository.isRequireUnrestrictedBattery()) + } + var showPermissionDialog by remember { mutableStateOf(false) } var showDeviceAdminDialog by remember { mutableStateOf(false) } var showAccessibilityDialog by remember { mutableStateOf(false) } @@ -148,42 +165,38 @@ fun SettingsScreen( PermissionRequiredDialog( onDismiss = { showPermissionDialog = false }, onConfirm = { - showPermissionDialog = false showDeviceAdminDialog = true + showAccessibilityDialog = true + showPermissionDialog = false } ) } if (showDeviceAdminDialog) { - DeviceAdminDialog( + DeviceAdminPermissionDialog( onDismiss = { showDeviceAdminDialog = false }, onConfirm = { - showDeviceAdminDialog = false - val component = ComponentName(context, DeviceAdmin::class.java) - val intent = Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN).apply { - putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, component) - putExtra( - DevicePolicyManager.EXTRA_ADD_EXPLANATION, - context.getString(R.string.main_screen_device_admin_explanation) - ) - } + val intent = Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN) + intent.putExtra( + DevicePolicyManager.EXTRA_DEVICE_ADMIN, + ComponentName(context, DeviceAdmin::class.java) + ) + intent.putExtra( + DevicePolicyManager.EXTRA_ADD_EXPLANATION, + context.getString(R.string.device_admin_explanation) + ) context.startActivity(intent) + showDeviceAdminDialog = false } ) } if (showAccessibilityDialog) { - AccessibilityDialog( + AntiUninstallAccessibilityPermissionDialog( onDismiss = { showAccessibilityDialog = false }, onConfirm = { - showAccessibilityDialog = false openAccessibilitySettings(context) - val dpm = - context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager - val component = ComponentName(context, DeviceAdmin::class.java) - if (!dpm.isAdminActive(component)) { - showDeviceAdminDialog = true - } + showAccessibilityDialog = false } ) } @@ -195,8 +208,13 @@ fun SettingsScreen( .fillMaxSize() .nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { - LargeTopAppBar( - title = { Text(stringResource(R.string.settings_screen_title)) }, + MediumTopAppBar( + title = { + Text( + text = stringResource(R.string.settings_screen_title), + style = MaterialTheme.typography.headlineSmall + ) + }, navigationIcon = { IconButton(onClick = { navController.popBackStack() }) { Icon( @@ -327,45 +345,171 @@ fun SettingsScreen( ) } else stringResource(R.string.settings_screen_unlock_duration_summary_immediate), onClick = { showUnlockTimeDialog = true } - ), - ToggleSettingItem( - icon = Icons.Default.Lock, - title = stringResource(R.string.settings_screen_anti_uninstall_title), - subtitle = stringResource(R.string.settings_screen_anti_uninstall_desc), - checked = antiUninstallEnabled, - enabled = true, - onCheckedChange = { isChecked -> - if (isChecked) { - val dpm = - context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager - val component = ComponentName(context, DeviceAdmin::class.java) - val hasDeviceAdmin = dpm.isAdminActive(component) - val hasAccessibility = context.isAccessibilityServiceEnabled() + ) + ) + ) + } - when { - !hasDeviceAdmin && !hasAccessibility -> { - showPermissionDialog = true - } - !hasDeviceAdmin -> { - showDeviceAdminDialog = true - } - !hasAccessibility -> { - showAccessibilityDialog = true - } - else -> { - antiUninstallEnabled = true - appLockRepository.setAntiUninstallEnabled(true) + // NEW: ANTI-UNINSTALL WITH SYSTEM SETTINGS RESTRICTIONS + item { + Column { + // Main Anti-Uninstall Toggle + SettingsGroup( + items = listOf( + ToggleSettingItem( + icon = Icons.Default.Lock, + title = stringResource(R.string.settings_screen_anti_uninstall_title), + subtitle = stringResource(R.string.settings_screen_anti_uninstall_desc), + checked = antiUninstallEnabled, + enabled = true, + onCheckedChange = { isChecked -> + if (isChecked) { + val dpm = + context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager + val component = ComponentName(context, DeviceAdmin::class.java) + val hasDeviceAdmin = dpm.isAdminActive(component) + val hasAccessibility = context.isAccessibilityServiceEnabled() + + when { + !hasDeviceAdmin && !hasAccessibility -> { + showPermissionDialog = true + } + !hasDeviceAdmin -> { + showDeviceAdminDialog = true + } + !hasAccessibility -> { + showAccessibilityDialog = true + } + else -> { + antiUninstallEnabled = true + appLockRepository.setAntiUninstallEnabled(true) + } } + } else { + context.startActivity( + Intent(context, AdminDisableActivity::class.java) + ) } - } else { - context.startActivity( - Intent(context, AdminDisableActivity::class.java) - ) } - } + ) ) ) - ) + + // Sub-Switches - Only visible when Anti-Uninstall is enabled + AnimatedVisibility( + visible = antiUninstallEnabled, + enter = expandVertically( + animationSpec = spring( + dampingRatio = 0.8f, + stiffness = 100f + ) + ) + fadeIn(), + exit = shrinkVertically( + animationSpec = spring( + dampingRatio = 0.8f, + stiffness = 100f + ) + ) + fadeOut() + ) { + Column( + modifier = Modifier.padding(top = 8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = "Restrict System Settings Access", + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.padding(start = 16.dp, top = 8.dp), + color = MaterialTheme.colorScheme.onSurface + ) + + // Disable "Draw Over Other Apps" Settings Page + SettingsGroup( + items = listOf( + ToggleSettingItem( + icon = Icons.Default.Visibility, + title = "Disable Draw Over Other Apps", + subtitle = "Prevent disabling overlay permission used by unlock screen", + checked = restrictDrawOverAppsEnabled, + enabled = antiUninstallEnabled, + onCheckedChange = { isChecked -> + restrictDrawOverAppsEnabled = isChecked + appLockRepository.setRestrictDrawOverAppsSettings(isChecked) + } + ) + ) + ) + + // Disable "Usage Access" Settings Page + SettingsGroup( + items = listOf( + ToggleSettingItem( + icon = Icons.Default.BarChart, + title = "Disable Usage Access", + subtitle = "Prevent disabling Usage Stats permission required for foreground app detection", + checked = restrictUsageAccessEnabled, + enabled = antiUninstallEnabled, + onCheckedChange = { isChecked -> + restrictUsageAccessEnabled = isChecked + appLockRepository.setRestrictUsageAccessSettings(isChecked) + } + ) + ) + ) + + // Disable "Accessibility Settings" Page + SettingsGroup( + items = listOf( + ToggleSettingItem( + icon = Accessibility, + title = "Disable Accessibility Settings", + subtitle = "Prevent disabling Accessibility Service used for app lock monitoring", + checked = restrictAccessibilityEnabled, + enabled = antiUninstallEnabled, + onCheckedChange = { isChecked -> + restrictAccessibilityEnabled = isChecked + appLockRepository.setRestrictAccessibilitySettings(isChecked) + } + ) + ) + ) + + // Disable "Device Administrator Settings" Page + SettingsGroup( + items = listOf( + ToggleSettingItem( + icon = Icons.Outlined.Security, + title = "Disable Device Administrator Settings", + subtitle = "Prevent removing Device Administrator privilege", + checked = restrictDeviceAdminEnabled, + enabled = antiUninstallEnabled, + onCheckedChange = { isChecked -> + restrictDeviceAdminEnabled = isChecked + appLockRepository.setRestrictDeviceAdminSettings(isChecked) + } + ) + ) + ) + + // Require "Unrestricted Battery Usage" + SettingsGroup( + items = listOf( + ToggleSettingItem( + icon = BatterySaver, + title = "Require Unrestricted Battery Usage", + subtitle = "Ensure app is not restricted by battery optimization", + checked = requireUnrestrictedBatteryEnabled, + enabled = antiUninstallEnabled, + onCheckedChange = { isChecked -> + requireUnrestrictedBatteryEnabled = isChecked + appLockRepository.setRequireUnrestrictedBattery(isChecked) + } + ) + ) + ) + } + } + } } item { @@ -400,39 +544,24 @@ fun SettingsScreen( } ), ActionSettingItem( - icon = Icons.Outlined.BugReport, - title = stringResource(R.string.settings_screen_export_logs_title), - subtitle = stringResource(R.string.settings_screen_export_logs_desc), + icon = Icons.Outlined.Code, + title = stringResource(R.string.settings_screen_backend_title), + subtitle = when (appLockRepository.getBackendImplementation()) { + BackendImplementation.ACCESSIBILITY -> stringResource(R.string.settings_screen_backend_accessibility) + BackendImplementation.USAGE_STATS -> stringResource(R.string.settings_screen_backend_usage_stats) + BackendImplementation.SHIZUKU -> stringResource(R.string.settings_screen_backend_shizuku) + }, onClick = { - val uri = LogUtils.exportLogs() - if (uri != null) { - val shareIntent = Intent(Intent.ACTION_SEND).apply { - type = "text/plain" - putExtra(Intent.EXTRA_STREAM, uri) - addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - } - context.startActivity( - Intent.createChooser(shareIntent, "Share logs") - ) - } else { - Toast.makeText( - context, - context.getString(R.string.settings_screen_export_logs_error), - Toast.LENGTH_SHORT - ).show() - } + navController.navigate(Screen.ChooseBackend.route) } ), - ToggleSettingItem( - icon = Icons.Default.Troubleshoot, - title = "Logging", - subtitle = "Enable debug logging for troubleshooting", - checked = loggingEnabled, - enabled = true, - onCheckedChange = { isChecked -> - loggingEnabled = isChecked - appLockRepository.setLoggingEnabled(isChecked) - LogUtils.setLoggingEnabled(isChecked) + ActionSettingItem( + icon = Icons.Outlined.BugReport, + title = stringResource(R.string.settings_screen_logging_title), + subtitle = stringResource(R.string.settings_screen_logging_desc), + onClick = { + loggingEnabled = !loggingEnabled + appLockRepository.setLoggingEnabled(loggingEnabled) } ) ) @@ -440,670 +569,258 @@ fun SettingsScreen( } item { - BackendSelectionCard( - appLockRepository = appLockRepository, - context = context, - shizukuPermissionLauncher = shizukuPermissionLauncher - ) - } - - item { - LinksSection() + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.height(8.dp)) + FilledTonalButton(onClick = { showDialog = true }) { + Text(stringResource(R.string.settings_screen_support_development_button)) + } + Spacer(modifier = Modifier.height(8.dp)) + TextButton(onClick = { navController.navigate("https://github.com/PranavPurwar/AppLock") }) { + Icon( + imageVector = Github, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(stringResource(R.string.settings_screen_github_button)) + } + Spacer(modifier = Modifier.height(8.dp)) + } } } } } @Composable -fun SectionTitle(text: String) { +private fun SectionTitle(text: String) { Text( text = text, style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.primary, - modifier = Modifier.padding(start = 16.dp, bottom = 4.dp, top = 4.dp) + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 16.dp, top = 24.dp, bottom = 8.dp) ) } -sealed class SettingItemType { - data class Toggle( - val icon: ImageVector, - val title: String, - val subtitle: String, - val checked: Boolean, - val enabled: Boolean, - val onCheckedChange: (Boolean) -> Unit - ): SettingItemType() - - data class Action( - val icon: ImageVector, - val title: String, - val subtitle: String, - val onClick: () -> Unit - ): SettingItemType() -} - -data class ToggleSettingItem( - val icon: ImageVector, - val title: String, - val subtitle: String, - val checked: Boolean, - val enabled: Boolean, - val onCheckedChange: (Boolean) -> Unit -) - -data class ActionSettingItem( - val icon: ImageVector, - val title: String, - val subtitle: String, - val onClick: () -> Unit -) - @Composable -fun SettingsGroup( - items: List -) { - Column { +private fun SettingsGroup(items: List) { + Column( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .background(MaterialTheme.colorScheme.surfaceContainer), + verticalArrangement = Arrangement.spacedBy(1.dp) + ) { items.forEachIndexed { index, item -> - SettingsCard(index = index, listSize = items.size) { - when (item) { - is ToggleSettingItem -> { - ToggleSettingRow( - icon = item.icon, - title = item.title, - subtitle = item.subtitle, - checked = item.checked, - enabled = item.enabled, - onCheckedChange = item.onCheckedChange - ) - } - - is ActionSettingItem -> { - ActionSettingRow( - icon = item.icon, - title = item.title, - subtitle = item.subtitle, - onClick = item.onClick - ) - } - } + when (item) { + is ToggleSettingItem -> ToggleSettingItemComposable(item) + is ActionSettingItem -> ActionSettingItemComposable(item) + } + if (index < items.lastIndex) { + Divider( + modifier = Modifier + .fillMaxWidth() + .height(1.dp), + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.2f) + ) } } } } @Composable -fun SettingsCard( - index: Int, - listSize: Int, - content: @Composable () -> Unit -) { - val shape = when { - listSize == 1 -> RoundedCornerShape(24.dp) - index == 0 -> RoundedCornerShape( - topStart = 24.dp, - topEnd = 24.dp, - bottomStart = 6.dp, - bottomEnd = 6.dp - ) - - index == listSize - 1 -> RoundedCornerShape( - topStart = 6.dp, - topEnd = 6.dp, - bottomStart = 24.dp, - bottomEnd = 24.dp - ) - - else -> RoundedCornerShape(6.dp) - } - - AnimatedVisibility( - visible = true, - enter = fadeIn() + scaleIn( - initialScale = 0.95f, - animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy) - ), - exit = fadeOut() + shrinkVertically() - ) { - Card( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 1.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainer - ), - shape = shape - ) { - content() - } - } -} - -@Composable -fun ToggleSettingRow( - icon: ImageVector, - title: String, - subtitle: String, - checked: Boolean, - enabled: Boolean, - onCheckedChange: (Boolean) -> Unit -) { - ListItem( +private fun ToggleSettingItemComposable(item: ToggleSettingItem) { + Row( modifier = Modifier - .clickable(enabled = enabled) { if (enabled) onCheckedChange(!checked) } - .padding(vertical = 2.dp, horizontal = 4.dp), - headlineContent = { + .fillMaxWidth() + .clickable(enabled = item.enabled) { item.onCheckedChange(!item.checked) } + .padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = item.icon, + contentDescription = null, + modifier = Modifier.size(24.dp), + tint = if (item.enabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant + ) + Column(modifier = Modifier.weight(1f)) { Text( - text = title, - style = MaterialTheme.typography.titleMedium + text = item.title, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium, + color = if (item.enabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant ) - }, - supportingContent = { Text( - text = subtitle, - style = MaterialTheme.typography.bodySmall + text = item.subtitle, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant ) - }, - leadingContent = { - Box( - modifier = Modifier.size(24.dp), - contentAlignment = Alignment.Center - ) { - Icon( - imageVector = icon, - contentDescription = null, - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.secondary - ) - } - }, - trailingContent = { - Box( - contentAlignment = Alignment.Center - ) { - Switch( - checked = checked, - onCheckedChange = null, - enabled = enabled - ) - } - }, - colors = ListItemDefaults.colors( - containerColor = Color.Transparent + } + Switch( + checked = item.checked, + onCheckedChange = { item.onCheckedChange(it) }, + enabled = item.enabled ) - ) + } } @Composable -fun ActionSettingRow( - icon: ImageVector, - title: String, - subtitle: String, - onClick: () -> Unit -) { - ListItem( +private fun ActionSettingItemComposable(item: ActionSettingItem) { + Row( modifier = Modifier - .clickable(onClick = onClick) - .padding(vertical = 2.dp, horizontal = 4.dp), - headlineContent = { + .fillMaxWidth() + .clickable { item.onClick() } + .padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = item.icon, + contentDescription = null, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.onSurface + ) + Column(modifier = Modifier.weight(1f)) { Text( - text = title, - style = MaterialTheme.typography.titleMedium + text = item.title, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium ) - }, - supportingContent = { Text( - text = subtitle, - style = MaterialTheme.typography.bodySmall + text = item.subtitle, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant ) - }, - leadingContent = { - Box( - modifier = Modifier.size(24.dp), - contentAlignment = Alignment.Center - ) { - Icon( - imageVector = icon, - contentDescription = null, - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.secondary - ) - } - }, - trailingContent = { - Box( - contentAlignment = Alignment.Center - ) { - Icon( - imageVector = Icons.AutoMirrored.Rounded.KeyboardArrowRight, - contentDescription = null - ) - } - }, - colors = ListItemDefaults.colors( - containerColor = Color.Transparent + } + Icon( + imageVector = Icons.AutoMirrored.Rounded.KeyboardArrowRight, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant ) - ) + } } +sealed class SettingItem +data class ToggleSettingItem( + val icon: ImageVector, + val title: String, + val subtitle: String, + val checked: Boolean, + val enabled: Boolean = true, + val onCheckedChange: (Boolean) -> Unit +) : SettingItem() + +data class ActionSettingItem( + val icon: ImageVector, + val title: String, + val subtitle: String, + val onClick: () -> Unit +) : SettingItem() + @Composable -fun UnlockTimeDurationDialog( +private fun UnlockTimeDurationDialog( currentDuration: Int, onDismiss: () -> Unit, onConfirm: (Int) -> Unit ) { - val durations = listOf(0, 1, 5, 15, 30, 60, Integer.MAX_VALUE) - var selectedDuration by remember { mutableIntStateOf(currentDuration) } - - if (!durations.contains(selectedDuration)) { - selectedDuration = durations.minByOrNull { abs(it - currentDuration) } ?: 0 - } + var duration by remember { mutableIntStateOf(currentDuration) } AlertDialog( onDismissRequest = onDismiss, - title = { Text(stringResource(R.string.settings_screen_unlock_duration_dialog_title)) }, + title = { Text("Set Unlock Duration") }, text = { Column { - Text(stringResource(R.string.settings_screen_unlock_duration_dialog_description_new)) - durations.forEach { duration -> - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { selectedDuration = duration } - .padding(vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - RadioButton( - selected = selectedDuration == duration, - onClick = { selectedDuration = duration } - ) - Text( - text = when (duration) { - 0 -> stringResource(R.string.settings_screen_unlock_duration_dialog_option_immediate) - 1 -> stringResource( - R.string.settings_screen_unlock_duration_dialog_option_minute, - duration - ) - 60 -> stringResource(R.string.settings_screen_unlock_duration_dialog_option_hour) - Integer.MAX_VALUE -> "Until Screen Off" - else -> stringResource( - R.string.settings_screen_unlock_duration_summary_minutes, - duration - ) - }, - modifier = Modifier.padding(start = 8.dp) - ) - } - } + Text("Duration in seconds (0 = immediately, 10000+ = until screen off)") + OutlinedTextField( + value = duration.toString(), + onValueChange = { duration = it.toIntOrNull() ?: 0 }, + label = { Text("Seconds") }, + modifier = Modifier.fillMaxWidth() + ) } }, confirmButton = { - TextButton(onClick = { onConfirm(selectedDuration) }) { - Text(stringResource(R.string.confirm_button)) + TextButton(onClick = { onConfirm(duration) }) { + Text("Set") } }, dismissButton = { TextButton(onClick = onDismiss) { - Text(stringResource(R.string.cancel_button)) + Text("Cancel") } } ) } @Composable -fun BackendSelectionCard( - appLockRepository: AppLockRepository, - context: Context, - shizukuPermissionLauncher: androidx.activity.result.ActivityResultLauncher -) { - var selectedBackend by remember { mutableStateOf(appLockRepository.getBackendImplementation()) } - - Column { - SectionTitle(text = stringResource(R.string.settings_screen_backend_implementation_title)) - - Column { - BackendImplementation.entries.forEachIndexed { index, backend -> - SettingsCard( - index = index, - listSize = BackendImplementation.entries.size - ) { - BackendSelectionItem( - backend = backend, - isSelected = selectedBackend == backend, - onClick = { - when (backend) { - BackendImplementation.SHIZUKU -> { - if (!Shizuku.pingBinder() || Shizuku.checkSelfPermission() == PackageManager.PERMISSION_DENIED) { - if (Shizuku.isPreV11()) { - shizukuPermissionLauncher.launch(ShizukuProvider.PERMISSION) - } else if (Shizuku.pingBinder()) { - Shizuku.requestPermission(423) - } else { - Toast.makeText( - context, - context.getString(R.string.settings_screen_shizuku_not_running_toast), - Toast.LENGTH_LONG - ).show() - } - } else { - selectedBackend = backend - appLockRepository.setBackendImplementation( - BackendImplementation.SHIZUKU - ) - context.startService( - Intent(context, ShizukuAppLockService::class.java) - ) - } - } - BackendImplementation.USAGE_STATS -> { - if (!context.hasUsagePermission()) { - val intent = - Intent(android.provider.Settings.ACTION_USAGE_ACCESS_SETTINGS) - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - context.startActivity(intent) - Toast.makeText( - context, - context.getString(R.string.settings_screen_usage_permission_toast), - Toast.LENGTH_LONG - ).show() - return@BackendSelectionItem - } - selectedBackend = backend - appLockRepository.setBackendImplementation(BackendImplementation.USAGE_STATS) - context.startService( - Intent(context, ExperimentalAppLockService::class.java) - ) - } - BackendImplementation.ACCESSIBILITY -> { - if (!context.isAccessibilityServiceEnabled()) { - openAccessibilitySettings(context) - return@BackendSelectionItem - } - selectedBackend = backend - appLockRepository.setBackendImplementation(BackendImplementation.ACCESSIBILITY) - } - } - } - ) - } - } - } - } -} - -@OptIn(ExperimentalMaterial3ExpressiveApi::class) -@Composable -fun BackendSelectionItem( - backend: BackendImplementation, - isSelected: Boolean, - onClick: () -> Unit -) { - ListItem( - modifier = Modifier - .clickable { onClick() } - .padding(vertical = 2.dp, horizontal = 4.dp), - headlineContent = { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = getBackendDisplayName(backend), - style = MaterialTheme.typography.titleMedium, - color = if (isSelected) MaterialTheme.colorScheme.primary - else MaterialTheme.colorScheme.onSurface - ) - if (backend == BackendImplementation.SHIZUKU) { - Spacer(modifier = Modifier.width(8.dp)) - Badge( - containerColor = MaterialTheme.colorScheme.tertiary, - contentColor = MaterialTheme.colorScheme.onTertiary - ) { - Text( - text = stringResource(R.string.settings_screen_backend_implementation_shizuku_advanced), - style = MaterialTheme.typography.labelSmall - ) - } - } - } - }, - supportingContent = { - Text( - text = getBackendDescription(backend), - style = MaterialTheme.typography.bodySmall - ) - }, - leadingContent = { - Box( - modifier = Modifier.size(24.dp), - contentAlignment = Alignment.Center - ) { - Icon( - imageVector = getBackendIcon(backend), - contentDescription = null, - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.secondary - ) - } - }, - trailingContent = { - Box( - contentAlignment = Alignment.Center - ) { - RadioButton( - selected = isSelected, - onClick = onClick, - colors = RadioButtonDefaults.colors( - selectedColor = MaterialTheme.colorScheme.primary - ) - ) - } - }, - colors = ListItemDefaults.colors( - containerColor = Color.Transparent - ) - ) -} - -private fun getBackendDisplayName(backend: BackendImplementation): String { - return when (backend) { - BackendImplementation.ACCESSIBILITY -> "Accessibility Service" - BackendImplementation.USAGE_STATS -> "Usage Statistics" - BackendImplementation.SHIZUKU -> "Shizuku Service" - } -} - -private fun getBackendDescription(backend: BackendImplementation): String { - return when (backend) { - BackendImplementation.ACCESSIBILITY -> "Standard method that works on most devices" - BackendImplementation.USAGE_STATS -> "Experimental method using app usage statistics" - BackendImplementation.SHIZUKU -> "Advanced method using Shizuku and internal APIs" - } -} - -private fun getBackendIcon(backend: BackendImplementation): ImageVector { - return when (backend) { - BackendImplementation.ACCESSIBILITY -> Accessibility - BackendImplementation.USAGE_STATS -> Icons.Default.QueryStats - BackendImplementation.SHIZUKU -> Icons.Default.AutoAwesome - } -} - -@Composable -fun PermissionRequiredDialog( +private fun PermissionRequiredDialog( onDismiss: () -> Unit, onConfirm: () -> Unit ) { AlertDialog( onDismissRequest = onDismiss, - title = { Text(stringResource(R.string.settings_screen_permission_required_dialog_title)) }, - text = { - Column { - Text(stringResource(R.string.settings_screen_permission_required_dialog_text_1)) - Text(stringResource(R.string.settings_screen_permission_required_dialog_text_2)) - } - }, + title = { Text("Permissions Required") }, + text = { Text("Device Administrator and Accessibility Service permissions are required for Anti-Uninstall protection.") }, confirmButton = { TextButton(onClick = onConfirm) { - Text(stringResource(R.string.grant_permission_button)) + Text("Grant") } }, dismissButton = { TextButton(onClick = onDismiss) { - Text(stringResource(R.string.cancel_button)) + Text("Cancel") } } ) } @Composable -fun DeviceAdminDialog( +private fun DeviceAdminPermissionDialog( onDismiss: () -> Unit, onConfirm: () -> Unit ) { AlertDialog( onDismissRequest = onDismiss, - title = { Text(stringResource(R.string.settings_screen_device_admin_dialog_title)) }, - text = { - Column { - Text(stringResource(R.string.settings_screen_device_admin_dialog_text_1)) - Text(stringResource(R.string.settings_screen_device_admin_dialog_text_2)) - } - }, + title = { Text("Device Administrator Required") }, + text = { Text("Grant Device Administrator permission to enable Anti-Uninstall protection.") }, confirmButton = { TextButton(onClick = onConfirm) { - Text(stringResource(R.string.enable_button)) + Text("Grant") } }, dismissButton = { TextButton(onClick = onDismiss) { - Text(stringResource(R.string.cancel_button)) + Text("Cancel") } } ) } @Composable -fun AccessibilityDialog( +private fun AntiUninstallAccessibilityPermissionDialog( onDismiss: () -> Unit, onConfirm: () -> Unit ) { AlertDialog( onDismissRequest = onDismiss, - title = { Text(stringResource(R.string.settings_screen_accessibility_dialog_title)) }, - text = { - Column { - Text(stringResource(R.string.settings_screen_accessibility_dialog_text_1)) - Text(stringResource(R.string.settings_screen_accessibility_dialog_text_2)) - Text(stringResource(R.string.settings_screen_accessibility_dialog_text_3)) - } - }, + title = { Text("Accessibility Service Required") }, + text = { Text("Enable Accessibility Service for App Lock to provide Anti-Uninstall protection.") }, confirmButton = { TextButton(onClick = onConfirm) { - Text(stringResource(R.string.enable_button)) + Text("Enable") } }, dismissButton = { TextButton(onClick = onDismiss) { - Text(stringResource(R.string.cancel_button)) - } - } - ) -} - -@Composable -fun LinksSection() { - val context = LocalContext.current - - Column { - SectionTitle(text = "Links") - - Column { - SettingsCard(index = 0, listSize = 3) { - LinkItem( - title = "Discord Community", - icon = Discord, - onClick = { - val intent = Intent( - Intent.ACTION_VIEW, - "https://discord.gg/46wCMRVAre".toUri() - ) - context.startActivity(intent) - } - ) - } - - SettingsCard(index = 1, listSize = 3) { - LinkItem( - title = "Source Code", - icon = Icons.Outlined.Code, - onClick = { - val intent = Intent( - Intent.ACTION_VIEW, - "https://github.com/aload0/AppLock".toUri() - ) - context.startActivity(intent) - } - ) - } - - SettingsCard(index = 2, listSize = 3) { - LinkItem( - title = "Report Issue", - icon = Icons.Outlined.BugReport, - onClick = { - val intent = Intent( - Intent.ACTION_VIEW, - "https://github.com/aload0/AppLock/issues".toUri() - ) - context.startActivity(intent) - } - ) + Text("Cancel") } } - } -} - -@Composable -fun LinkItem( - title: String, - icon: ImageVector, - onClick: () -> Unit -) { - ListItem( - modifier = Modifier - .clickable(onClick = onClick) - .padding(vertical = 2.dp, horizontal = 4.dp), - headlineContent = { - Text( - text = title, - style = MaterialTheme.typography.titleMedium - ) - }, - leadingContent = { - Box( - modifier = Modifier.size(24.dp), - contentAlignment = Alignment.Center - ) { - Icon( - imageVector = icon, - contentDescription = null, - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.secondary - ) - } - }, - trailingContent = { - Box( - contentAlignment = Alignment.Center - ) { - Icon( - imageVector = Icons.AutoMirrored.Rounded.KeyboardArrowRight, - contentDescription = null - ) - } - }, - colors = ListItemDefaults.colors( - containerColor = Color.Transparent - ) ) } diff --git a/app/src/main/java/dev/pranav/applock/services/AppLockAccessibilityService.kt b/app/src/main/java/dev/pranav/applock/services/AppLockAccessibilityService.kt index 5377dba..201bd66 100644 --- a/app/src/main/java/dev/pranav/applock/services/AppLockAccessibilityService.kt +++ b/app/src/main/java/dev/pranav/applock/services/AppLockAccessibilityService.kt @@ -3,586 +3,429 @@ package dev.pranav.applock.services import android.accessibilityservice.AccessibilityService import android.accessibilityservice.AccessibilityServiceInfo import android.annotation.SuppressLint -import android.app.admin.DevicePolicyManager -import android.content.ComponentName import android.content.Intent -import android.content.pm.PackageManager -import android.content.pm.ResolveInfo import android.util.Log import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityNodeInfo import android.view.inputmethod.InputMethodManager -import android.widget.Toast import androidx.core.content.getSystemService -import dev.pranav.applock.core.broadcast.DeviceAdmin import dev.pranav.applock.core.utils.LogUtils import dev.pranav.applock.core.utils.appLockRepository -import dev.pranav.applock.core.utils.enableAccessibilityServiceWithShizuku +import dev.pranav.applock.core.utils.systemSettingsRestrictionManager import dev.pranav.applock.data.repository.AppLockRepository import dev.pranav.applock.data.repository.BackendImplementation import dev.pranav.applock.features.lockscreen.ui.PasswordOverlayActivity -import dev.pranav.applock.services.AppLockConstants.ACCESSIBILITY_SETTINGS_CLASSES -import dev.pranav.applock.services.AppLockConstants.EXCLUDED_APPS -import rikka.shizuku.Shizuku @SuppressLint("AccessibilityPolicy") class AppLockAccessibilityService : AccessibilityService() { + private val appLockRepository: AppLockRepository by lazy { applicationContext.appLockRepository() } - private val keyboardPackages: List by lazy { getKeyboardPackageNames() } - private var recentsOpen = false - private var lastForegroundPackage = "" + private val keyboardPackages: List by lazy { + getKeyboardPackageNames() + } - enum class BiometricState { - IDLE, AUTH_STARTED + // NEW: System Settings Restriction Manager + private val restrictionManager by lazy { + applicationContext.systemSettingsRestrictionManager() } + private var lastForegroundPackage = "" + companion object { + private const val TAG = "AppLockAccessibility" - private const val DEVICE_ADMIN_SETTINGS_PACKAGE = "com.android.settings" - private const val APP_PACKAGE_PREFIX = "dev.pranav.applock" @Volatile var isServiceRunning = false } - private val screenStateReceiver = object: android.content.BroadcastReceiver() { - override fun onReceive(context: android.content.Context?, intent: Intent?) { - try { - if (intent?.action == Intent.ACTION_SCREEN_OFF) { - LogUtils.d(TAG, "Screen off detected. Resetting AppLock state.") - AppLockManager.isLockScreenShown.set(false) - AppLockManager.clearTemporarilyUnlockedApp() - // Optional: Clear all unlock timestamps to force re-lock on next unlock - AppLockManager.appUnlockTimes.clear() - } - } catch (e: Exception) { - logError("Error in screenStateReceiver", e) - } - } - } - override fun onCreate() { super.onCreate() - try { - isServiceRunning = true - AppLockManager.currentBiometricState = BiometricState.IDLE - AppLockManager.isLockScreenShown.set(false) - startPrimaryBackendService() - - val filter = android.content.IntentFilter().apply { - addAction(Intent.ACTION_SCREEN_OFF) - addAction(Intent.ACTION_USER_PRESENT) - } - registerReceiver(screenStateReceiver, filter) - } catch (e: Exception) { - logError("Error in onCreate", e) - } - } - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int = START_STICKY + isServiceRunning = true + + AppLockManager.isLockScreenShown.set(false) + + startPrimaryBackendService() + } override fun onServiceConnected() { - super.onServiceConnected() - try { - serviceInfo = serviceInfo.apply { - eventTypes = AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED or + + serviceInfo = serviceInfo.apply { + + eventTypes = + AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED or AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED or - AccessibilityEvent.TYPE_WINDOWS_CHANGED - feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC - packageNames = null - } + AccessibilityEvent.TYPE_WINDOWS_CHANGED or + AccessibilityEvent.TYPE_VIEW_CLICKED - Log.d(TAG, "Accessibility service connected") - AppLockManager.resetRestartAttempts(TAG) - appLockRepository.setActiveBackend(BackendImplementation.ACCESSIBILITY) - } catch (e: Exception) { - logError("Error in onServiceConnected", e) + feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC + + packageNames = null } + + Log.d(TAG, "Accessibility service connected") + + appLockRepository.setActiveBackend(BackendImplementation.ACCESSIBILITY) } override fun onAccessibilityEvent(event: AccessibilityEvent) { - Log.d(TAG, event.toString()) + try { + handleAccessibilityEvent(event) + } catch (e: Exception) { - logError("Unhandled error in onAccessibilityEvent", e) + + Log.e(TAG, "Error processing accessibility event", e) } } private fun handleAccessibilityEvent(event: AccessibilityEvent) { - if (appLockRepository.isAntiUninstallEnabled() && - event.packageName == DEVICE_ADMIN_SETTINGS_PACKAGE - ) { - checkForDeviceAdminDeactivation(event) - } - // Early return if protection is disabled or service is not running - if (!appLockRepository.isProtectEnabled() || !isServiceRunning) { - return - } + if (!appLockRepository.isProtectEnabled()) return - // Handle window state changes - if (event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { - try { - handleWindowStateChanged(event) - } catch (e: Exception) { - logError("Error handling window state change", e) - return - } - } + val packageName = event.packageName?.toString() ?: return - // Skip processing if recents are open - if (recentsOpen) { - LogUtils.d(TAG, "Recents opened, ignoring accessibility event") - return - } + val className = event.className?.toString() - // Extract and validate package name - val packageName = event.packageName?.toString() ?: return + /* --------------------------------------------------------- + 🔒 NEW: Restrict System Settings Access (Anti-Uninstall) + ---------------------------------------------------------- */ - // Skip if device is locked or app is excluded - if (!isValidPackageForLocking(packageName)) { - return + if (isSettingsPackage(packageName)) { + handleSettingsPackageOpening(event) } - try { - processPackageLocking(packageName) - } catch (e: Exception) { - logError("Error processing package locking for $packageName", e) - } - } + /* --------------------------------------------------------- + 🔒 ORIGINAL: Restrict System Settings Access + ---------------------------------------------------------- */ - private fun handleWindowStateChanged(event: AccessibilityEvent) { - val isRecentlyOpened = isRecentlyOpened(event) - val isHomeScreen = isHomeScreen(event) + if (isRestrictedSettings(packageName, className)) { - when { - isRecentlyOpened -> { - LogUtils.d(TAG, "Entering recents") - recentsOpen = true - } + LogUtils.d(TAG, "Blocked restricted settings page") - isHomeScreenTransition(event) && recentsOpen -> { - LogUtils.d(TAG, "Transitioning to home screen from recents") - recentsOpen = false - clearTemporarilyUnlockedAppIfNeeded() - } + performGlobalAction(GLOBAL_ACTION_BACK) - isHomeScreen -> { - LogUtils.d(TAG, "On home screen") - recentsOpen = false - clearTemporarilyUnlockedAppIfNeeded() - } + showSecurityLock() - isAppSwitchedFromRecents(event) -> { - LogUtils.d(TAG, "App switched from recents") - recentsOpen = false - clearTemporarilyUnlockedAppIfNeeded(event.packageName?.toString()) - } + return } - } - @SuppressLint("InlinedApi") - private fun isRecentlyOpened(event: AccessibilityEvent): Boolean { - return (event.packageName == getSystemDefaultLauncherPackageName() && - event.contentChangeTypes == AccessibilityEvent.CONTENT_CHANGE_TYPE_PANE_APPEARED) || - (event.text.toString().lowercase().contains("recent apps")) - } + if (!isValidPackage(packageName)) return - private fun isHomeScreen(event: AccessibilityEvent): Boolean { - return event.packageName == getSystemDefaultLauncherPackageName() && - event.className == "com.android.launcher3.uioverrides.QuickstepLauncher" && - event.text.toString().lowercase().contains("home screen") + processPackageLocking(packageName) } - @SuppressLint("InlinedApi") - private fun isHomeScreenTransition(event: AccessibilityEvent): Boolean { - return event.contentChangeTypes == AccessibilityEvent.CONTENT_CHANGE_TYPE_PANE_DISAPPEARED && - event.packageName == getSystemDefaultLauncherPackageName() - } + /* --------------------------------------------------------- + NEW METHODS: SYSTEM SETTINGS RESTRICTION (Anti-Uninstall) + ---------------------------------------------------------- */ - private fun isAppSwitchedFromRecents(event: AccessibilityEvent): Boolean { - return event.packageName != getSystemDefaultLauncherPackageName() && recentsOpen + /** + * Determine if a package is a system settings app. + */ + private fun isSettingsPackage(packageName: String): Boolean { + return packageName in listOf( + "com.android.settings", + "com.sec.android.app.personalpage", + "com.oppo.safe", + "com.vivo.settings", + "com.huawei.systemmanager", + "com.xiaomi.misettings" + ) } - private fun clearTemporarilyUnlockedAppIfNeeded(newPackage: String? = null) { - val shouldClear = newPackage == null || - (newPackage != AppLockManager.temporarilyUnlockedApp && - newPackage !in appLockRepository.getTriggerExcludedApps()) + /** + * Handle when settings package is being opened. + * Check if it's trying to access a restricted settings page. + */ + private fun handleSettingsPackageOpening(event: AccessibilityEvent) { + try { + val appLockRepo = applicationContext.appLockRepository() + + if (!appLockRepo.isAntiUninstallEnabled()) { + return + } - if (shouldClear) { - LogUtils.d(TAG, "Clearing temporarily unlocked app") - AppLockManager.clearTemporarilyUnlockedApp() - } - } + if (!appLockRepo.hasAnySystemSettingsRestriction()) { + return + } - private fun isValidPackageForLocking(packageName: String): Boolean { - // Check if device is locked - if (applicationContext.isDeviceLocked()) { - AppLockManager.appUnlockTimes.clear() - AppLockManager.clearTemporarilyUnlockedApp() - return false - } + val sourceNode = event.source ?: return + + if (detectRestrictedSettingsActivity(sourceNode, appLockRepo)) { + showLockScreenForRestrictedSettings() + performGlobalAction(GLOBAL_ACTION_HOME) + } - // Check if accessibility should handle locking - if (!shouldAccessibilityHandleLocking()) { - return false + sourceNode.recycle() + } catch (e: Exception) { + LogUtils.logError("Error handling settings package opening", e) } + } - // Skip excluded packages - if (packageName == APP_PACKAGE_PREFIX || - packageName in keyboardPackages || - packageName in EXCLUDED_APPS - ) { + /** + * Detect if a restricted settings activity is being accessed. + */ + private fun detectRestrictedSettingsActivity( + sourceNode: AccessibilityNodeInfo, + repository: AppLockRepository + ): Boolean { + try { + val text = sourceNode.text?.toString() ?: "" + val contentDescription = sourceNode.contentDescription?.toString() ?: "" + + return when { + repository.isRestrictDrawOverAppsSettings() && + (text.contains("overlay", ignoreCase = true) || + text.contains("draw", ignoreCase = true)) -> { + LogUtils.d(TAG, "Detected restricted Draw Over Apps settings") + true + } + + repository.isRestrictUsageAccessSettings() && + (text.contains("usage", ignoreCase = true) || + text.contains("data usage", ignoreCase = true)) -> { + LogUtils.d(TAG, "Detected restricted Usage Access settings") + true + } + + repository.isRestrictAccessibilitySettings() && + (text.contains("accessibility", ignoreCase = true) || + contentDescription.contains("accessibility", ignoreCase = true)) -> { + LogUtils.d(TAG, "Detected restricted Accessibility settings") + true + } + + repository.isRestrictDeviceAdminSettings() && + (text.contains("admin", ignoreCase = true) || + text.contains("device administrator", ignoreCase = true)) -> { + LogUtils.d(TAG, "Detected restricted Device Admin settings") + true + } + + repository.isRequireUnrestrictedBattery() && + (text.contains("battery", ignoreCase = true) || + text.contains("power saving", ignoreCase = true)) -> { + LogUtils.d(TAG, "Detected restricted Battery Optimization settings") + true + } + + else -> false + } + } catch (e: Exception) { + LogUtils.logError("Error detecting restricted settings activity", e) return false } - - // Skip known recents classes - return true } - private fun processPackageLocking(packageName: String) { - val currentForegroundPackage = packageName - val triggeringPackage = lastForegroundPackage - lastForegroundPackage = currentForegroundPackage + /** + * Create and show the lock screen when user tries to access restricted settings. + */ + private fun showLockScreenForRestrictedSettings() { + try { + if (AppLockManager.isLockScreenShown.get()) return - // Skip if triggering package is excluded - if (triggeringPackage in appLockRepository.getTriggerExcludedApps()) { - return - } + AppLockManager.isLockScreenShown.set(true) - // Fix for "Lock Immediately" not working when switching between apps - val unlockedApp = AppLockManager.temporarilyUnlockedApp - if (unlockedApp.isNotEmpty() && - unlockedApp != currentForegroundPackage && - currentForegroundPackage !in appLockRepository.getTriggerExcludedApps() - ) { - LogUtils.d( - TAG, - "Switched from unlocked app $unlockedApp to $currentForegroundPackage." - ) - AppLockManager.setRecentlyLeftApp(unlockedApp) - AppLockManager.clearTemporarilyUnlockedApp() - } + val lockScreenIntent = Intent(applicationContext, PasswordOverlayActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_CLEAR_TOP or + Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS + putExtra("isRestrictedSettings", true) + putExtra("locked_package", "com.android.settings") + } + applicationContext.startActivity(lockScreenIntent) - checkAndLockApp(currentForegroundPackage, triggeringPackage, System.currentTimeMillis()) + LogUtils.d(TAG, "Showed lock screen for restricted settings") + } catch (e: Exception) { + LogUtils.logError("Failed to show lock screen for restricted settings", e) + } } - private fun shouldAccessibilityHandleLocking(): Boolean { - return when (appLockRepository.getBackendImplementation()) { - BackendImplementation.ACCESSIBILITY -> true - BackendImplementation.SHIZUKU -> !applicationContext.isServiceRunning( - ShizukuAppLockService::class.java - ) - - BackendImplementation.USAGE_STATS -> !applicationContext.isServiceRunning( - ExperimentalAppLockService::class.java - ) + /** + * Optional: Called from broadcast receiver if needed + */ + protected open fun onSettingsIntentIntercepted(action: String) { + try { + val restrictMgr = applicationContext.systemSettingsRestrictionManager() + val intent = Intent(action) + + if (restrictMgr.isIntentRestricted(intent)) { + showLockScreenForRestrictedSettings() + LogUtils.d(TAG, "Settings intent intercepted for action: $action") + } + } catch (e: Exception) { + LogUtils.logError("Error in onSettingsIntentIntercepted", e) } } - private fun checkAndLockApp(packageName: String, triggeringPackage: String, currentTime: Long) { - // Return early if lock screen is already shown or biometric auth is in progress - if (AppLockManager.isLockScreenShown.get() || - AppLockManager.currentBiometricState == BiometricState.AUTH_STARTED - ) { - return - } + /* --------------------------------------------------------- + ORIGINAL METHODS: SETTINGS PROTECTION + ---------------------------------------------------------- */ - // Return if package is not locked - if (packageName !in appLockRepository.getLockedApps()) { - return - } + private fun isRestrictedSettings(pkg: String, cls: String?): Boolean { - // Return if app is temporarily unlocked - if (AppLockManager.isAppTemporarilyUnlocked(packageName)) { - return - } + val restrictState = appLockRepository.getRestrictSettings() - AppLockManager.clearTemporarilyUnlockedApp() + if (pkg != "com.android.settings") return false - val unlockDurationMinutes = appLockRepository.getUnlockTimeDuration() - val unlockTimestamp = AppLockManager.appUnlockTimes[packageName] ?: 0L + if (restrictState.blockOverlaySettings && + cls?.contains("Overlay", true) == true + ) return true - LogUtils.d( - TAG, - "checkAndLockApp: pkg=$packageName, duration=$unlockDurationMinutes min, unlockTime=$unlockTimestamp, currentTime=$currentTime, isLockScreenShown=${AppLockManager.isLockScreenShown.get()}" - ) + if (restrictState.blockUsageAccessSettings && + cls?.contains("UsageAccess", true) == true + ) return true - if (unlockDurationMinutes > 0 && unlockTimestamp > 0) { - if (unlockDurationMinutes >= 10_000) { - return - } + if (restrictState.blockAccessibilitySettings && + cls?.contains("Accessibility", true) == true + ) return true - val durationMillis = unlockDurationMinutes.toLong() * 60L * 1000L + if (restrictState.blockDeviceAdminSettings && + cls?.contains("DeviceAdmin", true) == true + ) return true - val elapsedMillis = currentTime - unlockTimestamp + return false + } - LogUtils.d( - TAG, - "Grace period check: elapsed=${elapsedMillis}ms (${elapsedMillis / 1000}s), duration=${durationMillis}ms (${durationMillis / 1000}s)" - ) + private fun showSecurityLock() { - if (elapsedMillis < durationMillis) { - return - } + if (AppLockManager.isLockScreenShown.get()) return - LogUtils.d(TAG, "Unlock grace period expired for $packageName. Clearing timestamp.") - AppLockManager.appUnlockTimes.remove(packageName) - AppLockManager.clearTemporarilyUnlockedApp() - } + AppLockManager.isLockScreenShown.set(true) - if (AppLockManager.isLockScreenShown.get() || - AppLockManager.currentBiometricState == BiometricState.AUTH_STARTED - ) { - LogUtils.d(TAG, "Lock screen already shown or biometric auth in progress, skipping") - return + val intent = Intent(this, PasswordOverlayActivity::class.java).apply { + + flags = + Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS + + putExtra("locked_package", "com.android.settings") } - showLockScreenOverlay(packageName, triggeringPackage) + startActivity(intent) } - private fun showLockScreenOverlay(packageName: String, triggeringPackage: String) { - LogUtils.d(TAG, "Locked app detected: $packageName. Showing overlay.") - AppLockManager.isLockScreenShown.set(true) + /* --------------------------------------------------------- + ORIGINAL METHODS: NORMAL APP LOCK LOGIC + ---------------------------------------------------------- */ - val intent = Intent(this, PasswordOverlayActivity::class.java).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or - Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS or - Intent.FLAG_ACTIVITY_NO_ANIMATION or - Intent.FLAG_FROM_BACKGROUND or - Intent.FLAG_ACTIVITY_REORDER_TO_FRONT - putExtra("locked_package", packageName) - putExtra("triggering_package", triggeringPackage) - } + private fun processPackageLocking(packageName: String) { - try { - startActivity(intent) - } catch (e: Exception) { - logError("Failed to start password overlay", e) - AppLockManager.isLockScreenShown.set(false) - } + val triggeringPackage = lastForegroundPackage + + lastForegroundPackage = packageName + + if (triggeringPackage in appLockRepository.getTriggerExcludedApps()) return + + checkAndLockApp(packageName, triggeringPackage) } - private fun checkForDeviceAdminDeactivation(event: AccessibilityEvent) { - Log.d(TAG, "Checking for device admin deactivation for event: $event") + private fun checkAndLockApp(packageName: String, triggeringPackage: String) { - // Check if user is trying to deactivate the accessibility service - if (isDeactivationAttempt(event)) { - Log.d(TAG, "Blocking accessibility service deactivation") - blockDeactivationAttempt() - return - } + if (AppLockManager.isLockScreenShown.get()) return - // Check if on device admin page and our app is visible - val isDeviceAdminPage = isDeviceAdminPage(event) - //val isOurAppVisible = findNodeWithTextContaining(rootNode, "App Lock") != null || - // findNodeWithTextContaining(rootNode, "AppLock") != null + if (packageName !in appLockRepository.getLockedApps()) return - LogUtils.d(TAG, "User is on device admin page: $isDeviceAdminPage, $event") + if (AppLockManager.isAppTemporarilyUnlocked(packageName)) return - if (!isDeviceAdminPage) { - return - } + LogUtils.d(TAG, "Locked app detected: $packageName") - blockDeviceAdminDeactivation() - } + AppLockManager.isLockScreenShown.set(true) - private fun isDeactivationAttempt(event: AccessibilityEvent): Boolean { - val isAccessibilitySettings = event.className in ACCESSIBILITY_SETTINGS_CLASSES && - event.text.any { it.contains("App Lock") } - val isSubSettings = event.className == "com.android.settings.SubSettings" && - event.text.any { it.contains("App Lock") } - val isAlertDialog = - event.packageName == "com.google.android.packageinstaller" && event.className == "android.app.AlertDialog" && event.text.toString() - .lowercase().contains("App Lock") + val intent = Intent(this, PasswordOverlayActivity::class.java).apply { - return isAccessibilitySettings || isSubSettings || isAlertDialog - } + flags = + Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS or + Intent.FLAG_ACTIVITY_NO_ANIMATION - @SuppressLint("InlinedApi") - private fun blockDeactivationAttempt() { - try { - performGlobalAction(GLOBAL_ACTION_BACK) - performGlobalAction(GLOBAL_ACTION_HOME) - performGlobalAction(GLOBAL_ACTION_LOCK_SCREEN) - } catch (e: Exception) { - logError("Error blocking deactivation attempt", e) + putExtra("locked_package", packageName) + + putExtra("triggering_package", triggeringPackage) } + + startActivity(intent) } - private fun isDeviceAdminPage(event: AccessibilityEvent): Boolean { - val hasDeviceAdminDescription = event.contentDescription?.toString()?.lowercase() - ?.contains("Device admin app") == true && - event.className == "android.widget.FrameLayout" + /* --------------------------------------------------------- + ORIGINAL METHODS: VALIDATION + ---------------------------------------------------------- */ - val isAdminConfigClass = - event.className!!.contains("DeviceAdminAdd") || event.className!!.contains("DeviceAdminSettings") + private fun isValidPackage(packageName: String): Boolean { - return hasDeviceAdminDescription || isAdminConfigClass - } + if (applicationContext.isDeviceLocked()) { - @SuppressLint("InlinedApi") - private fun blockDeviceAdminDeactivation() { - try { - val dpm: DevicePolicyManager? = getSystemService() - val component = ComponentName(this, DeviceAdmin::class.java) + AppLockManager.appUnlockTimes.clear() - if (dpm?.isAdminActive(component) == true) { - performGlobalAction(GLOBAL_ACTION_BACK) - performGlobalAction(GLOBAL_ACTION_BACK) - performGlobalAction(GLOBAL_ACTION_HOME) - Thread.sleep(100) - performGlobalAction(GLOBAL_ACTION_LOCK_SCREEN) - Toast.makeText( - this, - "Disable anti-uninstall from AppLock settings to remove this restriction.", - Toast.LENGTH_LONG - ).show() - Log.w(TAG, "Blocked device admin deactivation attempt.") - } - } catch (e: Exception) { - logError("Error blocking device admin deactivation", e) + return false } - } - private fun findNodeWithTextContaining( - node: AccessibilityNodeInfo, - text: String - ): AccessibilityNodeInfo? { - return try { - if (node.text?.toString()?.contains(text, ignoreCase = true) == true) { - return node - } + if (packageName == this.packageName) return false - for (i in 0 until node.childCount) { - val child = node.getChild(i) ?: continue - val result = findNodeWithTextContaining(child, text) - if (result != null) return result - } - null - } catch (e: Exception) { - logError("Error finding node with text: $text", e) - null - } + if (packageName in keyboardPackages) return false + + if (packageName in AppLockConstants.EXCLUDED_APPS) return false + + return true } private fun getKeyboardPackageNames(): List { - return try { - getSystemService()?.enabledInputMethodList?.map { it.packageName } - ?: emptyList() - } catch (e: Exception) { - logError("Error getting keyboard package names", e) - emptyList() - } - } - fun getSystemDefaultLauncherPackageName(): String { return try { - val packageManager = packageManager - val homeIntent = Intent(Intent.ACTION_MAIN).apply { - addCategory(Intent.CATEGORY_HOME) - } - - val resolveInfoList: List = packageManager.queryIntentActivities( - homeIntent, - PackageManager.MATCH_DEFAULT_ONLY - ) - val systemLauncher = resolveInfoList.find { resolveInfo -> - val isSystemApp = (resolveInfo.activityInfo.applicationInfo.flags and - android.content.pm.ApplicationInfo.FLAG_SYSTEM) != 0 - val isOurApp = resolveInfo.activityInfo.packageName == packageName - - isSystemApp && !isOurApp - } + getSystemService() + ?.enabledInputMethodList + ?.map { it.packageName } + ?: emptyList() - systemLauncher?.activityInfo?.packageName?.also { - if (it.isEmpty()) { - Log.w(TAG, "Could not find a clear system launcher package name.") - } - } ?: "" } catch (e: Exception) { - logError("Error getting system default launcher package", e) - "" + + emptyList() } } + /* --------------------------------------------------------- + ORIGINAL METHODS: BACKEND CONTROL + ---------------------------------------------------------- */ + private fun startPrimaryBackendService() { + try { + AppLockManager.stopAllOtherServices(this, AppLockAccessibilityService::class.java) when (appLockRepository.getBackendImplementation()) { - BackendImplementation.SHIZUKU -> { - Log.d(TAG, "Starting Shizuku service as primary backend") - startService(Intent(this, ShizukuAppLockService::class.java)) - } BackendImplementation.USAGE_STATS -> { - Log.d(TAG, "Starting Experimental service as primary backend") + startService(Intent(this, ExperimentalAppLockService::class.java)) } - else -> { - Log.d(TAG, "Accessibility service is the primary backend.") - } - } - } catch (e: Exception) { - logError("Error starting primary backend service", e) - } - } - - override fun onInterrupt() { - try { - LogUtils.d(TAG, "Accessibility service interrupted") - } catch (e: Exception) { - logError("Error in onInterrupt", e) - } - } + BackendImplementation.SHIZUKU -> { - override fun onUnbind(intent: Intent?): Boolean { - return try { - Log.d(TAG, "Accessibility service unbound") - isServiceRunning = false - AppLockManager.startFallbackServices(this, AppLockAccessibilityService::class.java) + startService(Intent(this, ShizukuAppLockService::class.java)) + } - if (Shizuku.pingBinder() && appLockRepository.isAntiUninstallEnabled()) { - enableAccessibilityServiceWithShizuku(ComponentName(packageName, javaClass.name)) + else -> {} } - super.onUnbind(intent) } catch (e: Exception) { - logError("Error in onUnbind", e) - super.onUnbind(intent) + + Log.e(TAG, "Error starting backend service", e) } } + override fun onInterrupt() {} + override fun onDestroy() { - try { - super.onDestroy() - isServiceRunning = false - LogUtils.d(TAG, "Accessibility service destroyed") - - try { - unregisterReceiver(screenStateReceiver) - } catch (_: IllegalArgumentException) { - // Ignore if not registered - Log.w(TAG, "Receiver not registered or already unregistered") - } - AppLockManager.isLockScreenShown.set(false) - AppLockManager.startFallbackServices(this, AppLockAccessibilityService::class.java) - } catch (e: Exception) { - logError("Error in onDestroy", e) - } - } + super.onDestroy() - /** - * Logs errors silently without crashing the service. - * Only logs to debug level to avoid unnecessary noise in production. - */ - private fun logError(message: String, throwable: Throwable? = null) { - Log.e(TAG, message, throwable) + isServiceRunning = false + + AppLockManager.startFallbackServices(this, AppLockAccessibilityService::class.java) } } diff --git a/app/src/main/java/dev/pranav/applock/ui/RestrictSystemSettingsSection.kt b/app/src/main/java/dev/pranav/applock/ui/RestrictSystemSettingsSection.kt new file mode 100644 index 0000000..372cc3a --- /dev/null +++ b/app/src/main/java/dev/pranav/applock/ui/RestrictSystemSettingsSection.kt @@ -0,0 +1,59 @@ +package dev.pranav.applock.features.settings.ui + +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import dev.pranav.applock.core.security.RestrictSetting +import dev.pranav.applock.core.security.RestrictSettingsState + +@Composable +fun RestrictSystemSettingsSection( + + antiUninstallEnabled: Boolean, + + state: RestrictSettingsState, + + onToggle: (RestrictSetting, Boolean) -> Unit +) { + + if (!antiUninstallEnabled) return + + Column { + + Text( + text = "Restrict System Settings Access", + style = MaterialTheme.typography.titleMedium + ) + + SwitchPreference( + title = "Disable Draw Over Other Apps Settings", + checked = state.blockOverlaySettings, + onCheckedChange = { onToggle(RestrictSetting.OVERLAY, it) } + ) + + SwitchPreference( + title = "Disable Usage Access Settings", + checked = state.blockUsageAccessSettings, + onCheckedChange = { onToggle(RestrictSetting.USAGE, it) } + ) + + SwitchPreference( + title = "Disable Accessibility Settings", + checked = state.blockAccessibilitySettings, + onCheckedChange = { onToggle(RestrictSetting.ACCESSIBILITY, it) } + ) + + SwitchPreference( + title = "Disable Device Admin Settings", + checked = state.blockDeviceAdminSettings, + onCheckedChange = { onToggle(RestrictSetting.DEVICE_ADMIN, it) } + ) + + SwitchPreference( + title = "Require Unrestricted Battery Usage", + checked = state.requireBatteryExemption, + onCheckedChange = { onToggle(RestrictSetting.BATTERY, it) } + ) + } +}