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) }
+ )
+ }
+}