diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index 15e36ed..0000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index ddf443b..7d7d9ec 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -18,8 +18,8 @@ android {
minSdk = 26
targetSdk = 36
// targetSdkPreview = "CANARY"
- versionCode = 223
- versionName = "2.2.3"
+ versionCode = 22301
+ versionName = "2.2.3.01(08-03-26)"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8a00795..846d210 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -37,7 +37,6 @@
android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
- android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AppLock"
@@ -85,7 +84,7 @@
diff --git a/app/src/main/java/dev/pranav/applock/core/broadcast/DeviceAdmin.kt b/app/src/main/java/dev/pranav/applock/core/broadcast/DeviceAdmin.kt
index dca2388..1215203 100644
--- a/app/src/main/java/dev/pranav/applock/core/broadcast/DeviceAdmin.kt
+++ b/app/src/main/java/dev/pranav/applock/core/broadcast/DeviceAdmin.kt
@@ -17,10 +17,6 @@ class DeviceAdmin : DeviceAdminReceiver() {
context.getSharedPreferences("app_lock_settings", Context.MODE_PRIVATE).edit {
putBoolean("anti_uninstall", true)
}
-
- val component = ComponentName(context, DeviceAdmin::class.java)
-
- getManager(context).setUninstallBlocked(component, context.packageName, true)
}
override fun onDisabled(context: Context, intent: android.content.Intent) {
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..2bc3b3e 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
@@ -69,6 +69,31 @@ class AppLockRepository(private val context: Context) {
preferencesRepository.setAntiUninstallEnabled(enabled)
fun isAntiUninstallEnabled(): Boolean = preferencesRepository.isAntiUninstallEnabled()
+
+ fun setAntiUninstallAdminSettingsEnabled(enabled: Boolean) =
+ preferencesRepository.setAntiUninstallAdminSettingsEnabled(enabled)
+
+ fun isAntiUninstallAdminSettingsEnabled(): Boolean =
+ preferencesRepository.isAntiUninstallAdminSettingsEnabled()
+
+ fun setAntiUninstallUsageStatsEnabled(enabled: Boolean) =
+ preferencesRepository.setAntiUninstallUsageStatsEnabled(enabled)
+
+ fun isAntiUninstallUsageStatsEnabled(): Boolean =
+ preferencesRepository.isAntiUninstallUsageStatsEnabled()
+
+ fun setAntiUninstallAccessibilityEnabled(enabled: Boolean) =
+ preferencesRepository.setAntiUninstallAccessibilityEnabled(enabled)
+
+ fun isAntiUninstallAccessibilityEnabled(): Boolean =
+ preferencesRepository.isAntiUninstallAccessibilityEnabled()
+
+ fun setAntiUninstallOverlayEnabled(enabled: Boolean) =
+ preferencesRepository.setAntiUninstallOverlayEnabled(enabled)
+
+ fun isAntiUninstallOverlayEnabled(): Boolean =
+ preferencesRepository.isAntiUninstallOverlayEnabled()
+
fun setProtectEnabled(enabled: Boolean) = preferencesRepository.setProtectEnabled(enabled)
fun isProtectEnabled(): Boolean = preferencesRepository.isProtectEnabled()
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..7d1a584 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
@@ -17,7 +17,7 @@ class PreferencesRepository(context: Context) {
context.getSharedPreferences(PREFS_NAME_SETTINGS, Context.MODE_PRIVATE)
fun setPassword(password: String) {
- appLockPrefs.edit { putString(KEY_PASSWORD, password) }
+ appLockPrefs.edit(commit = true) { putString(KEY_PASSWORD, password) }
}
fun getPassword(): String? {
@@ -30,7 +30,7 @@ class PreferencesRepository(context: Context) {
}
fun setPattern(pattern: String) {
- appLockPrefs.edit { putString(KEY_PATTERN, pattern) }
+ appLockPrefs.edit(commit = true) { putString(KEY_PATTERN, pattern) }
}
fun getPattern(): String? {
@@ -43,7 +43,7 @@ class PreferencesRepository(context: Context) {
}
fun setLockType(lockType: String) {
- settingsPrefs.edit { putString(KEY_LOCK_TYPE, lockType) }
+ settingsPrefs.edit(commit = true) { putString(KEY_LOCK_TYPE, lockType) }
}
fun getLockType(): String {
@@ -83,15 +83,47 @@ class PreferencesRepository(context: Context) {
}
fun setAntiUninstallEnabled(enabled: Boolean) {
- settingsPrefs.edit { putBoolean(KEY_ANTI_UNINSTALL, enabled) }
+ settingsPrefs.edit(commit = true) { putBoolean(KEY_ANTI_UNINSTALL, enabled) }
}
fun isAntiUninstallEnabled(): Boolean {
return settingsPrefs.getBoolean(KEY_ANTI_UNINSTALL, false)
}
+ fun setAntiUninstallAdminSettingsEnabled(enabled: Boolean) {
+ settingsPrefs.edit(commit = true) { putBoolean(KEY_ANTI_UNINSTALL_ADMIN_SETTINGS, enabled) }
+ }
+
+ fun isAntiUninstallAdminSettingsEnabled(): Boolean {
+ return settingsPrefs.getBoolean(KEY_ANTI_UNINSTALL_ADMIN_SETTINGS, false)
+ }
+
+ fun setAntiUninstallUsageStatsEnabled(enabled: Boolean) {
+ settingsPrefs.edit(commit = true) { putBoolean(KEY_ANTI_UNINSTALL_USAGE_STATS, enabled) }
+ }
+
+ fun isAntiUninstallUsageStatsEnabled(): Boolean {
+ return settingsPrefs.getBoolean(KEY_ANTI_UNINSTALL_USAGE_STATS, false)
+ }
+
+ fun setAntiUninstallAccessibilityEnabled(enabled: Boolean) {
+ settingsPrefs.edit(commit = true) { putBoolean(KEY_ANTI_UNINSTALL_ACCESSIBILITY, enabled) }
+ }
+
+ fun isAntiUninstallAccessibilityEnabled(): Boolean {
+ return settingsPrefs.getBoolean(KEY_ANTI_UNINSTALL_ACCESSIBILITY, false)
+ }
+
+ fun setAntiUninstallOverlayEnabled(enabled: Boolean) {
+ settingsPrefs.edit(commit = true) { putBoolean(KEY_ANTI_UNINSTALL_OVERLAY, enabled) }
+ }
+
+ fun isAntiUninstallOverlayEnabled(): Boolean {
+ return settingsPrefs.getBoolean(KEY_ANTI_UNINSTALL_OVERLAY, false)
+ }
+
fun setProtectEnabled(enabled: Boolean) {
- settingsPrefs.edit { putBoolean(KEY_APPLOCK_ENABLED, enabled) }
+ settingsPrefs.edit(commit = true) { putBoolean(KEY_APPLOCK_ENABLED, enabled) }
}
fun isProtectEnabled(): Boolean {
@@ -115,7 +147,7 @@ class PreferencesRepository(context: Context) {
}
fun setBackendImplementation(backend: BackendImplementation) {
- settingsPrefs.edit { putString(KEY_BACKEND_IMPLEMENTATION, backend.name) }
+ settingsPrefs.edit(commit = true) { putString(KEY_BACKEND_IMPLEMENTATION, backend.name) }
}
fun getBackendImplementation(): BackendImplementation {
@@ -164,6 +196,10 @@ class PreferencesRepository(context: Context) {
private const val KEY_DISABLE_HAPTICS = "disable_haptics"
private const val KEY_USE_MAX_BRIGHTNESS = "use_max_brightness"
private const val KEY_ANTI_UNINSTALL = "anti_uninstall"
+ private const val KEY_ANTI_UNINSTALL_ADMIN_SETTINGS = "anti_uninstall_admin_settings"
+ private const val KEY_ANTI_UNINSTALL_USAGE_STATS = "anti_uninstall_usage_stats"
+ private const val KEY_ANTI_UNINSTALL_ACCESSIBILITY = "anti_uninstall_accessibility"
+ private const val KEY_ANTI_UNINSTALL_OVERLAY = "anti_uninstall_overlay"
private const val KEY_UNLOCK_TIME_DURATION = "unlock_time_duration"
private const val KEY_BACKEND_IMPLEMENTATION = "backend_implementation"
private const val KEY_COMMUNITY_LINK_SHOWN = "community_link_shown"
diff --git a/app/src/main/java/dev/pranav/applock/features/admin/AdminDisableActivity.kt b/app/src/main/java/dev/pranav/applock/features/admin/AdminDisableActivity.kt
index 09708a9..3eed7d4 100644
--- a/app/src/main/java/dev/pranav/applock/features/admin/AdminDisableActivity.kt
+++ b/app/src/main/java/dev/pranav/applock/features/admin/AdminDisableActivity.kt
@@ -73,6 +73,8 @@ class AdminDisableActivity : ComponentActivity() {
R.string.password_verified_admin,
Toast.LENGTH_SHORT
).show()
+
+ devicePolicyManager.removeActiveAdmin(deviceAdminComponentName)
appLockRepository.setAntiUninstallEnabled(false)
finish()
},
@@ -111,6 +113,8 @@ class AdminDisableActivity : ComponentActivity() {
R.string.password_verified_admin,
Toast.LENGTH_SHORT
).show()
+
+ devicePolicyManager.removeActiveAdmin(deviceAdminComponentName)
appLockRepository.setAntiUninstallEnabled(false)
finish()
},
diff --git a/app/src/main/java/dev/pranav/applock/features/appintro/ui/AppIntroScreen.kt b/app/src/main/java/dev/pranav/applock/features/appintro/ui/AppIntroScreen.kt
index a0cceae..86fb316 100644
--- a/app/src/main/java/dev/pranav/applock/features/appintro/ui/AppIntroScreen.kt
+++ b/app/src/main/java/dev/pranav/applock/features/appintro/ui/AppIntroScreen.kt
@@ -193,6 +193,7 @@ fun AppIntroScreen(navController: NavController) {
NotificationManagerCompat.from(context).areNotificationsEnabled()
}
accessibilityServiceEnabled = context.isAccessibilityServiceEnabled()
+ usageStatsPermissionGranted = context.hasUsagePermission()
}
val onFinishCallback = {
@@ -355,55 +356,55 @@ fun AppIntroScreen(navController: NavController) {
onNext = { true }
)
- val methodSpecificPages = when (selectedMethod) {
- AppUsageMethod.ACCESSIBILITY -> listOf(
- IntroPage(
- title = stringResource(R.string.accessibility_service_title),
- description = stringResource(R.string.app_intro_accessibility_desc),
- icon = Accessibility,
- backgroundColor = Color(0xFFF1550E),
- contentColor = Color.White,
- onNext = {
- accessibilityServiceEnabled = context.isAccessibilityServiceEnabled()
- if (!accessibilityServiceEnabled) {
- val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- context.startActivity(intent)
- false
- } else {
- context.appLockRepository()
- .setBackendImplementation(BackendImplementation.ACCESSIBILITY)
- true
- }
+ val mandatoryPermissionPages = listOf(
+ IntroPage(
+ title = stringResource(R.string.accessibility_service_title),
+ description = stringResource(R.string.app_intro_accessibility_desc),
+ icon = Accessibility,
+ backgroundColor = Color(0xFFF1550E),
+ contentColor = Color.White,
+ onNext = {
+ accessibilityServiceEnabled = context.isAccessibilityServiceEnabled()
+ if (!accessibilityServiceEnabled) {
+ val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ context.startActivity(intent)
+ false
+ } else {
+ true
}
- )
- )
-
- AppUsageMethod.USAGE_STATS -> listOf(
- IntroPage(
- title = stringResource(R.string.app_intro_usage_stats_title),
- description = stringResource(R.string.app_intro_usage_stats_desc),
- icon = Icons.Default.QueryStats,
- backgroundColor = Color(0xFFB453A4),
- contentColor = Color.White,
- onNext = {
- usageStatsPermissionGranted = context.hasUsagePermission()
- if (!usageStatsPermissionGranted) {
- val intent = Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS)
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- context.startActivity(intent)
- false
- } else {
- context.appLockRepository()
- .setBackendImplementation(BackendImplementation.USAGE_STATS)
- context.startService(
- Intent(context, ExperimentalAppLockService::class.java)
- )
- true
- }
+ }
+ ),
+ IntroPage(
+ title = stringResource(R.string.app_intro_usage_stats_title),
+ description = stringResource(R.string.app_intro_usage_stats_desc),
+ icon = Icons.Default.QueryStats,
+ backgroundColor = Color(0xFFB453A4),
+ contentColor = Color.White,
+ onNext = {
+ usageStatsPermissionGranted = context.hasUsagePermission()
+ if (!usageStatsPermissionGranted) {
+ val intent = Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS)
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ context.startActivity(intent)
+ false
+ } else {
+ true
}
- )
+ }
)
+ )
+
+ val methodSpecificPages = when (selectedMethod) {
+ AppUsageMethod.ACCESSIBILITY -> {
+ context.appLockRepository().setBackendImplementation(BackendImplementation.ACCESSIBILITY)
+ emptyList()
+ }
+
+ AppUsageMethod.USAGE_STATS -> {
+ context.appLockRepository().setBackendImplementation(BackendImplementation.USAGE_STATS)
+ emptyList()
+ }
AppUsageMethod.SHIZUKU -> listOf(
IntroPage(
@@ -454,10 +455,12 @@ fun AppIntroScreen(navController: NavController) {
notificationPermissionGranted =
NotificationManagerCompat.from(context).areNotificationsEnabled()
}
+ accessibilityServiceEnabled = context.isAccessibilityServiceEnabled()
+ usageStatsPermissionGranted = context.hasUsagePermission()
val methodPermissionGranted = when (selectedMethod) {
- AppUsageMethod.ACCESSIBILITY -> context.isAccessibilityServiceEnabled()
- AppUsageMethod.USAGE_STATS -> context.hasUsagePermission()
+ AppUsageMethod.ACCESSIBILITY -> accessibilityServiceEnabled
+ AppUsageMethod.USAGE_STATS -> usageStatsPermissionGranted
AppUsageMethod.SHIZUKU -> {
if (Shizuku.isPreV11()) {
checkSelfPermission(
@@ -470,12 +473,11 @@ fun AppIntroScreen(navController: NavController) {
}
}
- // Only require all permissions if accessibility is selected
- val allPermissionsGranted = if (selectedMethod == AppUsageMethod.ACCESSIBILITY) {
- overlayPermissionGranted && notificationPermissionGranted && methodPermissionGranted
- } else {
- overlayPermissionGranted && notificationPermissionGranted && methodPermissionGranted
- }
+ val allPermissionsGranted = overlayPermissionGranted &&
+ notificationPermissionGranted &&
+ accessibilityServiceEnabled &&
+ usageStatsPermissionGranted &&
+ methodPermissionGranted
if (!allPermissionsGranted) {
Toast.makeText(
@@ -483,13 +485,18 @@ fun AppIntroScreen(navController: NavController) {
context.getString(R.string.all_permissions_required),
Toast.LENGTH_SHORT
).show()
+ } else {
+ // Ensure correct service is started if not Accessibility
+ if (selectedMethod == AppUsageMethod.USAGE_STATS) {
+ context.startService(Intent(context, ExperimentalAppLockService::class.java))
+ }
}
allPermissionsGranted
}
)
val allPages =
- basicPages + methodSelectionPage + methodSpecificPages + finalPage
+ basicPages + methodSelectionPage + mandatoryPermissionPages + methodSpecificPages + finalPage
AppIntro(
pages = allPages,
diff --git a/app/src/main/java/dev/pranav/applock/features/lockscreen/ui/PasswordOverlayScreen.kt b/app/src/main/java/dev/pranav/applock/features/lockscreen/ui/PasswordOverlayScreen.kt
index de95e8a..2245103 100644
--- a/app/src/main/java/dev/pranav/applock/features/lockscreen/ui/PasswordOverlayScreen.kt
+++ b/app/src/main/java/dev/pranav/applock/features/lockscreen/ui/PasswordOverlayScreen.kt
@@ -1,7 +1,16 @@
package dev.pranav.applock.features.lockscreen.ui
+import android.annotation.SuppressLint
+import android.app.admin.DevicePolicyManager
+import android.content.BroadcastReceiver
+import android.content.ComponentName
import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
import android.content.res.Configuration
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Bundle
import android.util.Log
@@ -17,6 +26,7 @@ import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
+import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
@@ -31,12 +41,15 @@ import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@@ -45,6 +58,7 @@ import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import dev.pranav.applock.R
+import dev.pranav.applock.core.broadcast.DeviceAdmin
import dev.pranav.applock.core.ui.shapes
import dev.pranav.applock.core.utils.appLockRepository
import dev.pranav.applock.core.utils.vibrate
@@ -56,6 +70,7 @@ import dev.pranav.applock.ui.icons.Fingerprint
import dev.pranav.applock.ui.theme.AppLockTheme
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import java.util.concurrent.Executor
class PasswordOverlayActivity : FragmentActivity() {
@@ -68,10 +83,35 @@ class PasswordOverlayActivity : FragmentActivity() {
private var isBiometricPromptShowingLocal = false
private var movedToBackground = false
- private var appName: String = ""
+ private var appName by mutableStateOf("")
+ private var appIcon by mutableStateOf(null)
private val TAG = "PasswordOverlayActivity"
+ private val systemDialogsReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ if (intent.action == Intent.ACTION_CLOSE_SYSTEM_DIALOGS) {
+ val reason = intent.getStringExtra("reason")
+ // Handle button presses
+ when (reason) {
+ "recentapps" -> {
+ // Recents button pressed - trigger HOME action
+ Log.d(TAG, "Recents button pressed - triggering HOME action")
+ goHome()
+ return
+ }
+ "homekey" -> {
+ // Home button pressed - trigger HOME action
+ Log.d(TAG, "Home button pressed - triggering HOME action")
+ goHome()
+ return
+ }
+ }
+ }
+ }
+ }
+
+ @SuppressLint("UnspecifiedRegisterReceiverFlag")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -87,12 +127,41 @@ class PasswordOverlayActivity : FragmentActivity() {
appLockRepository = AppLockRepository(applicationContext)
+ // Override back button to do nothing
onBackPressedDispatcher.addCallback(this) {
- // Prevent back navigation to maintain security
+ Log.d(TAG, "Back button pressed - ignoring on lock screen")
+ // Do nothing - swallow the back press
}
setupWindow()
- loadAppNameAndSetupUI()
+ loadAppDetailsAndSetupUI()
+
+ val filter = IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ registerReceiver(systemDialogsReceiver, filter, Context.RECEIVER_EXPORTED)
+ } else {
+ registerReceiver(systemDialogsReceiver, filter)
+ }
+ }
+
+ private fun goHome() {
+ try {
+ val dpm = getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
+ val adminComponent = ComponentName(this, DeviceAdmin::class.java)
+
+ if (appLockRepository.isAntiUninstallEnabled() && dpm.isAdminActive(adminComponent)) {
+ // If device admin is active, we can also lock the device as requested for better security
+ // dpm.lockNow() // Uncomment if immediate lock is preferred over just going home
+ }
+
+ val intent = Intent(Intent.ACTION_MAIN).apply {
+ addCategory(Intent.CATEGORY_HOME)
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ }
+ startActivity(intent)
+ } catch (e: Exception) {
+ Log.e(TAG, "Error going home: ${e.message}")
+ }
}
override fun onPostCreate(savedInstanceState: Bundle?) {
@@ -118,7 +187,8 @@ class PasswordOverlayActivity : FragmentActivity() {
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON or
- WindowManager.LayoutParams.FLAG_SECURE
+ WindowManager.LayoutParams.FLAG_SECURE or
+ WindowManager.LayoutParams.FLAG_FULLSCREEN
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
@@ -130,6 +200,14 @@ class PasswordOverlayActivity : FragmentActivity() {
window.setHideOverlayWindows(true)
}
+ // Disable system gestures and navigation
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ val flags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or
+ WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR or
+ WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
+
+ window.addFlags(flags)
+ }
val layoutParams = window.attributes
layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
@@ -138,17 +216,34 @@ class PasswordOverlayActivity : FragmentActivity() {
layoutParams.screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_FULL
}
window.attributes = layoutParams
+
+ // Immersive mode to hide system UI
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ window.decorView.systemUiVisibility = (
+ android.view.View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
+ android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
+ android.view.View.SYSTEM_UI_FLAG_FULLSCREEN
+ )
+ }
}
- private fun loadAppNameAndSetupUI() {
+ private fun loadAppDetailsAndSetupUI() {
lifecycleScope.launch(Dispatchers.IO) {
try {
- appName = packageManager.getApplicationLabel(
- packageManager.getApplicationInfo(lockedPackageNameFromIntent!!, 0)
- ).toString()
+ val pkgName = lockedPackageNameFromIntent!!
+ val info = packageManager.getApplicationInfo(pkgName, 0)
+ val label = packageManager.getApplicationLabel(info).toString()
+ val icon = packageManager.getApplicationIcon(info)
+
+ withContext(Dispatchers.Main) {
+ appName = label
+ appIcon = icon
+ }
} catch (e: Exception) {
- Log.e(TAG, "Error loading app name: ${e.message}")
- appName = getString(R.string.default_app_name)
+ Log.e(TAG, "Error loading app details: ${e.message}")
+ withContext(Dispatchers.Main) {
+ appName = getString(R.string.default_app_name)
+ }
}
}
setupUI()
@@ -192,6 +287,7 @@ class PasswordOverlayActivity : FragmentActivity() {
modifier = Modifier.padding(innerPadding),
fromMainActivity = false,
lockedAppName = appName,
+ lockedAppIcon = appIcon,
triggeringPackageName = triggeringPackageNameFromIntent,
onPatternAttempt = onPatternAttemptCallback
)
@@ -205,6 +301,7 @@ class PasswordOverlayActivity : FragmentActivity() {
onBiometricAuth = { triggerBiometricPrompt() },
onAuthSuccess = {},
lockedAppName = appName,
+ lockedAppIcon = appIcon,
triggeringPackageName = triggeringPackageNameFromIntent,
onPinAttempt = onPinAttemptCallback
)
@@ -247,8 +344,6 @@ class PasswordOverlayActivity : FragmentActivity() {
isBiometricPromptShowingLocal = false
lockedPackageNameFromIntent?.let { pkgName ->
AppLockManager.temporarilyUnlockAppWithBiometrics(pkgName)
- // Fix: Do NOT relaunch the app. Just finish the overlay to reveal the underlying activity.
- // This preserves the navigation stack/state of the locked app.
}
finishAfterTransition()
}
@@ -257,7 +352,7 @@ class PasswordOverlayActivity : FragmentActivity() {
override fun onResume() {
super.onResume()
movedToBackground = false
- AppLockManager.isLockScreenShown.set(true) // Set to true when activity is visible
+ AppLockManager.isLockScreenShown.set(true)
lifecycleScope.launch {
applyUserPreferences()
}
@@ -288,6 +383,13 @@ class PasswordOverlayActivity : FragmentActivity() {
}
}
+ override fun onUserLeaveHint() {
+ super.onUserLeaveHint()
+ Log.d(TAG, "User leave hint - going home from lock screen")
+ // Allow going home when user presses home/recents
+ goHome()
+ }
+
override fun onPause() {
super.onPause()
if (!isChangingConfigurations() && !isBiometricPromptShowingLocal && !movedToBackground) {
@@ -314,6 +416,11 @@ class PasswordOverlayActivity : FragmentActivity() {
override fun onDestroy() {
super.onDestroy()
+ try {
+ unregisterReceiver(systemDialogsReceiver)
+ } catch (e: Exception) {
+ // Already unregistered
+ }
AppLockManager.isLockScreenShown.set(false)
AppLockManager.reportBiometricAuthFinished()
Log.d(TAG, "PasswordOverlayActivity onDestroy for $lockedPackageNameFromIntent")
@@ -330,6 +437,7 @@ fun PasswordOverlayScreen(
onBiometricAuth: () -> Unit = {},
onAuthSuccess: () -> Unit,
lockedAppName: String? = null,
+ lockedAppIcon: Drawable? = null,
triggeringPackageName: String? = null,
onPinAttempt: ((pin: String) -> Boolean)? = null
) {
@@ -366,25 +474,13 @@ fun PasswordOverlayScreen(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
- Text(
- text = if (!fromMainActivity && !lockedAppName.isNullOrEmpty())
- "Continue to $lockedAppName"
- else
- stringResource(R.string.enter_password_to_continue),
- style = MaterialTheme.typography.titleLarge,
- textAlign = TextAlign.Center
+ AppHeader(
+ fromMainActivity = fromMainActivity,
+ lockedAppName = lockedAppName,
+ lockedAppIcon = lockedAppIcon,
+ style = MaterialTheme.typography.titleLarge
)
-// if (!fromMainActivity && !triggeringPackageName.isNullOrEmpty()) {
-// Spacer(modifier = Modifier.height(8.dp))
-// Text(
-// text = triggeringPackageName,
-// style = MaterialTheme.typography.labelSmall,
-// color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f),
-// textAlign = TextAlign.Center
-// )
-// }
-
Spacer(modifier = Modifier.height(16.dp))
PasswordIndicators(
@@ -436,27 +532,16 @@ fun PasswordOverlayScreen(
val topSpacerHeight = if (screenHeightDp < 600.dp) 12.dp else 48.dp
Spacer(modifier = Modifier.height(topSpacerHeight))
- Text(
- text = if (!fromMainActivity && !lockedAppName.isNullOrEmpty())
- "Continue to $lockedAppName"
- else
- stringResource(R.string.enter_password_to_continue),
+ AppHeader(
+ fromMainActivity = fromMainActivity,
+ lockedAppName = lockedAppName,
+ lockedAppIcon = lockedAppIcon,
style = if (!fromMainActivity && !lockedAppName.isNullOrEmpty())
MaterialTheme.typography.titleLargeEmphasized
else
- MaterialTheme.typography.headlineMediumEmphasized,
- textAlign = TextAlign.Center
+ MaterialTheme.typography.headlineMediumEmphasized
)
-// if (!fromMainActivity && !triggeringPackageName.isNullOrEmpty()) {
-// Text(
-// text = triggeringPackageName,
-// style = MaterialTheme.typography.labelSmall,
-// color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f),
-// textAlign = TextAlign.Center
-// )
-// }
-
Spacer(modifier = Modifier.height(16.dp))
PasswordIndicators(
@@ -500,6 +585,70 @@ fun PasswordOverlayScreen(
}
}
+@Composable
+fun AppHeader(
+ fromMainActivity: Boolean,
+ lockedAppName: String?,
+ lockedAppIcon: Drawable?,
+ style: androidx.compose.ui.text.TextStyle
+) {
+ if (!fromMainActivity && !lockedAppName.isNullOrEmpty()) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Text(
+ text = "Continue to",
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ letterSpacing = 1.sp
+ )
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ modifier = Modifier.padding(horizontal = 16.dp)
+ ) {
+ if (lockedAppIcon != null) {
+ val bitmap = remember(lockedAppIcon) {
+ val b = Bitmap.createBitmap(
+ lockedAppIcon.intrinsicWidth.coerceAtLeast(1),
+ lockedAppIcon.intrinsicHeight.coerceAtLeast(1),
+ Bitmap.Config.ARGB_8888
+ )
+ val canvas = Canvas(b)
+ lockedAppIcon.setBounds(0, 0, canvas.width, canvas.height)
+ lockedAppIcon.draw(canvas)
+ b.asImageBitmap()
+ }
+ Image(
+ bitmap = bitmap,
+ contentDescription = null,
+ modifier = Modifier
+ .size(40.dp)
+ .clip(RoundedCornerShape(8.dp))
+ )
+ }
+
+ Text(
+ text = lockedAppName,
+ style = style.copy(
+ fontWeight = FontWeight.Bold,
+ letterSpacing = 0.5.sp
+ ),
+ textAlign = TextAlign.Center
+ )
+ }
+ }
+ } else {
+ Text(
+ text = stringResource(R.string.enter_password_to_continue),
+ style = style,
+ textAlign = TextAlign.Center
+ )
+ }
+}
+
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalAnimationApi::class)
@Composable
fun PasswordIndicators(
@@ -679,9 +828,6 @@ fun KeypadSection(
}
}
- // Calculate available height for keypad (heuristic)
- // 4 rows of buttons + 3 spacings + biometric button (optional)
- // Estimate top content takes ~200dp
val estimatedTopContentHeight = 220.dp
val availableHeight = screenHeightDp - estimatedTopContentHeight
@@ -709,10 +855,7 @@ fun KeypadSection(
val totalHorizontalSpacing = buttonSpacing * 2
val widthBasedSize = (availableWidth - totalHorizontalSpacing) / 3.5f
- // Height constraint for portrait
val totalVerticalSpacing = buttonSpacing * 3
- // If biometric button is shown, it takes extra space, but it's floating or above?
- // In the current layout, it's inside the column at the top.
val biometricAllowance = if (showBiometricButton) 60.dp else 0.dp
val heightBasedSize =
(availableHeight - totalVerticalSpacing - biometricAllowance) / 4f
@@ -769,7 +912,6 @@ fun KeypadSection(
Modifier
.padding(horizontal = horizontalPadding)
.navigationBarsPadding()
- // Add a small bottom padding to ensure it doesn't touch the edge
.padding(bottom = 8.dp)
}
) {
diff --git a/app/src/main/java/dev/pranav/applock/features/lockscreen/ui/PatternLockScreen.kt b/app/src/main/java/dev/pranav/applock/features/lockscreen/ui/PatternLockScreen.kt
index 69b9f7d..d187a82 100644
--- a/app/src/main/java/dev/pranav/applock/features/lockscreen/ui/PatternLockScreen.kt
+++ b/app/src/main/java/dev/pranav/applock/features/lockscreen/ui/PatternLockScreen.kt
@@ -1,5 +1,6 @@
package dev.pranav.applock.features.lockscreen.ui
+import android.graphics.drawable.Drawable
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateFloatAsState
@@ -50,6 +51,7 @@ fun PatternLockScreen(
modifier: Modifier = Modifier,
fromMainActivity: Boolean = false,
lockedAppName: String? = null,
+ lockedAppIcon: Drawable? = null,
triggeringPackageName: String? = null,
onPatternAttempt: ((pattern: String) -> Boolean)? = null,
onBiometricAuth: (() -> Unit)? = null
@@ -119,43 +121,27 @@ fun PatternLockScreen(
.weight(1f)
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.SpaceBetween
+ verticalArrangement = Arrangement.Center
) {
- Column(
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
+ AppHeader(
+ fromMainActivity = fromMainActivity,
+ lockedAppName = lockedAppName,
+ lockedAppIcon = lockedAppIcon,
+ style = MaterialTheme.typography.titleMedium
+ )
+
+ if (showError) {
+ Spacer(modifier = Modifier.height(8.dp))
Text(
- text = if (!fromMainActivity && !lockedAppName.isNullOrEmpty())
- "Continue to $lockedAppName"
- else
- stringResource(R.string.enter_pattern_to_continue),
- style = MaterialTheme.typography.titleMedium,
- color = MaterialTheme.colorScheme.onSurface,
+ text = stringResource(R.string.incorrect_pattern_try_again),
+ color = MaterialTheme.colorScheme.error,
+ style = MaterialTheme.typography.bodySmall,
textAlign = TextAlign.Center
)
-//
-// if (!fromMainActivity && !triggeringPackageName.isNullOrEmpty()) {
-// Spacer(modifier = Modifier.height(8.dp))
-// Text(
-// text = triggeringPackageName,
-// style = MaterialTheme.typography.bodySmall,
-// color = MaterialTheme.colorScheme.onSurfaceVariant,
-// textAlign = TextAlign.Center
-// )
-// }
-
- if (showError) {
- Spacer(modifier = Modifier.height(8.dp))
- Text(
- text = stringResource(R.string.incorrect_pattern_try_again),
- color = MaterialTheme.colorScheme.error,
- style = MaterialTheme.typography.bodySmall,
- textAlign = TextAlign.Center
- )
- }
}
if (appLockRepository.isBiometricAuthEnabled() && onBiometricAuth != null) {
+ Spacer(modifier = Modifier.height(16.dp))
FilledTonalIconButton(
onClick = { onBiometricAuth() },
modifier = Modifier.size(44.dp),
@@ -206,26 +192,13 @@ fun PatternLockScreen(
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
- Text(
- text = if (!fromMainActivity && !lockedAppName.isNullOrEmpty())
- "Continue to $lockedAppName"
- else
- stringResource(R.string.enter_pattern_to_continue),
- style = MaterialTheme.typography.headlineMedium,
- color = MaterialTheme.colorScheme.onSurface,
- textAlign = TextAlign.Center
+ AppHeader(
+ fromMainActivity = fromMainActivity,
+ lockedAppName = lockedAppName,
+ lockedAppIcon = lockedAppIcon,
+ style = MaterialTheme.typography.headlineSmall
)
-// if (!fromMainActivity && !triggeringPackageName.isNullOrEmpty()) {
-// Spacer(modifier = Modifier.height(8.dp))
-// Text(
-// text = triggeringPackageName,
-// style = MaterialTheme.typography.labelLarge,
-// color = MaterialTheme.colorScheme.onSurfaceVariant,
-// textAlign = TextAlign.Center
-// )
-// }
-
if (showError) {
Spacer(modifier = Modifier.height(8.dp))
Text(
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..3805e25 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
@@ -15,6 +15,7 @@ import androidx.compose.animation.core.spring
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
@@ -31,10 +32,14 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
import androidx.navigation.NavController
import dev.pranav.applock.R
import dev.pranav.applock.core.broadcast.DeviceAdmin
@@ -60,8 +65,17 @@ fun SettingsScreen(
navController: NavController
) {
val context = LocalContext.current
+ val lifecycleOwner = LocalLifecycleOwner.current
val appLockRepository = remember { AppLockRepository(context) }
+ // Pre-fetch strings to avoid LocalContext.current resource querying in lambdas
+ val shizukuPermissionGrantedMsg = stringResource(R.string.settings_screen_shizuku_permission_granted)
+ val shizukuPermissionRequiredMsg = stringResource(R.string.settings_screen_shizuku_permission_required_desc)
+ val deviceAdminExplanation = stringResource(R.string.main_screen_device_admin_explanation)
+ val exportLogsError = stringResource(R.string.settings_screen_export_logs_error)
+ val shizukuNotRunningMsg = stringResource(R.string.settings_screen_shizuku_not_running_toast)
+ val usagePermissionMsg = stringResource(R.string.settings_screen_usage_permission_toast)
+
var showDialog by remember { mutableStateOf(false) }
var showUnlockTimeDialog by remember { mutableStateOf(false) }
@@ -69,17 +83,9 @@ fun SettingsScreen(
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
- Toast.makeText(
- context,
- context.getString(R.string.settings_screen_shizuku_permission_granted),
- Toast.LENGTH_SHORT
- ).show()
+ Toast.makeText(context, shizukuPermissionGrantedMsg, Toast.LENGTH_SHORT).show()
} else {
- Toast.makeText(
- context,
- context.getString(R.string.settings_screen_shizuku_permission_required_desc),
- Toast.LENGTH_SHORT
- ).show()
+ Toast.makeText(context, shizukuPermissionRequiredMsg, Toast.LENGTH_SHORT).show()
}
}
@@ -88,6 +94,12 @@ fun SettingsScreen(
var useBiometricAuth by remember { mutableStateOf(appLockRepository.isBiometricAuthEnabled()) }
var unlockTimeDuration by remember { mutableIntStateOf(appLockRepository.getUnlockTimeDuration()) }
var antiUninstallEnabled by remember { mutableStateOf(appLockRepository.isAntiUninstallEnabled()) }
+
+ var antiUninstallAdminSettings by remember { mutableStateOf(appLockRepository.isAntiUninstallAdminSettingsEnabled()) }
+ var antiUninstallUsageStats by remember { mutableStateOf(appLockRepository.isAntiUninstallUsageStatsEnabled()) }
+ var antiUninstallAccessibility by remember { mutableStateOf(appLockRepository.isAntiUninstallAccessibilityEnabled()) }
+ var antiUninstallOverlay by remember { mutableStateOf(appLockRepository.isAntiUninstallOverlayEnabled()) }
+
var disableHapticFeedback by remember { mutableStateOf(appLockRepository.shouldDisableHaptics()) }
var loggingEnabled by remember { mutableStateOf(appLockRepository.isLoggingEnabled()) }
@@ -95,6 +107,29 @@ fun SettingsScreen(
var showDeviceAdminDialog by remember { mutableStateOf(false) }
var showAccessibilityDialog by remember { mutableStateOf(false) }
+ // Sync state with repository on resume (e.g. after returning from AdminDisableActivity)
+ DisposableEffect(lifecycleOwner) {
+ val observer = LifecycleEventObserver { _, event ->
+ if (event == Lifecycle.Event.ON_RESUME) {
+ autoUnlock = appLockRepository.isAutoUnlockEnabled()
+ useMaxBrightness = appLockRepository.shouldUseMaxBrightness()
+ useBiometricAuth = appLockRepository.isBiometricAuthEnabled()
+ unlockTimeDuration = appLockRepository.getUnlockTimeDuration()
+ antiUninstallEnabled = appLockRepository.isAntiUninstallEnabled()
+ antiUninstallAdminSettings = appLockRepository.isAntiUninstallAdminSettingsEnabled()
+ antiUninstallUsageStats = appLockRepository.isAntiUninstallUsageStatsEnabled()
+ antiUninstallAccessibility = appLockRepository.isAntiUninstallAccessibilityEnabled()
+ antiUninstallOverlay = appLockRepository.isAntiUninstallOverlayEnabled()
+ disableHapticFeedback = appLockRepository.shouldDisableHaptics()
+ loggingEnabled = appLockRepository.isLoggingEnabled()
+ }
+ }
+ lifecycleOwner.lifecycle.addObserver(observer)
+ onDispose {
+ lifecycleOwner.lifecycle.removeObserver(observer)
+ }
+ }
+
val biometricManager = remember { BiometricManager.from(context) }
val isBiometricAvailable = remember {
biometricManager.canAuthenticate(
@@ -103,6 +138,20 @@ fun SettingsScreen(
) == BiometricManager.BIOMETRIC_SUCCESS
}
+ // Effect to handle anti-uninstall settings reset when disabled
+ LaunchedEffect(antiUninstallEnabled) {
+ if (!antiUninstallEnabled) {
+ antiUninstallAdminSettings = false
+ antiUninstallUsageStats = false
+ antiUninstallAccessibility = false
+ antiUninstallOverlay = false
+ appLockRepository.setAntiUninstallAdminSettingsEnabled(false)
+ appLockRepository.setAntiUninstallUsageStatsEnabled(false)
+ appLockRepository.setAntiUninstallAccessibilityEnabled(false)
+ appLockRepository.setAntiUninstallOverlayEnabled(false)
+ }
+ }
+
if (showDialog) {
AlertDialog(
onDismissRequest = { showDialog = false },
@@ -164,7 +213,7 @@ fun SettingsScreen(
putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, component)
putExtra(
DevicePolicyManager.EXTRA_ADD_EXPLANATION,
- context.getString(R.string.main_screen_device_admin_explanation)
+ deviceAdminExplanation
)
}
context.startActivity(intent)
@@ -343,15 +392,12 @@ fun SettingsScreen(
val hasAccessibility = context.isAccessibilityServiceEnabled()
when {
- !hasDeviceAdmin && !hasAccessibility -> {
- showPermissionDialog = true
+ !hasAccessibility -> {
+ showAccessibilityDialog = true
}
!hasDeviceAdmin -> {
showDeviceAdminDialog = true
}
- !hasAccessibility -> {
- showAccessibilityDialog = true
- }
else -> {
antiUninstallEnabled = true
appLockRepository.setAntiUninstallEnabled(true)
@@ -368,6 +414,62 @@ fun SettingsScreen(
)
}
+ if (antiUninstallEnabled) {
+ item {
+ SectionTitle(text = "Anti-Uninstall Settings")
+ }
+ item {
+ SettingsGroup(
+ items = listOf(
+ ToggleSettingItem(
+ icon = Icons.Default.AdminPanelSettings,
+ title = "Protect Device Admin",
+ subtitle = "Prevent access to device admin settings",
+ checked = antiUninstallAdminSettings,
+ enabled = antiUninstallEnabled,
+ onCheckedChange = { isChecked ->
+ antiUninstallAdminSettings = isChecked
+ appLockRepository.setAntiUninstallAdminSettingsEnabled(isChecked)
+ }
+ ),
+ ToggleSettingItem(
+ icon = Icons.Default.QueryStats,
+ title = "Protect Usage Stats",
+ subtitle = "Prevent access to usage stats settings",
+ checked = antiUninstallUsageStats,
+ enabled = antiUninstallEnabled,
+ onCheckedChange = { isChecked ->
+ antiUninstallUsageStats = isChecked
+ appLockRepository.setAntiUninstallUsageStatsEnabled(isChecked)
+ }
+ ),
+ ToggleSettingItem(
+ icon = Accessibility,
+ title = "Protect Accessibility",
+ subtitle = "Prevent access to accessibility settings",
+ checked = antiUninstallAccessibility,
+ enabled = antiUninstallEnabled,
+ onCheckedChange = { isChecked ->
+ antiUninstallAccessibility = isChecked
+ appLockRepository.setAntiUninstallAccessibilityEnabled(isChecked)
+ }
+ ),
+ ToggleSettingItem(
+ icon = Display,
+ title = "Protect Overlay Settings",
+ subtitle = "Prevent access to appear on top settings",
+ checked = antiUninstallOverlay,
+ enabled = antiUninstallEnabled,
+ onCheckedChange = { isChecked ->
+ antiUninstallOverlay = isChecked
+ appLockRepository.setAntiUninstallOverlayEnabled(isChecked)
+ }
+ )
+ )
+ )
+ }
+ }
+
item {
SectionTitle(text = stringResource(R.string.settings_screen_advanced_title))
}
@@ -393,7 +495,7 @@ fun SettingsScreen(
} else {
Toast.makeText(
context,
- context.getString(R.string.settings_screen_export_logs_error),
+ exportLogsError,
Toast.LENGTH_SHORT
).show()
}
@@ -417,7 +519,7 @@ fun SettingsScreen(
} else {
Toast.makeText(
context,
- context.getString(R.string.settings_screen_export_logs_error),
+ exportLogsError,
Toast.LENGTH_SHORT
).show()
}
@@ -443,7 +545,9 @@ fun SettingsScreen(
BackendSelectionCard(
appLockRepository = appLockRepository,
context = context,
- shizukuPermissionLauncher = shizukuPermissionLauncher
+ shizukuPermissionLauncher = shizukuPermissionLauncher,
+ shizukuNotRunningMsg = shizukuNotRunningMsg,
+ usagePermissionMsg = usagePermissionMsg
)
}
@@ -590,7 +694,12 @@ fun ToggleSettingRow(
) {
ListItem(
modifier = Modifier
- .clickable(enabled = enabled) { if (enabled) onCheckedChange(!checked) }
+ .toggleable(
+ value = checked,
+ enabled = enabled,
+ role = Role.Switch,
+ onValueChange = { onCheckedChange(it) }
+ )
.padding(vertical = 2.dp, horizontal = 4.dp),
headlineContent = {
Text(
@@ -754,7 +863,9 @@ fun UnlockTimeDurationDialog(
fun BackendSelectionCard(
appLockRepository: AppLockRepository,
context: Context,
- shizukuPermissionLauncher: androidx.activity.result.ActivityResultLauncher
+ shizukuPermissionLauncher: androidx.activity.result.ActivityResultLauncher,
+ shizukuNotRunningMsg: String,
+ usagePermissionMsg: String
) {
var selectedBackend by remember { mutableStateOf(appLockRepository.getBackendImplementation()) }
@@ -781,7 +892,7 @@ fun BackendSelectionCard(
} else {
Toast.makeText(
context,
- context.getString(R.string.settings_screen_shizuku_not_running_toast),
+ shizukuNotRunningMsg,
Toast.LENGTH_LONG
).show()
}
@@ -803,7 +914,7 @@ fun BackendSelectionCard(
context.startActivity(intent)
Toast.makeText(
context,
- context.getString(R.string.settings_screen_usage_permission_toast),
+ usagePermissionMsg,
Toast.LENGTH_LONG
).show()
return@BackendSelectionItem
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..b895f48 100644
--- a/app/src/main/java/dev/pranav/applock/services/AppLockAccessibilityService.kt
+++ b/app/src/main/java/dev/pranav/applock/services/AppLockAccessibilityService.kt
@@ -3,17 +3,24 @@ package dev.pranav.applock.services
import android.accessibilityservice.AccessibilityService
import android.accessibilityservice.AccessibilityServiceInfo
import android.annotation.SuppressLint
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
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.content.pm.ServiceInfo
+import android.os.Build
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.app.NotificationCompat
import androidx.core.content.getSystemService
+import dev.pranav.applock.R
import dev.pranav.applock.core.broadcast.DeviceAdmin
import dev.pranav.applock.core.utils.LogUtils
import dev.pranav.applock.core.utils.appLockRepository
@@ -22,6 +29,9 @@ 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.DEVICE_ADMIN_SETTINGS_CLASSES
+import dev.pranav.applock.services.AppLockConstants.USAGE_ACCESS_SETTINGS_CLASSES
+import dev.pranav.applock.services.AppLockConstants.OVERLAY_SETTINGS_CLASSES
import dev.pranav.applock.services.AppLockConstants.EXCLUDED_APPS
import rikka.shizuku.Shizuku
@@ -33,13 +43,16 @@ class AppLockAccessibilityService : AccessibilityService() {
private var recentsOpen = false
private var lastForegroundPackage = ""
+ private val NOTIFICATION_ID = 114
+ private val CHANNEL_ID = "AppLockAccessibilityServiceChannel"
+ private val notificationManager: NotificationManager by lazy { getSystemService(NotificationManager::class.java)!! }
+
enum class BiometricState {
IDLE, AUTH_STARTED
}
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
@@ -53,7 +66,6 @@ class AppLockAccessibilityService : AccessibilityService() {
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) {
@@ -69,6 +81,7 @@ class AppLockAccessibilityService : AccessibilityService() {
AppLockManager.currentBiometricState = BiometricState.IDLE
AppLockManager.isLockScreenShown.set(false)
startPrimaryBackendService()
+ startForegroundService()
val filter = android.content.IntentFilter().apply {
addAction(Intent.ACTION_SCREEN_OFF)
@@ -80,7 +93,10 @@ class AppLockAccessibilityService : AccessibilityService() {
}
}
- override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int = START_STICKY
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ startForegroundService()
+ return START_STICKY
+ }
override fun onServiceConnected() {
super.onServiceConnected()
@@ -96,33 +112,53 @@ class AppLockAccessibilityService : AccessibilityService() {
Log.d(TAG, "Accessibility service connected")
AppLockManager.resetRestartAttempts(TAG)
appLockRepository.setActiveBackend(BackendImplementation.ACCESSIBILITY)
+ startForegroundService()
} catch (e: Exception) {
logError("Error in onServiceConnected", e)
}
}
override fun onAccessibilityEvent(event: AccessibilityEvent) {
- Log.d(TAG, event.toString())
try {
+ // Block Recents button if lock screen is active
+ if (AppLockManager.isLockScreenShown.get()) {
+ if (isRecentsEvent(event)) {
+ LogUtils.d(TAG, "Blocking Recents access while lock screen is active. Triggering BACK action to stay on overlay.")
+ // Performing BACK when recents panel is opening will dismiss it, making the button feel "dead".
+ performGlobalAction(GLOBAL_ACTION_BACK)
+ return
+ }
+ }
+
handleAccessibilityEvent(event)
} catch (e: Exception) {
logError("Unhandled error in onAccessibilityEvent", e)
}
}
+ private fun isRecentsEvent(event: AccessibilityEvent): Boolean {
+ val packageName = event.packageName?.toString() ?: ""
+ val className = event.className?.toString() ?: ""
+ val text = event.text.toString().lowercase()
+
+ // Don't block if it's our own app's event
+ if (packageName == applicationContext.packageName) return false
+
+ return className in AppLockConstants.KNOWN_RECENTS_CLASSES ||
+ (packageName == "com.android.systemui" && className.contains("recents", ignoreCase = true)) ||
+ text.contains("recent apps") ||
+ text.contains("overview")
+ }
+
private fun handleAccessibilityEvent(event: AccessibilityEvent) {
- if (appLockRepository.isAntiUninstallEnabled() &&
- event.packageName == DEVICE_ADMIN_SETTINGS_PACKAGE
- ) {
- checkForDeviceAdminDeactivation(event)
+ if (appLockRepository.isAntiUninstallEnabled()) {
+ handleAntiUninstallBlocking(event)
}
- // Early return if protection is disabled or service is not running
if (!appLockRepository.isProtectEnabled() || !isServiceRunning) {
return
}
- // Handle window state changes
if (event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
try {
handleWindowStateChanged(event)
@@ -132,16 +168,12 @@ class AppLockAccessibilityService : AccessibilityService() {
}
}
- // Skip processing if recents are open
if (recentsOpen) {
- LogUtils.d(TAG, "Recents opened, ignoring accessibility event")
return
}
- // Extract and validate package name
val packageName = event.packageName?.toString() ?: return
- // Skip if device is locked or app is excluded
if (!isValidPackageForLocking(packageName)) {
return
}
@@ -153,30 +185,142 @@ class AppLockAccessibilityService : AccessibilityService() {
}
}
+ private fun handleAntiUninstallBlocking(event: AccessibilityEvent) {
+ val packageName = event.packageName?.toString() ?: return
+
+ // Broadly check for settings or package installer
+ val isSettings = packageName.contains("settings") ||
+ packageName.contains("packageinstaller") ||
+ packageName.contains("permissioncontroller") ||
+ packageName == "android"
+
+ if (!isSettings) return
+
+ val className = event.className?.toString() ?: ""
+ val root = rootInActiveWindow
+
+ // 1. Protect AppLock's own App Info and Uninstallation
+ if (root != null && (containsTextRecursive(root, "dev.pranav.applock") || containsTextRecursive(root, "App Lock"))) {
+ if (className.contains("AppDetails") || className.contains("InstalledAppDetails") ||
+ className.contains("Uninstaller") || className.contains("PackageInstaller") ||
+ className.contains("Settings\$AppDetailsActivity")) {
+ blockAccess("App Lock protection is active.")
+ return
+ }
+ }
+
+ // Sub switch 1: Device Admin Settings
+ if (appLockRepository.isAntiUninstallAdminSettingsEnabled()) {
+ if (className in DEVICE_ADMIN_SETTINGS_CLASSES ||
+ className.contains("DeviceAdminSettings") ||
+ className.contains("DeviceAdminAdd")) {
+ blockAccess("Device Admin settings are protected.")
+ return
+ }
+ if (root != null && (containsTextRecursive(root, "Device admin") || containsTextRecursive(root, "Device administrator"))) {
+ if (className.contains("SubSettings") || className.contains("SettingsActivity")) {
+ blockAccess("Device Admin settings are protected.")
+ return
+ }
+ }
+ }
+
+ // Sub switch 2: Usage Stats Screen
+ if (appLockRepository.isAntiUninstallUsageStatsEnabled()) {
+ if (className in USAGE_ACCESS_SETTINGS_CLASSES ||
+ className.contains("UsageAccessSettings") ||
+ className.contains("UsageStats")) {
+ blockAccess("Usage access settings are protected.")
+ return
+ }
+ if (root != null && (containsTextRecursive(root, "Usage access") || containsTextRecursive(root, "Usage stats"))) {
+ if (className.contains("SubSettings") || className.contains("SettingsActivity")) {
+ blockAccess("Usage access settings are protected.")
+ return
+ }
+ }
+ }
+
+ // Sub switch 3: Accessibility Settings
+ if (appLockRepository.isAntiUninstallAccessibilityEnabled()) {
+ if (className in ACCESSIBILITY_SETTINGS_CLASSES ||
+ className.contains("AccessibilitySettings") ||
+ className.contains("AccessibilityServiceWarning")) {
+ blockAccess("Accessibility settings are protected.")
+ return
+ }
+
+ val accessibilityKeywords = listOf("Accessibility", "Installed apps", "Downloaded apps", "Installed services", "Downloaded services")
+ if (root != null && accessibilityKeywords.any { containsTextRecursive(root, it) }) {
+ // If we see "App Lock" in an accessibility-related screen, block it.
+ if (containsTextRecursive(root, "App Lock")) {
+ blockAccess("Accessibility settings for App Lock are protected.")
+ return
+ }
+ }
+ }
+
+ // Sub switch 4: Overlay (Appear on top) Settings
+ if (appLockRepository.isAntiUninstallOverlayEnabled()) {
+ if (className in OVERLAY_SETTINGS_CLASSES ||
+ className.contains("DrawOverlayDetails") ||
+ className.contains("OverlaySettings")) {
+ blockAccess("Overlay settings are protected.")
+ return
+ }
+ if (root != null && (containsTextRecursive(root, "Display over other apps") || containsTextRecursive(root, "Appear on top"))) {
+ if (className.contains("SubSettings") || className.contains("SettingsActivity") || className.contains("DrawOverlayDetails")) {
+ blockAccess("Overlay settings are protected.")
+ return
+ }
+ }
+ }
+ }
+
+ private fun containsTextRecursive(node: AccessibilityNodeInfo?, text: String): Boolean {
+ if (node == null) return false
+
+ val nodeText = node.text?.toString() ?: ""
+ val contentDescription = node.contentDescription?.toString() ?: ""
+
+ if (nodeText.contains(text, ignoreCase = true) || contentDescription.contains(text, ignoreCase = true)) {
+ return true
+ }
+
+ for (i in 0 until node.childCount) {
+ val child = node.getChild(i)
+ if (containsTextRecursive(child, text)) {
+ return true
+ }
+ }
+ return false
+ }
+
+ private fun blockAccess(message: String) {
+ performGlobalAction(GLOBAL_ACTION_HOME)
+ Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
+ }
+
private fun handleWindowStateChanged(event: AccessibilityEvent) {
val isRecentlyOpened = isRecentlyOpened(event)
val isHomeScreen = isHomeScreen(event)
when {
isRecentlyOpened -> {
- LogUtils.d(TAG, "Entering recents")
recentsOpen = true
}
isHomeScreenTransition(event) && recentsOpen -> {
- LogUtils.d(TAG, "Transitioning to home screen from recents")
recentsOpen = false
clearTemporarilyUnlockedAppIfNeeded()
}
isHomeScreen -> {
- LogUtils.d(TAG, "On home screen")
recentsOpen = false
clearTemporarilyUnlockedAppIfNeeded()
}
isAppSwitchedFromRecents(event) -> {
- LogUtils.d(TAG, "App switched from recents")
recentsOpen = false
clearTemporarilyUnlockedAppIfNeeded(event.packageName?.toString())
}
@@ -212,25 +356,21 @@ class AppLockAccessibilityService : AccessibilityService() {
newPackage !in appLockRepository.getTriggerExcludedApps())
if (shouldClear) {
- LogUtils.d(TAG, "Clearing temporarily unlocked app")
AppLockManager.clearTemporarilyUnlockedApp()
}
}
private fun isValidPackageForLocking(packageName: String): Boolean {
- // Check if device is locked
if (applicationContext.isDeviceLocked()) {
AppLockManager.appUnlockTimes.clear()
AppLockManager.clearTemporarilyUnlockedApp()
return false
}
- // Check if accessibility should handle locking
if (!shouldAccessibilityHandleLocking()) {
return false
}
- // Skip excluded packages
if (packageName == APP_PACKAGE_PREFIX ||
packageName in keyboardPackages ||
packageName in EXCLUDED_APPS
@@ -238,7 +378,6 @@ class AppLockAccessibilityService : AccessibilityService() {
return false
}
- // Skip known recents classes
return true
}
@@ -247,21 +386,15 @@ class AppLockAccessibilityService : AccessibilityService() {
val triggeringPackage = lastForegroundPackage
lastForegroundPackage = currentForegroundPackage
- // Skip if triggering package is excluded
if (triggeringPackage in appLockRepository.getTriggerExcludedApps()) {
return
}
- // 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()
}
@@ -283,19 +416,16 @@ class AppLockAccessibilityService : AccessibilityService() {
}
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
}
- // Return if package is not locked
if (packageName !in appLockRepository.getLockedApps()) {
return
}
- // Return if app is temporarily unlocked
if (AppLockManager.isAppTemporarilyUnlocked(packageName)) {
return
}
@@ -305,30 +435,18 @@ class AppLockAccessibilityService : AccessibilityService() {
val unlockDurationMinutes = appLockRepository.getUnlockTimeDuration()
val unlockTimestamp = AppLockManager.appUnlockTimes[packageName] ?: 0L
- LogUtils.d(
- TAG,
- "checkAndLockApp: pkg=$packageName, duration=$unlockDurationMinutes min, unlockTime=$unlockTimestamp, currentTime=$currentTime, isLockScreenShown=${AppLockManager.isLockScreenShown.get()}"
- )
-
if (unlockDurationMinutes > 0 && unlockTimestamp > 0) {
if (unlockDurationMinutes >= 10_000) {
return
}
val durationMillis = unlockDurationMinutes.toLong() * 60L * 1000L
-
val elapsedMillis = currentTime - unlockTimestamp
- LogUtils.d(
- TAG,
- "Grace period check: elapsed=${elapsedMillis}ms (${elapsedMillis / 1000}s), duration=${durationMillis}ms (${durationMillis / 1000}s)"
- )
-
if (elapsedMillis < durationMillis) {
return
}
- LogUtils.d(TAG, "Unlock grace period expired for $packageName. Clearing timestamp.")
AppLockManager.appUnlockTimes.remove(packageName)
AppLockManager.clearTemporarilyUnlockedApp()
}
@@ -336,7 +454,6 @@ class AppLockAccessibilityService : AccessibilityService() {
if (AppLockManager.isLockScreenShown.get() ||
AppLockManager.currentBiometricState == BiometricState.AUTH_STARTED
) {
- LogUtils.d(TAG, "Lock screen already shown or biometric auth in progress, skipping")
return
}
@@ -344,7 +461,6 @@ class AppLockAccessibilityService : AccessibilityService() {
}
private fun showLockScreenOverlay(packageName: String, triggeringPackage: String) {
- LogUtils.d(TAG, "Locked app detected: $packageName. Showing overlay.")
AppLockManager.isLockScreenShown.set(true)
val intent = Intent(this, PasswordOverlayActivity::class.java).apply {
@@ -365,109 +481,6 @@ class AppLockAccessibilityService : AccessibilityService() {
}
}
- private fun checkForDeviceAdminDeactivation(event: AccessibilityEvent) {
- Log.d(TAG, "Checking for device admin deactivation for event: $event")
-
- // Check if user is trying to deactivate the accessibility service
- if (isDeactivationAttempt(event)) {
- Log.d(TAG, "Blocking accessibility service deactivation")
- blockDeactivationAttempt()
- 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
-
- LogUtils.d(TAG, "User is on device admin page: $isDeviceAdminPage, $event")
-
- if (!isDeviceAdminPage) {
- return
- }
-
- blockDeviceAdminDeactivation()
- }
-
- 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")
-
- return isAccessibilitySettings || isSubSettings || isAlertDialog
- }
-
- @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)
- }
- }
-
- private fun isDeviceAdminPage(event: AccessibilityEvent): Boolean {
- val hasDeviceAdminDescription = event.contentDescription?.toString()?.lowercase()
- ?.contains("Device admin app") == true &&
- event.className == "android.widget.FrameLayout"
-
- val isAdminConfigClass =
- event.className!!.contains("DeviceAdminAdd") || event.className!!.contains("DeviceAdminSettings")
-
- return hasDeviceAdminDescription || isAdminConfigClass
- }
-
- @SuppressLint("InlinedApi")
- private fun blockDeviceAdminDeactivation() {
- try {
- val dpm: DevicePolicyManager? = getSystemService()
- val component = ComponentName(this, DeviceAdmin::class.java)
-
- 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)
- }
- }
-
- private fun findNodeWithTextContaining(
- node: AccessibilityNodeInfo,
- text: String
- ): AccessibilityNodeInfo? {
- return try {
- if (node.text?.toString()?.contains(text, ignoreCase = true) == true) {
- return node
- }
-
- 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
- }
- }
-
private fun getKeyboardPackageNames(): List {
return try {
getSystemService()?.enabledInputMethodList?.map { it.packageName }
@@ -498,11 +511,7 @@ class AppLockAccessibilityService : AccessibilityService() {
isSystemApp && !isOurApp
}
- systemLauncher?.activityInfo?.packageName?.also {
- if (it.isEmpty()) {
- Log.w(TAG, "Could not find a clear system launcher package name.")
- }
- } ?: ""
+ systemLauncher?.activityInfo?.packageName ?: ""
} catch (e: Exception) {
logError("Error getting system default launcher package", e)
""
@@ -515,17 +524,14 @@ class AppLockAccessibilityService : AccessibilityService() {
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) {
@@ -533,17 +539,67 @@ class AppLockAccessibilityService : AccessibilityService() {
}
}
- override fun onInterrupt() {
- try {
- LogUtils.d(TAG, "Accessibility service interrupted")
- } catch (e: Exception) {
- logError("Error in onInterrupt", e)
+ private fun startForegroundService() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ createNotificationChannel()
+ val notification = createNotification()
+
+ val type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ determineForegroundServiceType()
+ } else 0
+
+ try {
+ if (type != 0) {
+ startForeground(NOTIFICATION_ID, notification, type)
+ } else {
+ startForeground(NOTIFICATION_ID, notification)
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to start foreground service", e)
+ }
+ }
+ }
+
+ private fun determineForegroundServiceType(): Int {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ val dpm = getSystemService(DevicePolicyManager::class.java)
+ val component = ComponentName(this, DeviceAdmin::class.java)
+
+ return if (dpm?.isAdminActive(component) == true) {
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED
+ } else {
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
+ }
+ }
+ return 0
+ }
+
+ private fun createNotificationChannel() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val serviceChannel = NotificationChannel(
+ CHANNEL_ID,
+ "AppLock Accessibility Service",
+ NotificationManager.IMPORTANCE_LOW
+ )
+ notificationManager.createNotificationChannel(serviceChannel)
}
}
+ private fun createNotification(): Notification {
+ return NotificationCompat.Builder(this, CHANNEL_ID)
+ .setContentTitle("App Lock")
+ .setContentText("Accessibility service is protecting your apps")
+ .setSmallIcon(R.drawable.baseline_shield_24)
+ .setPriority(NotificationCompat.PRIORITY_LOW)
+ .setOngoing(true)
+ .build()
+ }
+
+ override fun onInterrupt() {
+ }
+
override fun onUnbind(intent: Intent?): Boolean {
return try {
- Log.d(TAG, "Accessibility service unbound")
isServiceRunning = false
AppLockManager.startFallbackServices(this, AppLockAccessibilityService::class.java)
@@ -562,13 +618,10 @@ class AppLockAccessibilityService : AccessibilityService() {
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)
@@ -578,10 +631,6 @@ class AppLockAccessibilityService : AccessibilityService() {
}
}
- /**
- * 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)
}
diff --git a/app/src/main/java/dev/pranav/applock/services/AppLockManager.kt b/app/src/main/java/dev/pranav/applock/services/AppLockManager.kt
index f34af00..dc6c816 100644
--- a/app/src/main/java/dev/pranav/applock/services/AppLockManager.kt
+++ b/app/src/main/java/dev/pranav/applock/services/AppLockManager.kt
@@ -1,6 +1,5 @@
package dev.pranav.applock.services
-import android.app.ActivityManager
import android.app.KeyguardManager
import android.content.Context
import android.content.Intent
@@ -19,6 +18,8 @@ object AppLockConstants {
"com.android.quickstep.RecentsActivity",
"com.android.systemui.recents.RecentsView",
"com.android.systemui.recents.RecentsPanelView",
+ "com.sec.android.app.launcher.recents.RecentsActivity",
+ "com.google.android.apps.nexuslauncher.RecentsActivity"
)
val EXCLUDED_APPS = setOf(
@@ -36,7 +37,51 @@ object AppLockConstants {
"com.android.settings.accessibility.AccessibilitySettings",
"com.android.settings.accessibility.AccessibilityMenuActivity",
"com.android.settings.accessibility.AccessibilityShortcutActivity",
- "com.android.settings.Settings\$AccessibilitySettingsActivity"
+ "com.android.settings.Settings\$AccessibilitySettingsActivity",
+ "com.android.settings.Settings\$AccessibilitySettings",
+ // Samsung specific
+ "com.samsung.android.settings.accessibility.AccessibilitySettings",
+ "com.samsung.android.settings.accessibility.AccessibilityShortcutActivity",
+ "com.samsung.android.settings.accessibility.AccessibilityMenuActivity",
+ "com.samsung.android.settings.accessibility.home.AccessibilitySettings",
+ "com.samsung.android.settings.accessibility.AccessibilityDetailsSettings",
+ "com.samsung.android.settings.accessibility.InstalledAppsActivity",
+ "com.samsung.android.settings.accessibility.ListServiceAccessibilitySettings",
+ "com.samsung.android.settings.accessibility.advanced.AdvancedSettingsActivity",
+ "com.samsung.android.settings.Settings\$AccessibilitySettingsActivity",
+ "com.samsung.android.settings.Settings\$AccessibilitySettings"
+ )
+
+ val DEVICE_ADMIN_SETTINGS_CLASSES = setOf(
+ "com.android.settings.Settings\$DeviceAdminSettingsActivity",
+ "com.android.settings.DeviceAdminSettings",
+ "com.android.settings.applications.specialaccess.deviceadmin.DeviceAdminSettings",
+ "com.android.settings.DeviceAdminAdd",
+ // Samsung specific
+ "com.samsung.android.settings.deviceadmin.DeviceAdminSettings",
+ "com.samsung.android.settings.deviceadmin.DeviceAdminAdd",
+ "com.samsung.android.settings.Settings\$DeviceAdminSettingsActivity"
+ )
+
+ val USAGE_ACCESS_SETTINGS_CLASSES = setOf(
+ "com.android.settings.Settings\$UsageAccessSettingsActivity",
+ "com.android.settings.applications.specialaccess.usageaccess.UsageAccessSettings",
+ "com.android.settings.UsageAccessSettings",
+ // Samsung specific
+ "com.samsung.android.settings.usageaccess.UsageAccessSettings",
+ "com.samsung.android.settings.Settings\$UsageAccessSettingsActivity"
+ )
+
+ val OVERLAY_SETTINGS_CLASSES = setOf(
+ "com.android.settings.Settings\$OverlaySettingsActivity",
+ "com.android.settings.Settings\$DrawOverlayDetailsActivity",
+ "com.android.settings.applications.specialaccess.drawoverlay.DrawOverlayDetails",
+ "com.android.settings.DrawOverlayDetails",
+ // Samsung specific
+ "com.samsung.android.settings.applications.specialaccess.drawoverlay.DrawOverlayDetails",
+ "com.samsung.android.settings.applications.specialaccess.drawoverlay.OverlaySettings",
+ "com.samsung.android.settings.Settings\$OverlaySettingsActivity",
+ "com.samsung.android.settings.Settings\$DrawOverlayDetailsActivity"
)
const val MAX_RESTART_ATTEMPTS = 3
@@ -49,11 +94,13 @@ fun Context.isDeviceLocked(): Boolean {
return keyguardManager?.isKeyguardLocked ?: false
}
-@Suppress("DEPRECATION")
fun Context.isServiceRunning(serviceClass: Class<*>): Boolean {
- val manager = getSystemService(ActivityManager::class.java) ?: return false
- return manager.getRunningServices(Int.MAX_VALUE)
- .any { serviceClass.name == it.service.className }
+ return when (serviceClass) {
+ AppLockAccessibilityService::class.java -> AppLockAccessibilityService.isServiceRunning
+ ShizukuAppLockService::class.java -> ShizukuAppLockService.isServiceRunning
+ ExperimentalAppLockService::class.java -> ExperimentalAppLockService.isServiceRunning
+ else -> false
+ }
}
object AppLockManager {
diff --git a/app/src/main/java/dev/pranav/applock/services/ExperimentalAppLockService.kt b/app/src/main/java/dev/pranav/applock/services/ExperimentalAppLockService.kt
index aa38e00..e9410c4 100644
--- a/app/src/main/java/dev/pranav/applock/services/ExperimentalAppLockService.kt
+++ b/app/src/main/java/dev/pranav/applock/services/ExperimentalAppLockService.kt
@@ -42,6 +42,11 @@ class ExperimentalAppLockService : Service() {
private var timer: Timer? = null
private var previousForegroundPackage = ""
+ companion object {
+ @Volatile
+ var isServiceRunning = false
+ }
+
private val screenStateReceiver = object: android.content.BroadcastReceiver() {
override fun onReceive(context: android.content.Context?, intent: Intent?) {
if (intent?.action == Intent.ACTION_SCREEN_OFF) {
@@ -64,6 +69,7 @@ class ExperimentalAppLockService : Service() {
return START_NOT_STICKY
}
+ isServiceRunning = true
AppLockManager.resetRestartAttempts(TAG)
appLockRepository.setActiveBackend(BackendImplementation.USAGE_STATS)
AppLockManager.stopAllOtherServices(this, this::class.java)
@@ -82,6 +88,7 @@ class ExperimentalAppLockService : Service() {
}
override fun onDestroy() {
+ isServiceRunning = false
timer?.cancel()
LogUtils.d(TAG, "Service destroyed. Checking for fallback.")
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 87a4760..052b355 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,5 +1,5 @@
- App Lock
+ AppLock-alod0-by-AP
Prevents unauthorized people from uninstalling the app.
Allows App Lock to detect when protected apps are opened and show password verification.
diff --git a/app/src/main/res/xml/accessibility_service_config.xml b/app/src/main/res/xml/accessibility_service_config.xml
index 7d4e056..e3ee6ca 100644
--- a/app/src/main/res/xml/accessibility_service_config.xml
+++ b/app/src/main/res/xml/accessibility_service_config.xml
@@ -1,10 +1,9 @@
-