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 @@ -