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..0bb98b3 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 = 22304
+ versionName = "3.1.0.00(18-03-26)"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8a00795..8567ce4 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -14,6 +14,10 @@
+
+
+
+
-
+
+ android:label="@string/app_name"
+ android:theme="@style/Theme.AppLock"
+ tools:ignore="RedundantLabel">
@@ -58,13 +65,17 @@
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:excludeFromRecents="true"
android:exported="false"
+ android:label="@string/app_name"
android:taskAffinity=""
- android:theme="@android:style/Theme.Material.NoActionBar.TranslucentDecor" />
+ android:theme="@android:style/Theme.Material.NoActionBar.TranslucentDecor"
+ tools:ignore="RedundantLabel" />
+ android:label="@string/app_name"
+ android:theme="@style/Theme.AppLock"
+ tools:ignore="RedundantLabel" />
@@ -104,6 +115,7 @@
android:name=".core.broadcast.DeviceAdmin"
android:description="@string/device_admin_description"
android:exported="false"
+ android:label="@string/app_name"
android:permission="android.permission.BIND_DEVICE_ADMIN">
diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png
index bd75f7e..edd9727 100644
Binary files a/app/src/main/ic_launcher-playstore.png and b/app/src/main/ic_launcher-playstore.png differ
diff --git a/app/src/main/java/dev/pranav/applock/MainActivity.kt b/app/src/main/java/dev/pranav/applock/MainActivity.kt
index f85cd6f..66a5414 100644
--- a/app/src/main/java/dev/pranav/applock/MainActivity.kt
+++ b/app/src/main/java/dev/pranav/applock/MainActivity.kt
@@ -3,7 +3,14 @@ package dev.pranav.applock
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
+import androidx.compose.material3.Text
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
import androidx.fragment.app.FragmentActivity
+import dev.pranav.applock.core.utils.BiometricStatus
+import dev.pranav.applock.core.utils.getBiometricStatus
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LifecycleEventEffect
import androidx.navigation.compose.rememberNavController
@@ -22,18 +29,25 @@ class MainActivity : FragmentActivity() {
navigationManager = NavigationManager(this)
+ val biometricStatus = getBiometricStatus(this)
setContent {
AppLockTheme {
- val navController = rememberNavController()
- val startDestination = navigationManager.determineStartDestination()
+ if (biometricStatus is BiometricStatus.Unavailable) {
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ Text(biometricStatus.message)
+ }
+ } else {
+ val navController = rememberNavController()
+ val startDestination = navigationManager.determineStartDestination()
- AppNavHost(
- navController = navController,
- startDestination = startDestination
- )
+ AppNavHost(
+ navController = navController,
+ startDestination = startDestination
+ )
- LifecycleEventEffect(Lifecycle.Event.ON_RESUME) {
- handleOnResume(navController)
+ LifecycleEventEffect(Lifecycle.Event.ON_RESUME) {
+ handleOnResume(navController)
+ }
}
}
}
@@ -46,7 +60,7 @@ class MainActivity : FragmentActivity() {
return
}
- if (currentRoute != Screen.PasswordOverlay.route && currentRoute != Screen.SetPassword.route && currentRoute != Screen.SetPasswordPattern.route) {
+ if (currentRoute != Screen.PasswordOverlay.route && currentRoute != Screen.SetPassword.route && currentRoute != Screen.SetPasswordPattern.route && currentRoute != Screen.SetPasswordText.route) {
navController.navigate(Screen.PasswordOverlay.route)
}
}
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/core/navigation/AppNavigator.kt b/app/src/main/java/dev/pranav/applock/core/navigation/AppNavigator.kt
index cace84e..31c4602 100644
--- a/app/src/main/java/dev/pranav/applock/core/navigation/AppNavigator.kt
+++ b/app/src/main/java/dev/pranav/applock/core/navigation/AppNavigator.kt
@@ -15,21 +15,25 @@ import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import dev.pranav.applock.AppLockApplication
+import dev.pranav.applock.core.utils.IntruderSelfieManager
import dev.pranav.applock.core.utils.LogUtils
import dev.pranav.applock.data.repository.PreferencesRepository
import dev.pranav.applock.features.antiuninstall.ui.AntiUninstallScreen
import dev.pranav.applock.features.appintro.ui.AppIntroScreen
import dev.pranav.applock.features.applist.ui.MainScreen
+import dev.pranav.applock.features.intruderselfie.ui.IntruderSelfieScreen
import dev.pranav.applock.features.lockscreen.ui.PasswordOverlayScreen
import dev.pranav.applock.features.lockscreen.ui.PatternLockScreen
import dev.pranav.applock.features.setpassword.ui.PatternSetPasswordScreen
import dev.pranav.applock.features.setpassword.ui.SetPasswordScreen
+import dev.pranav.applock.features.setpassword.ui.SetPasswordTextScreen
import dev.pranav.applock.features.settings.ui.SettingsScreen
import dev.pranav.applock.features.triggerexclusions.ui.TriggerExclusionsScreen
@Composable
fun AppNavHost(navController: NavHostController, startDestination: String) {
val application = LocalContext.current.applicationContext as AppLockApplication
+ val context = LocalContext.current
NavHost(
navController = navController,
@@ -52,23 +56,39 @@ fun AppNavHost(navController: NavHostController, startDestination: String) {
}
composable(Screen.ChangePassword.route) {
- if (application.appLockRepository.getLockType() == PreferencesRepository.LOCK_TYPE_PATTERN) {
- PatternSetPasswordScreen(navController, false)
- } else {
- SetPasswordScreen(navController, isFirstTimeSetup = false)
+ when (application.appLockRepository.getLockType()) {
+ PreferencesRepository.LOCK_TYPE_PATTERN -> PatternSetPasswordScreen(navController, false)
+ PreferencesRepository.LOCK_TYPE_PASSWORD -> SetPasswordTextScreen(navController, false)
+ else -> SetPasswordScreen(navController, isFirstTimeSetup = false)
}
}
+ composable(Screen.ChangePasswordPin.route) {
+ SetPasswordScreen(navController, isFirstTimeSetup = false)
+ }
+
+ composable(Screen.ChangePasswordPattern.route) {
+ PatternSetPasswordScreen(navController, false)
+ }
+
+ composable(Screen.ChangePasswordText.route) {
+ SetPasswordTextScreen(navController, false)
+ }
+
composable(Screen.SetPasswordPattern.route) {
PatternSetPasswordScreen(navController, isFirstTimeSetup = true)
}
+ composable(Screen.SetPasswordText.route) {
+ SetPasswordTextScreen(navController, isFirstTimeSetup = true)
+ }
+
composable(Screen.Main.route) {
MainScreen(navController)
}
composable(Screen.PasswordOverlay.route) {
- val context = LocalActivity.current as FragmentActivity
+ val activity = LocalActivity.current as FragmentActivity
val lockType = application.appLockRepository.getLockType()
when (lockType) {
@@ -78,12 +98,47 @@ fun AppNavHost(navController: NavHostController, startDestination: String) {
onPatternAttempt = { pattern ->
val isValid = application.appLockRepository.validatePattern(pattern)
if (isValid) {
+ IntruderSelfieManager.resetFailedAttempts()
handleAuthenticationSuccess(navController)
+ } else {
+ IntruderSelfieManager.recordFailedAttempt(context)
}
isValid
},
onBiometricAuth = {
- handleBiometricAuthentication(context, navController)
+ handleBiometricAuthentication(activity, navController)
+ },
+ onForgotPasscodeReset = {
+ navController.navigate(Screen.ChangePasswordPattern.route)
+ }
+ )
+ }
+
+ PreferencesRepository.LOCK_TYPE_PASSWORD -> {
+ PasswordOverlayScreen(
+ showBiometricButton = application.appLockRepository.isBiometricAuthEnabled(),
+ fromMainActivity = true,
+ useTextPassword = true,
+ onBiometricAuth = {
+ handleBiometricAuthentication(activity, navController)
+ },
+ onAuthSuccess = {
+ IntruderSelfieManager.resetFailedAttempts()
+ handleAuthenticationSuccess(navController)
+ },
+ onForgotPasscodeReset = {
+ navController.navigate(Screen.ChangePasswordText.route)
+ },
+ onPinAttempt = { pin, isFinal ->
+ val correctPassword = application.appLockRepository.getPassword() ?: ""
+ val isValid = pin == correctPassword
+ if (isValid) {
+ IntruderSelfieManager.resetFailedAttempts()
+ handleAuthenticationSuccess(navController)
+ } else if (isFinal) {
+ IntruderSelfieManager.recordFailedAttempt(context)
+ }
+ isValid
}
)
}
@@ -93,10 +148,30 @@ fun AppNavHost(navController: NavHostController, startDestination: String) {
showBiometricButton = application.appLockRepository.isBiometricAuthEnabled(),
fromMainActivity = true,
onBiometricAuth = {
- handleBiometricAuthentication(context, navController)
+ handleBiometricAuthentication(activity, navController)
},
onAuthSuccess = {
+ IntruderSelfieManager.resetFailedAttempts()
handleAuthenticationSuccess(navController)
+ },
+ onForgotPasscodeReset = {
+ navController.navigate(Screen.ChangePasswordPin.route)
+ },
+ onPinAttempt = { pin, isFinal ->
+ val correctPassword = application.appLockRepository.getPassword() ?: ""
+ val isValid = pin == correctPassword
+ if (isValid) {
+ IntruderSelfieManager.resetFailedAttempts()
+ handleAuthenticationSuccess(navController)
+ } else {
+ // Proper failed attempt count for PIN:
+ // 1. If it's a "Proceed" button click (isFinal = true), always record.
+ // 2. If it's an auto-unlock check, only record if length matches.
+ if (isFinal || (pin.length >= correctPassword.length && correctPassword.isNotEmpty())) {
+ IntruderSelfieManager.recordFailedAttempt(context)
+ }
+ }
+ isValid
}
)
}
@@ -114,6 +189,10 @@ fun AppNavHost(navController: NavHostController, startDestination: String) {
composable(Screen.AntiUninstall.route) {
AntiUninstallScreen(navController)
}
+
+ composable(Screen.IntruderSelfies.route) {
+ IntruderSelfieScreen(navController)
+ }
}
}
@@ -135,6 +214,7 @@ private fun handleBiometricAuthentication(
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
LogUtils.d(TAG, "Biometric authentication succeeded")
+ IntruderSelfieManager.resetFailedAttempts()
navigateToMain(navController)
}
@@ -182,6 +262,6 @@ private fun navigateToMain(navController: NavHostController) {
private const val TAG = "AppNavHost"
private const val ANIMATION_DURATION = 400
private const val SCALE_INITIAL = 0.9f
-private const val BIOMETRIC_TITLE = "Confirm password"
+private const val BIOMETRIC_TITLE = "APP Lock by AP"
private const val BIOMETRIC_SUBTITLE = "Confirm biometric to continue"
private const val BIOMETRIC_NEGATIVE_BUTTON = "Use PIN"
diff --git a/app/src/main/java/dev/pranav/applock/core/navigation/Screen.kt b/app/src/main/java/dev/pranav/applock/core/navigation/Screen.kt
index 1043033..980f9c1 100644
--- a/app/src/main/java/dev/pranav/applock/core/navigation/Screen.kt
+++ b/app/src/main/java/dev/pranav/applock/core/navigation/Screen.kt
@@ -4,10 +4,15 @@ sealed class Screen(val route: String) {
object AppIntro : Screen("app_intro")
object SetPassword : Screen("set_password")
object SetPasswordPattern : Screen("set_password_pattern")
+ object SetPasswordText : Screen("set_password_text")
object ChangePassword : Screen("change_password")
+ object ChangePasswordPin : Screen("change_password_pin")
+ object ChangePasswordPattern : Screen("change_password_pattern")
+ object ChangePasswordText : Screen("change_password_text")
object Main : Screen("main")
object PasswordOverlay : Screen("password_overlay")
object Settings : Screen("settings")
object TriggerExclusions : Screen("trigger_exclusions")
object AntiUninstall: Screen("anti_uninstall")
+ object IntruderSelfies: Screen("intruder_selfies")
}
diff --git a/app/src/main/java/dev/pranav/applock/core/utils/BiometricUtils.kt b/app/src/main/java/dev/pranav/applock/core/utils/BiometricUtils.kt
new file mode 100644
index 0000000..6e1554e
--- /dev/null
+++ b/app/src/main/java/dev/pranav/applock/core/utils/BiometricUtils.kt
@@ -0,0 +1,27 @@
+package dev.pranav.applock.core.utils
+
+import android.content.Context
+import androidx.biometric.BiometricManager
+
+sealed class BiometricStatus {
+ data object Available : BiometricStatus()
+ data class Unavailable(val message: String) : BiometricStatus()
+}
+
+fun getBiometricStatus(context: Context): BiometricStatus {
+ val biometricManager = BiometricManager.from(context)
+ return when (
+ biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)
+ ) {
+ BiometricManager.BIOMETRIC_SUCCESS -> BiometricStatus.Available
+ BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> BiometricStatus.Unavailable(
+ "Sorry this app cant run on this device because of security limitation: no biometric hardware."
+ )
+ BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> BiometricStatus.Unavailable(
+ "Sorry this app cant run on this device because of security limitation: biometric not enabled."
+ )
+ else -> BiometricStatus.Unavailable(
+ "Sorry this app cant run on this device because of security limitation: biometric authentication unavailable."
+ )
+ }
+}
diff --git a/app/src/main/java/dev/pranav/applock/core/utils/IntruderSelfieManager.kt b/app/src/main/java/dev/pranav/applock/core/utils/IntruderSelfieManager.kt
new file mode 100644
index 0000000..cfbd522
--- /dev/null
+++ b/app/src/main/java/dev/pranav/applock/core/utils/IntruderSelfieManager.kt
@@ -0,0 +1,113 @@
+package dev.pranav.applock.core.utils
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.ImageFormat
+import android.hardware.camera2.*
+import android.media.ImageReader
+import android.os.Handler
+import android.os.HandlerThread
+import android.util.Log
+import java.io.File
+import java.io.FileOutputStream
+import java.text.SimpleDateFormat
+import java.util.*
+
+object IntruderSelfieManager {
+ private const val TAG = "IntruderSelfieManager"
+ private var failedAttempts = 0
+
+ fun recordFailedAttempt(context: Context) {
+ val repository = context.appLockRepository()
+ if (!repository.isIntruderSelfieEnabled()) return
+
+ failedAttempts++
+ val requiredAttempts = repository.getIntruderSelfieAttempts()
+
+ if (failedAttempts >= requiredAttempts) {
+ captureSelfie(context)
+ failedAttempts = 0 // Reset after capture
+ }
+ }
+
+ fun resetFailedAttempts() {
+ failedAttempts = 0
+ }
+
+ @SuppressLint("MissingPermission")
+ private fun captureSelfie(context: Context) {
+ val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
+ try {
+ val cameraId = cameraManager.cameraIdList.find { id ->
+ cameraManager.getCameraCharacteristics(id)
+ .get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT
+ } ?: return
+
+ val handlerThread = HandlerThread("CameraBackground").apply { start() }
+ val handler = Handler(handlerThread.looper)
+
+ val imageReader = ImageReader.newInstance(640, 480, ImageFormat.JPEG, 1)
+ imageReader.setOnImageAvailableListener({ reader ->
+ val image = reader.acquireLatestImage()
+ val buffer = image.planes[0].buffer
+ val bytes = ByteArray(buffer.remaining())
+ buffer.get(bytes)
+ saveSelfie(context, bytes)
+ image.close()
+ // Done capturing, we can stop the thread later or here
+ }, handler)
+
+ cameraManager.openCamera(cameraId, object : CameraDevice.StateCallback() {
+ override fun onOpened(camera: CameraDevice) {
+ val captureBuilder = camera.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE)
+ captureBuilder.addTarget(imageReader.surface)
+
+ // On some devices, we might need a preview surface too, but we try without it first for "silent" capture
+ camera.createCaptureSession(listOf(imageReader.surface), object : CameraCaptureSession.StateCallback() {
+ override fun onConfigured(session: CameraCaptureSession) {
+ session.capture(captureBuilder.build(), object : CameraCaptureSession.CaptureCallback() {
+ override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) {
+ camera.close()
+ handlerThread.quitSafely()
+ }
+ }, handler)
+ }
+ override fun onConfigureFailed(session: CameraCaptureSession) {
+ camera.close()
+ handlerThread.quitSafely()
+ }
+ }, handler)
+ }
+
+ override fun onDisconnected(camera: CameraDevice) {
+ camera.close()
+ handlerThread.quitSafely()
+ }
+
+ override fun onError(camera: CameraDevice, error: Int) {
+ camera.close()
+ handlerThread.quitSafely()
+ }
+ }, handler)
+
+ } catch (e: Exception) {
+ Log.e(TAG, "Error capturing selfie", e)
+ }
+ }
+
+ private fun saveSelfie(context: Context, data: ByteArray) {
+ try {
+ val selfieDir = File(context.filesDir, "intruder_selfies")
+ if (!selfieDir.exists()) selfieDir.mkdirs()
+
+ val timeStamp = SimpleDateFormat("HHmmss_ddMMyyyy", Locale.getDefault()).format(Date())
+ val fileName = "Intruder_$timeStamp.jpg"
+ val file = File(selfieDir, fileName)
+
+ FileOutputStream(file).use { it.write(data) }
+ Log.d(TAG, "Selfie saved: ${file.absolutePath}")
+ } catch (e: Exception) {
+ Log.e(TAG, "Error saving selfie", e)
+ }
+ }
+}
diff --git a/app/src/main/java/dev/pranav/applock/core/utils/LogUtils.kt b/app/src/main/java/dev/pranav/applock/core/utils/LogUtils.kt
index 8f0afe5..cac2995 100644
--- a/app/src/main/java/dev/pranav/applock/core/utils/LogUtils.kt
+++ b/app/src/main/java/dev/pranav/applock/core/utils/LogUtils.kt
@@ -1,14 +1,20 @@
package dev.pranav.applock.core.utils
import android.annotation.SuppressLint
+import android.content.ContentValues
import android.content.Context
import android.net.Uri
import android.os.Build
+import android.os.Environment
+import android.provider.MediaStore
import android.util.Log
import androidx.core.content.FileProvider
import java.io.File
+import java.text.SimpleDateFormat
import java.time.Instant
import java.time.temporal.ChronoUnit
+import java.util.Date
+import java.util.Locale
import kotlin.concurrent.thread
@SuppressLint("StaticFieldLeak")
@@ -88,6 +94,55 @@ object LogUtils {
}
}
+ /**
+ * Export logs to the default Downloads directory in a subfolder with the app name.
+ * The filename format is "Logs+Time of export(hhmmss)+app name".
+ */
+ fun exportLogsToDownloads(appName: String): String? {
+ val timeStamp = SimpleDateFormat("HHmmss", Locale.getDefault()).format(Date())
+ val fileName = "Logs+${timeStamp}+$appName.txt"
+
+ return try {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ val resolver = context.contentResolver
+ val contentValues = ContentValues().apply {
+ put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
+ put(MediaStore.MediaColumns.MIME_TYPE, "text/plain")
+ put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS + "/" + appName)
+ }
+
+ val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
+ uri?.let {
+ resolver.openOutputStream(it)?.use { outputStream ->
+ val process = Runtime.getRuntime().exec("logcat -d")
+ process.inputStream.use { inputStream ->
+ inputStream.copyTo(outputStream)
+ }
+ }
+ "${Environment.DIRECTORY_DOWNLOADS}/$appName/$fileName"
+ }
+ } else {
+ // Fallback for older Android versions
+ val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
+ val appDir = File(downloadsDir, appName)
+ if (!appDir.exists()) {
+ appDir.mkdirs()
+ }
+ val targetFile = File(appDir, fileName)
+ targetFile.outputStream().use { outputStream ->
+ val process = Runtime.getRuntime().exec("logcat -d")
+ process.inputStream.use { inputStream ->
+ inputStream.copyTo(outputStream)
+ }
+ }
+ "${Environment.DIRECTORY_DOWNLOADS}/$appName/$fileName"
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Error exporting logs to downloads", e)
+ null
+ }
+ }
+
/**
* Clear all security and audit logs.
* Called when the app is updated.
diff --git a/app/src/main/java/dev/pranav/applock/core/utils/RecoveryKeyManager.kt b/app/src/main/java/dev/pranav/applock/core/utils/RecoveryKeyManager.kt
new file mode 100644
index 0000000..2d0958c
--- /dev/null
+++ b/app/src/main/java/dev/pranav/applock/core/utils/RecoveryKeyManager.kt
@@ -0,0 +1,44 @@
+package dev.pranav.applock.core.utils
+
+import android.content.ContentValues
+import android.content.Context
+import android.os.Build
+import android.os.Environment
+import android.provider.MediaStore
+import java.io.File
+import java.security.SecureRandom
+
+object RecoveryKeyManager {
+ private const val APP_FOLDER = "APP Lock by AP"
+ private const val FILE_PREFIX = "recovery-key-"
+ private const val CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
+ private val random = SecureRandom()
+
+ fun generateRecoveryKey(length: Int = 64): String = buildString(length) {
+ repeat(length) {
+ append(CHARSET[random.nextInt(CHARSET.length)])
+ }
+ }
+
+ fun saveRecoveryKeyToDownloads(context: Context, recoveryKey: String): Result = runCatching {
+ val fileName = "$FILE_PREFIX${System.currentTimeMillis()}.txt"
+ val body = "Recovery key for APP Lock by AP\n\n$recoveryKey\n"
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ val values = ContentValues().apply {
+ put(MediaStore.Downloads.DISPLAY_NAME, fileName)
+ put(MediaStore.Downloads.MIME_TYPE, "text/plain")
+ put(MediaStore.Downloads.RELATIVE_PATH, "${Environment.DIRECTORY_DOWNLOADS}/$APP_FOLDER")
+ }
+ val resolver = context.contentResolver
+ val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
+ ?: error("Could not create download file")
+ resolver.openOutputStream(uri)?.bufferedWriter()?.use { it.write(body) }
+ ?: error("Could not open download output stream")
+ } else {
+ val downloads = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
+ val dir = File(downloads, APP_FOLDER).apply { mkdirs() }
+ File(dir, fileName).writeText(body)
+ }
+ "Downloads/$APP_FOLDER/$fileName"
+ }
+}
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..a8698a1 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
@@ -2,6 +2,7 @@ package dev.pranav.applock.data.repository
import android.content.Context
import dev.pranav.applock.data.manager.BackendServiceManager
+import kotlinx.coroutines.flow.Flow
/**
* Main repository that coordinates between different specialized repositories and managers.
@@ -50,6 +51,11 @@ class AppLockRepository(private val context: Context) {
fun validatePattern(inputPattern: String): Boolean =
preferencesRepository.validatePattern(inputPattern)
+ fun setRecoveryKey(recoveryKey: String) = preferencesRepository.setRecoveryKey(recoveryKey)
+ fun getRecoveryKey(): String? = preferencesRepository.getRecoveryKey()
+ fun validateRecoveryKey(inputRecoveryKey: String): Boolean =
+ preferencesRepository.validateRecoveryKey(inputRecoveryKey)
+
fun setLockType(lockType: String) = preferencesRepository.setLockType(lockType)
fun getLockType(): String = preferencesRepository.getLockType()
@@ -60,6 +66,18 @@ class AppLockRepository(private val context: Context) {
fun setUseMaxBrightness(enabled: Boolean) = preferencesRepository.setUseMaxBrightness(enabled)
fun shouldUseMaxBrightness(): Boolean = preferencesRepository.shouldUseMaxBrightness()
+ fun setAmoledModeEnabled(enabled: Boolean) = preferencesRepository.setAmoledModeEnabled(enabled)
+ fun isAmoledModeEnabled(): Boolean = preferencesRepository.isAmoledModeEnabled()
+ fun amoledModeFlow(): Flow = preferencesRepository.amoledModeFlow
+
+ fun setDynamicColorEnabled(enabled: Boolean) = preferencesRepository.setDynamicColorEnabled(enabled)
+ fun isDynamicColorEnabled(): Boolean = preferencesRepository.isDynamicColorEnabled()
+ fun dynamicColorFlow(): Flow = preferencesRepository.dynamicColorFlow
+
+ fun setAppThemeMode(themeMode: AppThemeMode) = preferencesRepository.setAppThemeMode(themeMode)
+ fun getAppThemeMode(): AppThemeMode = preferencesRepository.getAppThemeMode()
+ fun appThemeModeFlow(): Flow = preferencesRepository.appThemeModeFlow
+
fun setDisableHaptics(enabled: Boolean) = preferencesRepository.setDisableHaptics(enabled)
fun shouldDisableHaptics(): Boolean = preferencesRepository.shouldDisableHaptics()
fun setShowSystemApps(enabled: Boolean) = preferencesRepository.setShowSystemApps(enabled)
@@ -69,6 +87,37 @@ 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 setPreventAllAppUninstallEnabled(enabled: Boolean) =
+ preferencesRepository.setPreventAllAppUninstallEnabled(enabled)
+
+ fun isPreventAllAppUninstallEnabled(): Boolean =
+ preferencesRepository.isPreventAllAppUninstallEnabled()
+
fun setProtectEnabled(enabled: Boolean) = preferencesRepository.setProtectEnabled(enabled)
fun isProtectEnabled(): Boolean = preferencesRepository.isProtectEnabled()
@@ -91,6 +140,11 @@ class AppLockRepository(private val context: Context) {
fun isLoggingEnabled(): Boolean = preferencesRepository.isLoggingEnabled()
fun setLoggingEnabled(enabled: Boolean) = preferencesRepository.setLoggingEnabled(enabled)
+ fun setIntruderSelfieEnabled(enabled: Boolean) = preferencesRepository.setIntruderSelfieEnabled(enabled)
+ fun isIntruderSelfieEnabled(): Boolean = preferencesRepository.isIntruderSelfieEnabled()
+ fun setIntruderSelfieAttempts(attempts: Int) = preferencesRepository.setIntruderSelfieAttempts(attempts)
+ fun getIntruderSelfieAttempts(): Int = preferencesRepository.getIntruderSelfieAttempts()
+
fun setActiveBackend(backend: BackendImplementation) =
backendServiceManager.setActiveBackend(backend)
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..99a6189 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
@@ -3,6 +3,9 @@ package dev.pranav.applock.data.repository
import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
/**
* Repository for managing application preferences and settings.
@@ -17,7 +20,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 +33,20 @@ class PreferencesRepository(context: Context) {
}
fun setPattern(pattern: String) {
- appLockPrefs.edit { putString(KEY_PATTERN, pattern) }
+ appLockPrefs.edit(commit = true) { putString(KEY_PATTERN, pattern) }
+ }
+
+ fun setRecoveryKey(recoveryKey: String) {
+ appLockPrefs.edit(commit = true) { putString(KEY_RECOVERY_KEY, recoveryKey) }
+ }
+
+ fun getRecoveryKey(): String? {
+ return appLockPrefs.getString(KEY_RECOVERY_KEY, null)
+ }
+
+ fun validateRecoveryKey(inputRecoveryKey: String): Boolean {
+ val storedRecoveryKey = getRecoveryKey()
+ return storedRecoveryKey != null && inputRecoveryKey == storedRecoveryKey
}
fun getPattern(): String? {
@@ -43,7 +59,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 {
@@ -66,6 +82,44 @@ class PreferencesRepository(context: Context) {
return settingsPrefs.getBoolean(KEY_USE_MAX_BRIGHTNESS, false)
}
+ fun setAmoledModeEnabled(enabled: Boolean) {
+ settingsPrefs.edit { putBoolean(KEY_AMOLED_MODE_ENABLED, enabled) }
+ _amoledModeFlow.value = enabled
+ }
+
+ fun isAmoledModeEnabled(): Boolean {
+ return settingsPrefs.getBoolean(KEY_AMOLED_MODE_ENABLED, false)
+ }
+
+ val amoledModeFlow: Flow = _amoledModeFlow.asStateFlow()
+
+ fun setDynamicColorEnabled(enabled: Boolean) {
+ settingsPrefs.edit { putBoolean(KEY_DYNAMIC_COLOR, enabled) }
+ _dynamicColorFlow.value = enabled
+ }
+
+ fun isDynamicColorEnabled(): Boolean {
+ return settingsPrefs.getBoolean(KEY_DYNAMIC_COLOR, false)
+ }
+
+ val dynamicColorFlow: Flow = _dynamicColorFlow.asStateFlow()
+
+ fun setAppThemeMode(themeMode: AppThemeMode) {
+ settingsPrefs.edit { putString(KEY_APP_THEME_MODE, themeMode.name) }
+ _appThemeModeFlow.value = themeMode
+ }
+
+ fun getAppThemeMode(): AppThemeMode {
+ val mode = settingsPrefs.getString(KEY_APP_THEME_MODE, AppThemeMode.SYSTEM.name)
+ return try {
+ AppThemeMode.valueOf(mode ?: AppThemeMode.SYSTEM.name)
+ } catch (_: IllegalArgumentException) {
+ AppThemeMode.SYSTEM
+ }
+ }
+
+ val appThemeModeFlow: Flow = _appThemeModeFlow.asStateFlow()
+
fun setDisableHaptics(enabled: Boolean) {
settingsPrefs.edit { putBoolean(KEY_DISABLE_HAPTICS, enabled) }
}
@@ -83,15 +137,55 @@ 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 setPreventAllAppUninstallEnabled(enabled: Boolean) {
+ settingsPrefs.edit(commit = true) { putBoolean(KEY_PREVENT_ALL_APP_UNINSTALL, enabled) }
+ }
+
+ fun isPreventAllAppUninstallEnabled(): Boolean {
+ return settingsPrefs.getBoolean(KEY_PREVENT_ALL_APP_UNINSTALL, 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 +209,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 {
@@ -154,16 +248,41 @@ class PreferencesRepository(context: Context) {
settingsPrefs.edit { putBoolean(KEY_LOGGING_ENABLED, enabled) }
}
+ fun setIntruderSelfieEnabled(enabled: Boolean) {
+ settingsPrefs.edit { putBoolean(KEY_INTRUDER_SELFIE_ENABLED, enabled) }
+ }
+
+ fun isIntruderSelfieEnabled(): Boolean {
+ return settingsPrefs.getBoolean(KEY_INTRUDER_SELFIE_ENABLED, false)
+ }
+
+ fun setIntruderSelfieAttempts(attempts: Int) {
+ settingsPrefs.edit { putInt(KEY_INTRUDER_SELFIE_ATTEMPTS, attempts) }
+ }
+
+ fun getIntruderSelfieAttempts(): Int {
+ return settingsPrefs.getInt(KEY_INTRUDER_SELFIE_ATTEMPTS, 3)
+ }
+
companion object {
private const val PREFS_NAME_APP_LOCK = "app_lock_prefs"
private const val PREFS_NAME_SETTINGS = "app_lock_settings"
private const val KEY_PASSWORD = "password"
private const val KEY_PATTERN = "pattern"
+ private const val KEY_RECOVERY_KEY = "recovery_key"
private const val KEY_BIOMETRIC_AUTH_ENABLED = "use_biometric_auth"
private const val KEY_DISABLE_HAPTICS = "disable_haptics"
private const val KEY_USE_MAX_BRIGHTNESS = "use_max_brightness"
+ private const val KEY_AMOLED_MODE_ENABLED = "amoled_mode_enabled"
+ private const val KEY_DYNAMIC_COLOR = "dynamic_color"
+ private const val KEY_APP_THEME_MODE = "app_theme_mode"
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_PREVENT_ALL_APP_UNINSTALL = "prevent_all_app_uninstall"
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"
@@ -174,11 +293,37 @@ class PreferencesRepository(context: Context) {
private const val KEY_AUTO_UNLOCK = "auto_unlock"
private const val KEY_SHOW_SYSTEM_APPS = "show_system_apps"
private const val KEY_LOCK_TYPE = "lock_type"
+ private const val KEY_INTRUDER_SELFIE_ENABLED = "intruder_selfie_enabled"
+ private const val KEY_INTRUDER_SELFIE_ATTEMPTS = "intruder_selfie_attempts"
private const val DEFAULT_PROTECT_ENABLED = true
private const val DEFAULT_UNLOCK_DURATION = 0
const val LOCK_TYPE_PIN = "pin"
const val LOCK_TYPE_PATTERN = "pattern"
+ const val LOCK_TYPE_PASSWORD = "password"
+
+ // Static flows to ensure all repository instances share the same state
+ private val _amoledModeFlow = MutableStateFlow(false)
+ private val _dynamicColorFlow = MutableStateFlow(false)
+ private val _appThemeModeFlow = MutableStateFlow(AppThemeMode.SYSTEM)
+
+ // Initialize static flows with actual values on first creation
+ private var initialized = false
}
+
+ init {
+ if (!initialized) {
+ _amoledModeFlow.value = isAmoledModeEnabled()
+ _dynamicColorFlow.value = isDynamicColorEnabled()
+ _appThemeModeFlow.value = getAppThemeMode()
+ initialized = true
+ }
+ }
+}
+
+enum class AppThemeMode {
+ SYSTEM,
+ LIGHT,
+ DARK
}
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..123f4f4 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()
},
@@ -196,7 +200,7 @@ fun AdminDisableScreen(
fromMainActivity = false,
onBiometricAuth = {},
onAuthSuccess = {},
- onPinAttempt = { pin ->
+ onPinAttempt = { pin, _ ->
val isValid = validatePassword(pin)
if (isValid) {
onPasswordVerified()
diff --git a/app/src/main/java/dev/pranav/applock/features/antiuninstall/ui/AntiUninstallScreen.kt b/app/src/main/java/dev/pranav/applock/features/antiuninstall/ui/AntiUninstallScreen.kt
index 1c429b4..38b9fe0 100644
--- a/app/src/main/java/dev/pranav/applock/features/antiuninstall/ui/AntiUninstallScreen.kt
+++ b/app/src/main/java/dev/pranav/applock/features/antiuninstall/ui/AntiUninstallScreen.kt
@@ -357,11 +357,40 @@ private fun SearchTopBar(
@Composable
private fun AppProtectionItem(app: AppInfo, isProtected: Boolean, onToggle: () -> Unit) {
+ var showConfirmation by remember { mutableStateOf(false) }
+
+ if (showConfirmation) {
+ AlertDialog(
+ onDismissRequest = { showConfirmation = false },
+ title = { Text("Remove Protection?") },
+ text = { Text("Are you sure you want to remove anti-uninstall protection from ${app.name}?") },
+ confirmButton = {
+ TextButton(onClick = {
+ showConfirmation = false
+ onToggle()
+ }) {
+ Text("Remove", color = MaterialTheme.colorScheme.error)
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = { showConfirmation = false }) {
+ Text("Cancel")
+ }
+ }
+ )
+ }
+
Surface(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
- .clickable { onToggle() },
+ .clickable {
+ if (isProtected) {
+ showConfirmation = true
+ } else {
+ onToggle()
+ }
+ },
color = MaterialTheme.colorScheme.surfaceContainer,
shape = RoundedCornerShape(12.dp)
) {
@@ -393,18 +422,50 @@ private fun AppProtectionItem(app: AppInfo, isProtected: Boolean, onToggle: () -
overflow = TextOverflow.Ellipsis
)
}
- Switch(checked = isProtected, onCheckedChange = { onToggle() })
+ Switch(
+ checked = isProtected,
+ onCheckedChange = {
+ if (isProtected) {
+ showConfirmation = true
+ } else {
+ onToggle()
+ }
+ }
+ )
}
}
}
@Composable
private fun ManualPackageItem(packageName: String, onToggle: () -> Unit) {
+ var showConfirmation by remember { mutableStateOf(false) }
+
+ if (showConfirmation) {
+ AlertDialog(
+ onDismissRequest = { showConfirmation = false },
+ title = { Text("Remove Protection?") },
+ text = { Text("Are you sure you want to remove anti-uninstall protection from $packageName?") },
+ confirmButton = {
+ TextButton(onClick = {
+ showConfirmation = false
+ onToggle()
+ }) {
+ Text("Remove", color = MaterialTheme.colorScheme.error)
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = { showConfirmation = false }) {
+ Text("Cancel")
+ }
+ }
+ )
+ }
+
Surface(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
- .clickable { onToggle() },
+ .clickable { showConfirmation = true },
color = MaterialTheme.colorScheme.surfaceContainer,
shape = RoundedCornerShape(12.dp)
) {
@@ -437,7 +498,7 @@ private fun ManualPackageItem(packageName: String, onToggle: () -> Unit) {
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
- Switch(checked = true, onCheckedChange = { onToggle() })
+ Switch(checked = true, onCheckedChange = { showConfirmation = true })
}
}
}
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/applist/ui/MainScreen.kt b/app/src/main/java/dev/pranav/applock/features/applist/ui/MainScreen.kt
index d92116d..7705b8e 100644
--- a/app/src/main/java/dev/pranav/applock/features/applist/ui/MainScreen.kt
+++ b/app/src/main/java/dev/pranav/applock/features/applist/ui/MainScreen.kt
@@ -35,6 +35,7 @@ import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
@@ -147,7 +148,7 @@ fun MainScreen(
modifier = Modifier
.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection),
- containerColor = MaterialTheme.colorScheme.surfaceContainer,
+ containerColor = MaterialTheme.colorScheme.background,
topBar = {
MediumFlexibleTopAppBar(
title = {
@@ -228,7 +229,7 @@ fun MainScreen(
},
scrollBehavior = scrollBehavior,
colors = TopAppBarDefaults.topAppBarColors(
- containerColor = MaterialTheme.colorScheme.surfaceContainer
+ containerColor = MaterialTheme.colorScheme.background
)
)
},
@@ -401,8 +402,8 @@ private fun ProtectedAppsDashboard(
} else {
LazyColumn(
modifier = modifier,
- contentPadding = PaddingValues(bottom = 88.dp, top = 8.dp), // Extra padding for FAB
- verticalArrangement = Arrangement.spacedBy(4.dp)
+ contentPadding = PaddingValues(bottom = 88.dp, top = 8.dp, start = 16.dp, end = 16.dp), // Extra padding for FAB and sides
+ verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(lockedApps, key = { it.packageName }) { appInfo ->
ProtectedAppItem(
@@ -438,7 +439,7 @@ private fun EmptyDashboardState(modifier: Modifier = Modifier) {
text = "Tap the + button to selectively secure your apps.",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
- textAlign = androidx.compose.ui.text.style.TextAlign.Center,
+ textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 32.dp)
)
}
@@ -568,6 +569,7 @@ private fun ProtectedAppItem(
var appName by remember(appInfo) { mutableStateOf(null) }
var icon by remember(appInfo) { mutableStateOf(null) }
+ var showConfirmation by remember { mutableStateOf(false) }
LaunchedEffect(appInfo) {
withContext(Dispatchers.IO) {
@@ -576,64 +578,89 @@ private fun ProtectedAppItem(
}
}
- ListItem(
- headlineContent = {
- if (appName != null) {
+ if (showConfirmation) {
+ AlertDialog(
+ onDismissRequest = { showConfirmation = false },
+ title = { Text("Remove Protection?") },
+ text = { Text("Are you sure you want to remove protection from ${appName ?: "this app"}?") },
+ confirmButton = {
+ TextButton(onClick = {
+ showConfirmation = false
+ onUnlock()
+ }) {
+ Text("Remove", color = MaterialTheme.colorScheme.error)
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = { showConfirmation = false }) {
+ Text("Cancel")
+ }
+ }
+ )
+ }
+
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(16.dp)),
+ color = MaterialTheme.colorScheme.surfaceContainer,
+ shape = RoundedCornerShape(16.dp)
+ ) {
+ ListItem(
+ headlineContent = {
+ if (appName != null) {
+ Text(
+ text = appName!!,
+ style = MaterialTheme.typography.bodyLarge,
+ fontWeight = FontWeight.Medium,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ },
+ supportingContent = {
Text(
- text = appName!!,
- style = MaterialTheme.typography.bodyLarge,
- fontWeight = FontWeight.Medium,
+ text = "Protected",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
- }
- },
- supportingContent = {
- Text(
- text = "Protected",
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.primary,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis
- )
- },
- leadingContent = {
- Surface(
- modifier = Modifier.size(48.dp),
- shape = RoundedCornerShape(12.dp),
- color = MaterialTheme.colorScheme.surfaceContainerHigh
- ) {
- Box(
- modifier = Modifier.fillMaxSize(),
- contentAlignment = Alignment.Center
+ },
+ leadingContent = {
+ Surface(
+ modifier = Modifier.size(48.dp),
+ shape = RoundedCornerShape(12.dp),
+ color = MaterialTheme.colorScheme.surfaceContainerHigh
) {
- if (icon != null) {
- Image(
- bitmap = icon!!,
- contentDescription = appName,
- modifier = Modifier.size(32.dp)
- )
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ if (icon != null) {
+ Image(
+ bitmap = icon!!,
+ contentDescription = appName,
+ modifier = Modifier.size(32.dp)
+ )
+ }
}
}
- }
- },
- trailingContent = {
- IconButton(onClick = onUnlock) {
- Icon(
- imageVector = Icons.Outlined.LockOpen,
- contentDescription = "Unlock ${appName ?: "app"}",
- tint = MaterialTheme.colorScheme.onSurfaceVariant
- )
- }
- },
- colors = ListItemDefaults.colors(
- containerColor = MaterialTheme.colorScheme.surfaceContainer
- ),
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp, vertical = 4.dp)
- .clip(RoundedCornerShape(16.dp))
- )
+ },
+ trailingContent = {
+ IconButton(onClick = { showConfirmation = true }) {
+ Icon(
+ imageVector = Icons.Outlined.LockOpen,
+ contentDescription = "Unlock ${appName ?: "app"}",
+ tint = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ },
+ colors = ListItemDefaults.colors(
+ containerColor = androidx.compose.ui.graphics.Color.Transparent
+ )
+ )
+ }
}
@Composable
@@ -654,64 +681,72 @@ private fun SelectableAppItem(
}
}
- ListItem(
- headlineContent = {
- if (appName != null) {
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 4.dp)
+ .clip(RoundedCornerShape(16.dp))
+ .clickable(onClick = onClick),
+ color = MaterialTheme.colorScheme.surfaceContainer,
+ shape = RoundedCornerShape(16.dp)
+ ) {
+ ListItem(
+ headlineContent = {
+ if (appName != null) {
+ Text(
+ text = appName!!,
+ style = MaterialTheme.typography.bodyLarge,
+ fontWeight = FontWeight.Medium,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ },
+ supportingContent = {
Text(
- text = appName!!,
- style = MaterialTheme.typography.bodyLarge,
- fontWeight = FontWeight.Medium,
+ text = appInfo.packageName,
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
- }
- },
- supportingContent = {
- Text(
- text = appInfo.packageName,
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis
- )
- },
- leadingContent = {
- Surface(
- modifier = Modifier.size(42.dp),
- shape = RoundedCornerShape(8.dp),
- color = MaterialTheme.colorScheme.surfaceContainerHigh
- ) {
- Box(
- modifier = Modifier.fillMaxSize(),
- contentAlignment = Alignment.Center
+ },
+ leadingContent = {
+ Surface(
+ modifier = Modifier.size(42.dp),
+ shape = RoundedCornerShape(8.dp),
+ color = MaterialTheme.colorScheme.surfaceContainerHigh
) {
- if (icon != null) {
- Image(
- bitmap = icon!!,
- contentDescription = appName,
- modifier = Modifier.size(28.dp)
- )
- } else {
- CircularProgressIndicator(
- modifier = Modifier.size(16.dp),
- strokeWidth = 2.dp
- )
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ if (icon != null) {
+ Image(
+ bitmap = icon!!,
+ contentDescription = appName,
+ modifier = Modifier.size(28.dp)
+ )
+ } else {
+ CircularProgressIndicator(
+ modifier = Modifier.size(16.dp),
+ strokeWidth = 2.dp
+ )
+ }
}
}
- }
- },
- trailingContent = {
- Checkbox(
- checked = isSelected,
- onCheckedChange = null
+ },
+ trailingContent = {
+ Checkbox(
+ checked = isSelected,
+ onCheckedChange = null
+ )
+ },
+ colors = ListItemDefaults.colors(
+ containerColor = androidx.compose.ui.graphics.Color.Transparent
)
- },
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp, vertical = 4.dp)
- .clip(RoundedCornerShape(16.dp))
- .clickable(onClick = onClick)
- )
+ )
+ }
}
@Composable
diff --git a/app/src/main/java/dev/pranav/applock/features/intruderselfie/ui/IntruderSelfieScreen.kt b/app/src/main/java/dev/pranav/applock/features/intruderselfie/ui/IntruderSelfieScreen.kt
new file mode 100644
index 0000000..442aec3
--- /dev/null
+++ b/app/src/main/java/dev/pranav/applock/features/intruderselfie/ui/IntruderSelfieScreen.kt
@@ -0,0 +1,163 @@
+package dev.pranav.applock.features.intruderselfie.ui
+
+import android.graphics.BitmapFactory
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.filled.Delete
+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.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.navigation.NavController
+import java.io.File
+import java.text.SimpleDateFormat
+import java.util.*
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun IntruderSelfieScreen(navController: NavController) {
+ val context = LocalContext.current
+ var selfies by remember { mutableStateOf(listOf()) }
+ var selfieToDelete by remember { mutableStateOf(null) }
+
+ fun loadSelfies() {
+ val selfieDir = File(context.filesDir, "intruder_selfies")
+ if (selfieDir.exists()) {
+ selfies = selfieDir.listFiles()?.filter { it.extension == "jpg" }?.sortedByDescending { it.lastModified() } ?: emptyList()
+ }
+ }
+
+ LaunchedEffect(Unit) {
+ loadSelfies()
+ }
+
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text("Intruder Selfies") },
+ navigationIcon = {
+ IconButton(onClick = { navController.popBackStack() }) {
+ Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
+ }
+ }
+ )
+ }
+ ) { padding ->
+ if (selfies.isEmpty()) {
+ Box(modifier = Modifier.fillMaxSize().padding(padding), contentAlignment = Alignment.Center) {
+ Text("No intruder selfies captured yet.", style = MaterialTheme.typography.bodyLarge)
+ }
+ } else {
+ LazyColumn(
+ modifier = Modifier.fillMaxSize().padding(padding),
+ contentPadding = PaddingValues(16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ items(selfies) { selfie ->
+ SelfieCard(
+ file = selfie,
+ onDelete = { selfieToDelete = selfie }
+ )
+ }
+ }
+ }
+ }
+
+ if (selfieToDelete != null) {
+ AlertDialog(
+ onDismissRequest = { selfieToDelete = null },
+ title = { Text("Delete Selfie") },
+ text = { Text("Are you sure you want to delete this intruder selfie?") },
+ confirmButton = {
+ TextButton(
+ onClick = {
+ selfieToDelete?.delete()
+ selfieToDelete = null
+ loadSelfies()
+ }
+ ) {
+ Text("Delete", color = MaterialTheme.colorScheme.error)
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = { selfieToDelete = null }) {
+ Text("Cancel")
+ }
+ }
+ )
+ }
+}
+
+@Composable
+fun SelfieCard(file: File, onDelete: () -> Unit) {
+ val bitmap = remember(file) {
+ BitmapFactory.decodeFile(file.absolutePath)
+ }
+
+ val timestamp = remember(file) {
+ val date = Date(file.lastModified())
+ val sdf = SimpleDateFormat("HH:mm:ss dd/MM/yyyy", Locale.getDefault())
+ sdf.format(date)
+ }
+
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(16.dp),
+ colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerHigh)
+ ) {
+ Column {
+ if (bitmap != null) {
+ Image(
+ bitmap = bitmap.asImageBitmap(),
+ contentDescription = "Intruder Selfie",
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(250.dp)
+ .clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)),
+ contentScale = ContentScale.Crop
+ )
+ }
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Column {
+ Text(
+ text = "Intruder Detected",
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold
+ )
+ Text(
+ text = timestamp,
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+
+ IconButton(onClick = onDelete) {
+ Icon(
+ imageVector = Icons.Default.Delete,
+ contentDescription = "Delete",
+ tint = MaterialTheme.colorScheme.error
+ )
+ }
+ }
+ }
+ }
+}
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..e668f1c 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
@@ -28,15 +38,19 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
import androidx.compose.material3.*
+import androidx.compose.ui.text.input.PasswordVisualTransformation
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,7 +59,9 @@ 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.IntruderSelfieManager
import dev.pranav.applock.core.utils.appLockRepository
import dev.pranav.applock.core.utils.vibrate
import dev.pranav.applock.data.repository.AppLockRepository
@@ -54,9 +70,12 @@ import dev.pranav.applock.services.AppLockManager
import dev.pranav.applock.ui.icons.Backspace
import dev.pranav.applock.ui.icons.Fingerprint
import dev.pranav.applock.ui.theme.AppLockTheme
+import dev.pranav.applock.features.setpassword.ui.RecoveryKeyDialog
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import java.util.concurrent.Executor
+import java.util.concurrent.atomic.AtomicBoolean
class PasswordOverlayActivity : FragmentActivity() {
private lateinit var executor: Executor
@@ -68,10 +87,30 @@ class PasswordOverlayActivity : FragmentActivity() {
private var isBiometricPromptShowingLocal = false
private var movedToBackground = false
- private var appName: String = ""
+ private val isGoingHome = AtomicBoolean(false)
+
+ 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", "homekey" -> {
+ // Home or Recents button pressed - trigger HOME action
+ Log.d(TAG, "System dialog action detected ($reason) - triggering HOME action")
+ goHome()
+ }
+ }
+ }
+ }
+ }
+
+ @SuppressLint("UnspecifiedRegisterReceiverFlag")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -87,12 +126,44 @@ 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() {
+ if (isGoingHome.getAndSet(true)) return // Prevent multiple calls
+
+ try {
+ // Record that we are leaving the locked app to ensure grace period triggers correctly
+ lockedPackageNameFromIntent?.let {
+ AppLockManager.setRecentlyLeftApp(it)
+ }
+
+ val intent = Intent(Intent.ACTION_MAIN).apply {
+ addCategory(Intent.CATEGORY_HOME)
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ }
+ startActivity(intent)
+
+ // Finish activity after starting home intent to clear overlay
+ finish()
+ } catch (e: Exception) {
+ Log.e(TAG, "Error going home: ${e.message}")
+ isGoingHome.set(false)
+ }
}
override fun onPostCreate(savedInstanceState: Bundle?) {
@@ -118,7 +189,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 +202,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,31 +218,58 @@ 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) {
+ @Suppress("DEPRECATION")
+ 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()
}
private fun setupUI() {
- val onPinAttemptCallback = { pin: String ->
- val isValid = appLockRepository.validatePassword(pin)
+ val onPinAttemptCallback = { pin: String, isFinal: Boolean ->
+ val correctPassword = appLockRepository.getPassword() ?: ""
+ val isValid = pin == correctPassword
+
if (isValid) {
+ IntruderSelfieManager.resetFailedAttempts()
lockedPackageNameFromIntent?.let { pkgName ->
AppLockManager.unlockApp(pkgName)
-
finishAfterTransition()
}
+ } else {
+ // Proper failed attempt count:
+ // 1. If it's a "Proceed" button click, it's always a failed attempt.
+ // 2. If it's an auto-unlock check, only count if the length matches or exceeds the correct password length.
+ if (isFinal || (pin.length >= correctPassword.length && correctPassword.isNotEmpty())) {
+ IntruderSelfieManager.recordFailedAttempt(this@PasswordOverlayActivity)
+ }
}
isValid
}
@@ -170,11 +277,14 @@ class PasswordOverlayActivity : FragmentActivity() {
val onPatternAttemptCallback = { pattern: String ->
val isValid = appLockRepository.validatePattern(pattern)
if (isValid) {
+ IntruderSelfieManager.resetFailedAttempts()
lockedPackageNameFromIntent?.let { pkgName ->
AppLockManager.unlockApp(pkgName)
finishAfterTransition()
}
+ } else {
+ IntruderSelfieManager.recordFailedAttempt(this@PasswordOverlayActivity)
}
isValid
}
@@ -183,7 +293,8 @@ class PasswordOverlayActivity : FragmentActivity() {
AppLockTheme {
Scaffold(
modifier = Modifier.fillMaxSize(),
- contentColor = MaterialTheme.colorScheme.primaryContainer
+ containerColor = MaterialTheme.colorScheme.background,
+ contentColor = MaterialTheme.colorScheme.onBackground
) { innerPadding ->
val lockType = remember { appLockRepository.getLockType() }
when (lockType) {
@@ -192,8 +303,27 @@ class PasswordOverlayActivity : FragmentActivity() {
modifier = Modifier.padding(innerPadding),
fromMainActivity = false,
lockedAppName = appName,
+ lockedAppIcon = appIcon,
+ triggeringPackageName = triggeringPackageNameFromIntent,
+ onPatternAttempt = onPatternAttemptCallback,
+ onBiometricAuth = { triggerBiometricPrompt() },
+ onForgotPasscodeReset = { launchResetFlow() }
+ )
+ }
+
+ PreferencesRepository.LOCK_TYPE_PASSWORD -> {
+ PasswordOverlayScreen(
+ modifier = Modifier.padding(innerPadding),
+ showBiometricButton = appLockRepository.isBiometricAuthEnabled(),
+ fromMainActivity = false,
+ useTextPassword = true,
+ onBiometricAuth = { triggerBiometricPrompt() },
+ onAuthSuccess = { IntruderSelfieManager.resetFailedAttempts() },
+ lockedAppName = appName,
+ lockedAppIcon = appIcon,
triggeringPackageName = triggeringPackageNameFromIntent,
- onPatternAttempt = onPatternAttemptCallback
+ onForgotPasscodeReset = { launchResetFlow() },
+ onPinAttempt = onPinAttemptCallback
)
}
@@ -203,9 +333,11 @@ class PasswordOverlayActivity : FragmentActivity() {
showBiometricButton = appLockRepository.isBiometricAuthEnabled(),
fromMainActivity = false,
onBiometricAuth = { triggerBiometricPrompt() },
- onAuthSuccess = {},
+ onAuthSuccess = { IntruderSelfieManager.resetFailedAttempts() },
lockedAppName = appName,
+ lockedAppIcon = appIcon,
triggeringPackageName = triggeringPackageNameFromIntent,
+ onForgotPasscodeReset = { launchResetFlow() },
onPinAttempt = onPinAttemptCallback
)
}
@@ -215,6 +347,13 @@ class PasswordOverlayActivity : FragmentActivity() {
}
}
+ private fun launchResetFlow() {
+ startActivity(Intent(this, dev.pranav.applock.MainActivity::class.java).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ })
+ finish()
+ }
+
private fun setupBiometricPromptInternal() {
executor = ContextCompat.getMainExecutor(this)
biometricPrompt =
@@ -239,16 +378,21 @@ class PasswordOverlayActivity : FragmentActivity() {
super.onAuthenticationError(errorCode, errString)
isBiometricPromptShowingLocal = false
AppLockManager.reportBiometricAuthFinished()
- Log.w(TAG, "Authentication error: $errString ($errorCode)")
+
+ // Only log unexpected errors, ignore cancellation during navigation
+ if (errorCode != BiometricPrompt.ERROR_CANCELED &&
+ errorCode != BiometricPrompt.ERROR_USER_CANCELED &&
+ errorCode != BiometricPrompt.ERROR_NEGATIVE_BUTTON) {
+ Log.w(TAG, "Authentication error: $errString ($errorCode)")
+ }
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
isBiometricPromptShowingLocal = false
+ IntruderSelfieManager.resetFailedAttempts()
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 +401,8 @@ class PasswordOverlayActivity : FragmentActivity() {
override fun onResume() {
super.onResume()
movedToBackground = false
- AppLockManager.isLockScreenShown.set(true) // Set to true when activity is visible
+ isGoingHome.set(false)
+ AppLockManager.isLockScreenShown.set(true)
lifecycleScope.launch {
applyUserPreferences()
}
@@ -275,7 +420,7 @@ class PasswordOverlayActivity : FragmentActivity() {
}
fun triggerBiometricPrompt() {
- if (appLockRepository.isBiometricAuthEnabled()) {
+ if (appLockRepository.isBiometricAuthEnabled() && !isGoingHome.get()) {
AppLockManager.reportBiometricAuthStarted()
isBiometricPromptShowingLocal = true
try {
@@ -288,9 +433,16 @@ 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) {
+ if (!isChangingConfigurations() && !isBiometricPromptShowingLocal && !movedToBackground && !isGoingHome.get()) {
AppLockManager.isLockScreenShown.set(false)
AppLockManager.reportBiometricAuthFinished()
finish()
@@ -314,6 +466,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,8 +487,11 @@ fun PasswordOverlayScreen(
onBiometricAuth: () -> Unit = {},
onAuthSuccess: () -> Unit,
lockedAppName: String? = null,
+ lockedAppIcon: Drawable? = null,
triggeringPackageName: String? = null,
- onPinAttempt: ((pin: String) -> Boolean)? = null
+ useTextPassword: Boolean = false,
+ onForgotPasscodeReset: (() -> Unit)? = null,
+ onPinAttempt: ((pin: String, isFinal: Boolean) -> Boolean)? = null
) {
val appLockRepository = LocalContext.current.appLockRepository()
val windowInfo = LocalWindowInfo.current
@@ -345,13 +505,66 @@ fun PasswordOverlayScreen(
Surface(
modifier = modifier.fillMaxSize(),
- color = MaterialTheme.colorScheme.surfaceContainer
+ color = MaterialTheme.colorScheme.background
) {
val passwordState = remember { mutableStateOf("") }
var showError by remember { mutableStateOf(false) }
+ var showRecoveryDialog by remember { mutableStateOf(false) }
val minLength = 4
- if (isLandscape) {
+ if (showRecoveryDialog) {
+ RecoveryKeyDialog(
+ onDismiss = { showRecoveryDialog = false },
+ onValidated = { onForgotPasscodeReset?.invoke() }
+ )
+ }
+
+ if (useTextPassword) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(24.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ AppHeader(
+ fromMainActivity = fromMainActivity,
+ lockedAppName = lockedAppName,
+ lockedAppIcon = lockedAppIcon,
+ style = MaterialTheme.typography.headlineSmall
+ )
+ Spacer(modifier = Modifier.height(24.dp))
+ OutlinedTextField(
+ value = passwordState.value,
+ onValueChange = {
+ passwordState.value = it
+ showError = false
+ },
+ label = { Text("Password") },
+ visualTransformation = PasswordVisualTransformation(),
+ singleLine = true,
+ modifier = Modifier.fillMaxWidth()
+ )
+ if (showError) {
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(stringResource(R.string.incorrect_password_error), color = MaterialTheme.colorScheme.error)
+ }
+ Spacer(modifier = Modifier.height(16.dp))
+ Button(onClick = {
+ val valid = onPinAttempt?.invoke(passwordState.value, true) ?: false
+ if (valid) {
+ onAuthSuccess()
+ } else {
+ showError = true
+ passwordState.value = ""
+ }
+ }, modifier = Modifier.fillMaxWidth()) { Text("Unlock") }
+ TextButton(onClick = { showRecoveryDialog = true }) { Text("Forgot passcode?") }
+ if (showBiometricButton) {
+ TextButton(onClick = onBiometricAuth) { Text("Use biometric") }
+ }
+ }
+ } else if (isLandscape) {
Row(
modifier = Modifier
.fillMaxSize()
@@ -366,25 +579,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(
@@ -417,11 +618,12 @@ fun PasswordOverlayScreen(
showError = false
if (appLockRepository.isAutoUnlockEnabled()) {
- onPinAttempt?.invoke(passwordState.value)
+ onPinAttempt?.invoke(passwordState.value, false)
}
},
onPinIncorrect = { showError = true }
)
+ TextButton(onClick = { showRecoveryDialog = true }) { Text("Forgot passcode?") }
}
}
} else {
@@ -436,27 +638,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(
@@ -486,11 +677,12 @@ fun PasswordOverlayScreen(
showError = false
if (appLockRepository.isAutoUnlockEnabled()) {
- onPinAttempt?.invoke(passwordState.value)
+ onPinAttempt?.invoke(passwordState.value, false)
}
},
onPinIncorrect = { showError = true }
)
+ TextButton(onClick = { showRecoveryDialog = true }) { Text("Forgot passcode?") }
}
}
}
@@ -500,6 +692,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(
@@ -556,7 +812,7 @@ fun PasswordIndicators(
) {
LazyRow(
state = lazyListState,
- contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 16.dp),
+ contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(
indicatorSpacing,
Alignment.CenterHorizontally
@@ -648,7 +904,7 @@ fun KeypadSection(
fromMainActivity: Boolean = false,
onBiometricAuth: () -> Unit,
onAuthSuccess: () -> Unit,
- onPinAttempt: ((pin: String) -> Boolean)? = null,
+ onPinAttempt: ((pin: String, isFinal: Boolean) -> Boolean)? = null,
onPasswordChange: () -> Unit,
onPinIncorrect: () -> Unit
) {
@@ -679,9 +935,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
@@ -694,10 +947,11 @@ fun KeypadSection(
horizontalPadding,
showBiometricButton
) {
+ val rows = if (showBiometricButton) 5f else 4f
if (isLandscape) {
val availableLandscapeHeight = screenHeightDp * 0.8f
- val totalVerticalSpacing = buttonSpacing * 3
- val heightBasedSize = (availableLandscapeHeight - totalVerticalSpacing) / 4f
+ val totalVerticalSpacing = buttonSpacing * (rows - 1)
+ val heightBasedSize = (availableLandscapeHeight - totalVerticalSpacing) / rows
val availableWidth = (screenWidthDp * 0.45f)
val totalHorizontalSpacing = buttonSpacing * 2
@@ -709,13 +963,9 @@ 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 totalVerticalSpacing = buttonSpacing * (rows - 1)
val heightBasedSize =
- (availableHeight - totalVerticalSpacing - biometricAllowance) / 4f
+ (availableHeight - totalVerticalSpacing) / rows
minOf(widthBasedSize, heightBasedSize)
}
@@ -769,26 +1019,9 @@ fun KeypadSection(
Modifier
.padding(horizontal = horizontalPadding)
.navigationBarsPadding()
- // Add a small bottom padding to ensure it doesn't touch the edge
.padding(bottom = 8.dp)
}
) {
- if (showBiometricButton) {
- FilledTonalIconButton(
- onClick = onBiometricAuth,
- modifier = Modifier.size(44.dp),
- shape = RoundedCornerShape(40),
- ) {
- Icon(
- imageVector = Fingerprint,
- modifier = Modifier
- .fillMaxSize()
- .padding(10.dp),
- contentDescription = stringResource(R.string.biometric_authentication_cd),
- tint = MaterialTheme.colorScheme.surfaceTint
- )
- }
- }
KeypadRow(
disableHaptics = disableHaptics,
keys = listOf("1", "2", "3"),
@@ -818,6 +1051,26 @@ fun KeypadSection(
buttonSize = buttonSize,
buttonSpacing = buttonSpacing
)
+
+ if (showBiometricButton) {
+ FilledTonalIconButton(
+ onClick = onBiometricAuth,
+ modifier = Modifier.size(buttonSize),
+ shape = CircleShape,
+ colors = IconButtonDefaults.filledTonalIconButtonColors(
+ containerColor = MaterialTheme.colorScheme.secondaryContainer
+ )
+ ) {
+ Icon(
+ imageVector = Fingerprint,
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(buttonSize * 0.25f),
+ contentDescription = stringResource(R.string.biometric_authentication_cd),
+ tint = MaterialTheme.colorScheme.onSecondaryContainer
+ )
+ }
+ }
}
}
@@ -836,7 +1089,7 @@ private fun handleKeypadSpecialButtonLogic(
minLength: Int,
fromMainActivity: Boolean,
onAuthSuccess: () -> Unit,
- onPinAttempt: ((pin: String) -> Boolean)?,
+ onPinAttempt: ((pin: String, isFinal: Boolean) -> Boolean)?,
context: Context,
onPasswordChange: () -> Unit,
onPinIncorrect: () -> Unit
@@ -862,9 +1115,12 @@ private fun handleKeypadSpecialButtonLogic(
}
if (passwordState.value.length >= minLength) {
if (fromMainActivity) {
- if (appLockRepository.validatePassword(passwordState.value)) {
+ val correctPassword = appLockRepository.getPassword() ?: ""
+ if (passwordState.value == correctPassword) {
+ IntruderSelfieManager.resetFailedAttempts()
onAuthSuccess()
} else {
+ IntruderSelfieManager.recordFailedAttempt(context)
passwordState.value = ""
if (!appLockRepository.shouldDisableHaptics()) {
vibrate(context, 100)
@@ -873,7 +1129,7 @@ private fun handleKeypadSpecialButtonLogic(
}
} else {
onPinAttempt?.let { attempt ->
- val pinWasCorrectAndProcessed = attempt(passwordState.value)
+ val pinWasCorrectAndProcessed = attempt(passwordState.value, true)
if (!pinWasCorrectAndProcessed) {
passwordState.value = ""
if (!appLockRepository.shouldDisableHaptics()) {
@@ -918,7 +1174,7 @@ fun KeypadRow(
val targetColor = if (isPressed) {
MaterialTheme.colorScheme.primaryContainer
} else {
- if (icons.isNotEmpty() && index < icons.size && icons[index] != null) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surfaceBright
+ MaterialTheme.colorScheme.secondaryContainer
}
val animatedContainerColor by animateColorAsState(
@@ -953,7 +1209,7 @@ fun KeypadRow(
),
elevation = ButtonDefaults.filledTonalButtonElevation()
) {
- val contentColor = MaterialTheme.colorScheme.onPrimaryContainer
+ val contentColor = if (isPressed) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSecondaryContainer
if (icons.isNotEmpty() && index < icons.size && icons[index] != null) {
Icon(
@@ -965,6 +1221,7 @@ fun KeypadRow(
} else {
Text(
text = key,
+ color = contentColor,
style = MaterialTheme.typography.headlineLargeEmphasized.copy(
fontSize = animatedFontSize.sp
),
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..d4c337c 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
@@ -15,13 +16,16 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -40,6 +44,7 @@ import com.mrhwsn.composelock.Dot
import com.mrhwsn.composelock.LockCallback
import com.mrhwsn.composelock.PatternLock
import dev.pranav.applock.R
+import dev.pranav.applock.core.utils.IntruderSelfieManager
import dev.pranav.applock.core.utils.appLockRepository
import dev.pranav.applock.core.utils.vibrate
import dev.pranav.applock.ui.icons.Fingerprint
@@ -50,9 +55,11 @@ 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
+ onBiometricAuth: (() -> Unit)? = null,
+ onForgotPasscodeReset: (() -> Unit)? = null
) {
val appLockRepository = LocalContext.current.appLockRepository()
val context = LocalContext.current
@@ -64,7 +71,7 @@ fun PatternLockScreen(
Surface(
modifier = modifier.fillMaxSize(),
- color = MaterialTheme.colorScheme.surface
+ color = MaterialTheme.colorScheme.background
) {
var showError by remember { mutableStateOf(false) }
@@ -101,6 +108,9 @@ fun PatternLockScreen(
if (!isValid) {
showError = true
errorShakeOffset = 10f
+ // recordFailedAttempt is already handled in PasswordOverlayActivity's onPatternAttemptCallback
+ } else {
+ IntruderSelfieManager.resetFailedAttempts()
}
}
}
@@ -119,55 +129,44 @@ 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
- )
- }
}
+ TextButton(onClick = { onForgotPasscodeReset?.invoke() }) { Text("Forgot passcode?") }
+
if (appLockRepository.isBiometricAuthEnabled() && onBiometricAuth != null) {
+ Spacer(modifier = Modifier.height(24.dp))
FilledTonalIconButton(
onClick = { onBiometricAuth() },
- modifier = Modifier.size(44.dp),
- shape = RoundedCornerShape(40),
+ modifier = Modifier.size(64.dp),
+ shape = CircleShape,
+ colors = IconButtonDefaults.filledTonalIconButtonColors(
+ containerColor = MaterialTheme.colorScheme.secondaryContainer
+ )
) {
Icon(
imageVector = Fingerprint,
modifier = Modifier
.fillMaxSize()
- .padding(10.dp),
+ .padding(16.dp),
contentDescription = stringResource(R.string.biometric_authentication_cd),
- tint = MaterialTheme.colorScheme.surfaceTint
+ tint = MaterialTheme.colorScheme.onSecondaryContainer
)
}
}
@@ -206,26 +205,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(
@@ -238,27 +224,10 @@ fun PatternLockScreen(
}
Column(
- horizontalAlignment = Alignment.CenterHorizontally
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ modifier = Modifier.weight(1f)
) {
- if (appLockRepository.isBiometricAuthEnabled() && onBiometricAuth != null) {
- FilledTonalIconButton(
- onClick = { onBiometricAuth() },
- modifier = Modifier.size(44.dp),
- shape = RoundedCornerShape(40),
- ) {
- Icon(
- imageVector = Fingerprint,
- modifier = Modifier
- .fillMaxSize()
- .padding(10.dp),
- contentDescription = stringResource(R.string.biometric_authentication_cd),
- tint = MaterialTheme.colorScheme.surfaceTint
- )
- }
-
- Spacer(modifier = Modifier.height(16.dp))
- }
-
PatternLock(
modifier = Modifier
.fillMaxWidth()
@@ -273,6 +242,30 @@ fun PatternLockScreen(
animationDuration = 120,
callback = lockCallback
)
+
+ TextButton(onClick = { onForgotPasscodeReset?.invoke() }) { Text("Forgot passcode?") }
+
+ if (appLockRepository.isBiometricAuthEnabled() && onBiometricAuth != null) {
+ Spacer(modifier = Modifier.height(32.dp))
+
+ FilledTonalIconButton(
+ onClick = { onBiometricAuth() },
+ modifier = Modifier.size(72.dp),
+ shape = CircleShape,
+ colors = IconButtonDefaults.filledTonalIconButtonColors(
+ containerColor = MaterialTheme.colorScheme.secondaryContainer
+ )
+ ) {
+ Icon(
+ imageVector = Fingerprint,
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(18.dp),
+ contentDescription = stringResource(R.string.biometric_authentication_cd),
+ tint = MaterialTheme.colorScheme.onSecondaryContainer
+ )
+ }
+ }
}
}
}
diff --git a/app/src/main/java/dev/pranav/applock/features/setpassword/ui/PatternSetPasswordScreen.kt b/app/src/main/java/dev/pranav/applock/features/setpassword/ui/PatternSetPasswordScreen.kt
index c14d1a9..ff19fdc 100644
--- a/app/src/main/java/dev/pranav/applock/features/setpassword/ui/PatternSetPasswordScreen.kt
+++ b/app/src/main/java/dev/pranav/applock/features/setpassword/ui/PatternSetPasswordScreen.kt
@@ -44,6 +44,7 @@ import com.mrhwsn.composelock.PatternLock
import dev.pranav.applock.AppLockApplication
import dev.pranav.applock.R
import dev.pranav.applock.core.navigation.Screen
+import dev.pranav.applock.core.utils.RecoveryKeyManager
import dev.pranav.applock.core.utils.vibrate
import dev.pranav.applock.data.repository.PreferencesRepository
@@ -56,11 +57,11 @@ fun PatternSetPasswordScreen(
var patternState by remember { mutableStateOf("") }
var confirmPatternState by remember { mutableStateOf("") }
var isConfirmationMode by remember { mutableStateOf(false) }
- var isVerifyOldPasswordMode by remember { mutableStateOf(!isFirstTimeSetup) }
var showMismatchError by remember { mutableStateOf(false) }
var showMinLengthError by remember { mutableStateOf(false) }
- var showInvalidOldPasswordError by remember { mutableStateOf(false) }
+ var showRecoveryDialog by remember { mutableStateOf(false) }
+ var generatedRecoveryKey by remember { mutableStateOf("") }
val minLength = 4
val context = LocalContext.current
@@ -74,23 +75,6 @@ fun PatternSetPasswordScreen(
val screenHeight = windowInfo.containerSize.height
val isLandscape = screenWidth > screenHeight
- BackHandler {
- if (isFirstTimeSetup) {
- if (isConfirmationMode) {
- isConfirmationMode = false
- } else {
- Toast.makeText(context, R.string.set_pin_to_continue_toast, Toast.LENGTH_SHORT)
- .show()
- }
- } else {
- if (navController.previousBackStackEntry != null) {
- navController.popBackStack()
- } else {
- activity?.finish()
- }
- }
- }
-
val fragmentActivity = LocalActivity.current as? androidx.fragment.app.FragmentActivity
fun launchDeviceCredentialAuth() {
@@ -108,15 +92,29 @@ fun PatternSetPasswordScreen(
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
- isVerifyOldPasswordMode = false
- patternState = ""
- confirmPatternState = ""
- showInvalidOldPasswordError = false
+ // In this context, we just allow them to set a new pattern
}
})
biometricPrompt.authenticate(promptInfo)
}
+ BackHandler {
+ if (isFirstTimeSetup) {
+ if (isConfirmationMode) {
+ isConfirmationMode = false
+ } else {
+ Toast.makeText(context, R.string.set_pin_to_continue_toast, Toast.LENGTH_SHORT)
+ .show()
+ }
+ } else {
+ if (navController.previousBackStackEntry != null) {
+ navController.popBackStack()
+ } else {
+ activity?.finish()
+ }
+ }
+ }
+
fun switchToPinMethod() {
navController.navigate(Screen.SetPassword.route) {
popUpTo(Screen.SetPasswordPattern.route) { inclusive = true }
@@ -125,7 +123,6 @@ fun PatternSetPasswordScreen(
fun submitPattern() {
val currentPattern = when {
- isVerifyOldPasswordMode -> patternState
isConfirmationMode -> confirmPatternState
else -> patternState
}
@@ -136,17 +133,6 @@ fun PatternSetPasswordScreen(
}
when {
- isVerifyOldPasswordMode -> {
- if (appLockRepository!!.validatePattern(patternState)) {
- isVerifyOldPasswordMode = false
- patternState = ""
- showInvalidOldPasswordError = false
- } else {
- showInvalidOldPasswordError = true
- patternState = ""
- }
- }
-
!isConfirmationMode -> {
isConfirmationMode = true
showMinLengthError = false
@@ -162,12 +148,9 @@ fun PatternSetPasswordScreen(
Toast.LENGTH_SHORT
).show()
- navController.navigate(Screen.Main.route) {
- popUpTo(Screen.SetPassword.route) { inclusive = true }
- if (isFirstTimeSetup) {
- popUpTo(Screen.AppIntro.route) { inclusive = true }
- }
- }
+ generatedRecoveryKey = RecoveryKeyManager.generateRecoveryKey()
+ appLockRepository?.setRecoveryKey(generatedRecoveryKey)
+ showRecoveryDialog = true
} else {
showMismatchError = true
confirmPatternState = ""
@@ -176,6 +159,21 @@ fun PatternSetPasswordScreen(
}
}
+ if (showRecoveryDialog) {
+ RecoveryKeyGeneratedDialog(
+ recoveryKey = generatedRecoveryKey,
+ onDismiss = {
+ showRecoveryDialog = false
+ navController.navigate(Screen.Main.route) {
+ popUpTo(Screen.SetPasswordPattern.route) { inclusive = true }
+ if (isFirstTimeSetup) {
+ popUpTo(Screen.AppIntro.route) { inclusive = true }
+ }
+ }
+ }
+ )
+ }
+
Scaffold(
containerColor = MaterialTheme.colorScheme.surfaceContainer,
topBar = {
@@ -184,7 +182,6 @@ fun PatternSetPasswordScreen(
title = {
Text(
text = when {
- isVerifyOldPasswordMode -> stringResource(R.string.enter_current_pin_title)
isConfirmationMode -> stringResource(R.string.confirm_pin_title)
else -> stringResource(R.string.set_new_pin_title)
},
@@ -215,7 +212,6 @@ fun PatternSetPasswordScreen(
) {
Text(
text = when {
- isVerifyOldPasswordMode -> "Enter current Pattern"
isConfirmationMode -> "Confirm pattern"
else -> "Create a pattern"
},
@@ -241,14 +237,6 @@ fun PatternSetPasswordScreen(
textAlign = TextAlign.Center
)
}
- if (showInvalidOldPasswordError) {
- Text(
- text = "Incorrect pattern",
- color = MaterialTheme.colorScheme.error,
- style = MaterialTheme.typography.bodySmall,
- textAlign = TextAlign.Center
- )
- }
Spacer(modifier = Modifier.height(12.dp))
@@ -259,46 +247,38 @@ fun PatternSetPasswordScreen(
textAlign = TextAlign.Center
)
- if (isVerifyOldPasswordMode) {
+ if (!isFirstTimeSetup) {
Spacer(modifier = Modifier.height(12.dp))
TextButton(onClick = { launchDeviceCredentialAuth() }) {
Text(stringResource(R.string.reset_using_device_password_button))
}
}
- if (isFirstTimeSetup && !isVerifyOldPasswordMode && !isConfirmationMode) {
+ if (isFirstTimeSetup && !isConfirmationMode) {
TextButton(onClick = { switchToPinMethod() }) {
- Text("Use PIN Instead")
+ Text("Use PIN")
+ }
+ TextButton(onClick = {
+ navController.navigate(Screen.SetPasswordText.route) {
+ popUpTo(Screen.SetPasswordPattern.route) { inclusive = true }
+ }
+ }) {
+ Text("Use Password")
}
}
- if (isVerifyOldPasswordMode || isConfirmationMode) {
+ if (isConfirmationMode) {
Spacer(modifier = Modifier.height(8.dp))
TextButton(
onClick = {
- if (isVerifyOldPasswordMode) {
- if (navController.previousBackStackEntry != null) {
- navController.popBackStack()
- } else {
- activity?.finish()
- }
- } else {
- isConfirmationMode = false
- if (!isFirstTimeSetup) {
- isVerifyOldPasswordMode = true
- }
- }
+ isConfirmationMode = false
patternState = ""
confirmPatternState = ""
showMismatchError = false
showMinLengthError = false
- showInvalidOldPasswordError = false
}
) {
- Text(
- if (isVerifyOldPasswordMode) stringResource(R.string.cancel_button)
- else stringResource(R.string.start_over_button)
- )
+ Text(stringResource(R.string.start_over_button))
}
}
}
@@ -329,9 +309,7 @@ fun PatternSetPasswordScreen(
override fun onResult(result: List) {
val patternString = result.joinToString("") { it.id.toString() }
- if (isVerifyOldPasswordMode) {
- patternState = patternString
- } else if (isConfirmationMode) {
+ if (isConfirmationMode) {
confirmPatternState = patternString
} else {
patternState = patternString
@@ -355,7 +333,6 @@ fun PatternSetPasswordScreen(
) {
Text(
text = when {
- isVerifyOldPasswordMode -> stringResource(R.string.enter_current_pin_label)
isConfirmationMode -> stringResource(R.string.confirm_new_pin_label)
else -> stringResource(R.string.create_new_pin_label)
},
@@ -383,16 +360,6 @@ fun PatternSetPasswordScreen(
)
}
- if (showInvalidOldPasswordError) {
- Spacer(modifier = Modifier.height(8.dp))
- Text(
- text = "Incorrect pattern",
- color = MaterialTheme.colorScheme.error,
- style = MaterialTheme.typography.labelLarge,
- textAlign = TextAlign.Center
- )
- }
-
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.enter_pattern_to_continue),
@@ -433,9 +400,7 @@ fun PatternSetPasswordScreen(
override fun onResult(result: List) {
val patternString = result.joinToString("") { it.id.toString() }
- if (isVerifyOldPasswordMode) {
- patternState = patternString
- } else if (isConfirmationMode) {
+ if (isConfirmationMode) {
confirmPatternState = patternString
} else {
patternState = patternString
@@ -447,44 +412,34 @@ fun PatternSetPasswordScreen(
Spacer(modifier = Modifier.height(16.dp))
- if (!isVerifyOldPasswordMode && !isConfirmationMode) {
+ if (!isConfirmationMode) {
TextButton(onClick = { switchToPinMethod() }) {
- Text("Use PIN Instead")
+ Text("Use PIN")
+ }
+ TextButton(onClick = {
+ navController.navigate(Screen.SetPasswordText.route)
+ }) {
+ Text("Use Password")
}
}
- if (isVerifyOldPasswordMode) {
+ if (!isFirstTimeSetup) {
TextButton(onClick = { launchDeviceCredentialAuth() }) {
Text(stringResource(R.string.reset_using_device_password_button))
}
}
- if (isVerifyOldPasswordMode || isConfirmationMode) {
+ if (isConfirmationMode) {
TextButton(
onClick = {
- if (isVerifyOldPasswordMode) {
- if (navController.previousBackStackEntry != null) {
- navController.popBackStack()
- } else {
- activity?.finish()
- }
- } else {
- isConfirmationMode = false
- if (!isFirstTimeSetup) {
- isVerifyOldPasswordMode = true
- }
- }
+ isConfirmationMode = false
patternState = ""
confirmPatternState = ""
showMismatchError = false
showMinLengthError = false
- showInvalidOldPasswordError = false
}
) {
- Text(
- if (isVerifyOldPasswordMode) stringResource(R.string.cancel_button)
- else stringResource(R.string.start_over_button)
- )
+ Text(stringResource(R.string.start_over_button))
}
}
}
diff --git a/app/src/main/java/dev/pranav/applock/features/setpassword/ui/RecoveryKeyComponents.kt b/app/src/main/java/dev/pranav/applock/features/setpassword/ui/RecoveryKeyComponents.kt
new file mode 100644
index 0000000..c4d385c
--- /dev/null
+++ b/app/src/main/java/dev/pranav/applock/features/setpassword/ui/RecoveryKeyComponents.kt
@@ -0,0 +1,115 @@
+package dev.pranav.applock.features.setpassword.ui
+
+import android.widget.Toast
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalClipboardManager
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.unit.dp
+import dev.pranav.applock.core.utils.RecoveryKeyManager
+import dev.pranav.applock.core.utils.appLockRepository
+
+@Composable
+fun RecoveryKeyDialog(
+ onDismiss: () -> Unit,
+ onValidated: () -> Unit
+) {
+ val context = LocalContext.current
+ val repo = context.appLockRepository()
+ val recoveryKey = remember { mutableStateOf("") }
+ val error = remember { mutableStateOf(null) }
+
+ AlertDialog(
+ onDismissRequest = onDismiss,
+ title = { Text("Forgot passcode") },
+ text = {
+ Column {
+ Text("Enter your recovery key to reset your current lock.")
+ Spacer(Modifier.height(12.dp))
+ OutlinedTextField(
+ value = recoveryKey.value,
+ onValueChange = {
+ recoveryKey.value = it
+ error.value = null
+ },
+ modifier = Modifier.fillMaxWidth(),
+ singleLine = true,
+ label = { Text("Recovery key") },
+ supportingText = {
+ error.value?.let { Text(it, color = MaterialTheme.colorScheme.error) }
+ }
+ )
+ }
+ },
+ confirmButton = {
+ TextButton(onClick = {
+ if (repo.validateRecoveryKey(recoveryKey.value.trim())) {
+ onValidated()
+ onDismiss()
+ } else {
+ error.value = "Invalid recovery key"
+ }
+ }) { Text("Reset lock") }
+ },
+ dismissButton = {
+ TextButton(onClick = onDismiss) { Text("Cancel") }
+ }
+ )
+}
+
+@Composable
+fun RecoveryKeyGeneratedDialog(
+ recoveryKey: String,
+ onDismiss: () -> Unit
+) {
+ val context = LocalContext.current
+ val clipboardManager = LocalClipboardManager.current
+
+ AlertDialog(
+ onDismissRequest = {},
+ title = { Text("Save your recovery key") },
+ text = {
+ Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
+ Text("This key is shown only once. Save it now to reset your lock later if you forget your passcode.")
+ Text(
+ text = recoveryKey,
+ style = MaterialTheme.typography.bodyMedium,
+ modifier = Modifier.padding(vertical = 4.dp)
+ )
+ }
+ },
+ confirmButton = {
+ TextButton(onClick = {
+ val result = RecoveryKeyManager.saveRecoveryKeyToDownloads(context, recoveryKey)
+ result.onSuccess {
+ Toast.makeText(context, "Recovery key saved to $it", Toast.LENGTH_LONG).show()
+ }.onFailure {
+ Toast.makeText(context, it.message ?: "Failed to save recovery key", Toast.LENGTH_LONG).show()
+ }
+ }) { Text("Save as TXT") }
+ },
+ dismissButton = {
+ Column {
+ TextButton(onClick = {
+ clipboardManager.setText(AnnotatedString(recoveryKey))
+ Toast.makeText(context, "Recovery key copied", Toast.LENGTH_SHORT).show()
+ }) { Text("Copy") }
+ TextButton(onClick = onDismiss) { Text("Continue") }
+ }
+ }
+ )
+}
diff --git a/app/src/main/java/dev/pranav/applock/features/setpassword/ui/SetPasswordScreen.kt b/app/src/main/java/dev/pranav/applock/features/setpassword/ui/SetPasswordScreen.kt
index 85fb9c5..74aec19 100644
--- a/app/src/main/java/dev/pranav/applock/features/setpassword/ui/SetPasswordScreen.kt
+++ b/app/src/main/java/dev/pranav/applock/features/setpassword/ui/SetPasswordScreen.kt
@@ -43,6 +43,7 @@ import androidx.navigation.NavController
import dev.pranav.applock.AppLockApplication
import dev.pranav.applock.R
import dev.pranav.applock.core.navigation.Screen
+import dev.pranav.applock.core.utils.RecoveryKeyManager
import dev.pranav.applock.data.repository.PreferencesRepository
import dev.pranav.applock.features.lockscreen.ui.KeypadRow
import dev.pranav.applock.features.lockscreen.ui.PasswordIndicators
@@ -61,11 +62,10 @@ fun SetPasswordScreen(
var confirmPasswordState by remember { mutableStateOf("") }
var isConfirmationMode by remember { mutableStateOf(false) }
- var isVerifyOldPasswordMode by remember { mutableStateOf(!isFirstTimeSetup) }
-
var showMismatchError by remember { mutableStateOf(false) }
var showLengthError by remember { mutableStateOf(false) }
- var showInvalidOldPasswordError by remember { mutableStateOf(false) }
+ var showRecoveryDialog by remember { mutableStateOf(false) }
+ var generatedRecoveryKey by remember { mutableStateOf("") }
val minLength = 4
val context = LocalContext.current
@@ -131,31 +131,19 @@ fun SetPasswordScreen(
}
}
- val fragmentActivity = LocalActivity.current as? androidx.fragment.app.FragmentActivity
-
- fun launchDeviceCredentialAuth() {
- if (fragmentActivity == null) return
- val executor = ContextCompat.getMainExecutor(context)
- val promptInfo = BiometricPrompt.PromptInfo.Builder()
- .setTitle(context.getString(R.string.authenticate_to_reset_pin_title))
- .setSubtitle(context.getString(R.string.use_device_pin_pattern_password_subtitle))
- .setAllowedAuthenticators(
- BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL
- )
- .build()
- val biometricPrompt = BiometricPrompt(
- fragmentActivity, executor,
- object : BiometricPrompt.AuthenticationCallback() {
- override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
- super.onAuthenticationSucceeded(result)
- isVerifyOldPasswordMode = false
- passwordState = ""
- confirmPasswordState = ""
- showInvalidOldPasswordError = false
+ if (showRecoveryDialog) {
+ RecoveryKeyGeneratedDialog(
+ recoveryKey = generatedRecoveryKey,
+ onDismiss = {
+ showRecoveryDialog = false
+ navController.navigate(Screen.Main.route) {
+ popUpTo(Screen.SetPassword.route) { inclusive = true }
+ if (isFirstTimeSetup) {
+ popUpTo(Screen.AppIntro.route) { inclusive = true }
+ }
}
-
- })
- biometricPrompt.authenticate(promptInfo)
+ }
+ )
}
Scaffold(
@@ -166,7 +154,6 @@ fun SetPasswordScreen(
title = {
Text(
text = when {
- isVerifyOldPasswordMode -> stringResource(R.string.enter_current_pin_title)
isConfirmationMode -> stringResource(R.string.confirm_pin_title)
else -> stringResource(R.string.set_new_pin_title)
},
@@ -199,7 +186,6 @@ fun SetPasswordScreen(
) {
Text(
text = when {
- isVerifyOldPasswordMode -> stringResource(R.string.enter_current_pin_label)
isConfirmationMode -> stringResource(R.string.confirm_new_pin_label)
else -> stringResource(R.string.create_new_pin_label)
},
@@ -225,17 +211,8 @@ fun SetPasswordScreen(
modifier = Modifier.padding(8.dp)
)
}
- if (showInvalidOldPasswordError) {
- Text(
- text = stringResource(R.string.incorrect_pin_try_again),
- color = MaterialTheme.colorScheme.error,
- style = MaterialTheme.typography.bodyMedium,
- modifier = Modifier.padding(8.dp)
- )
- }
val currentPassword = when {
- isVerifyOldPasswordMode -> passwordState
isConfirmationMode -> confirmPasswordState
else -> passwordState
}
@@ -248,7 +225,6 @@ fun SetPasswordScreen(
Text(
text = when {
- isVerifyOldPasswordMode -> stringResource(R.string.enter_current_pin_label)
isConfirmationMode -> stringResource(R.string.re_enter_new_pin_confirm_label)
else -> stringResource(R.string.tooltip_create_pin_min_length)
},
@@ -257,41 +233,18 @@ fun SetPasswordScreen(
textAlign = TextAlign.Center
)
- if (isVerifyOldPasswordMode) {
- Spacer(modifier = Modifier.height(16.dp))
- TextButton(onClick = { launchDeviceCredentialAuth() }) {
- Text(stringResource(R.string.reset_using_device_password_button))
- }
- }
-
- if (isVerifyOldPasswordMode || isConfirmationMode) {
+ if (isConfirmationMode) {
Spacer(modifier = Modifier.height(8.dp))
TextButton(
onClick = {
- if (isVerifyOldPasswordMode) {
- if (navController.previousBackStackEntry != null) {
- navController.popBackStack()
- } else {
- activity?.finish()
- }
- } else {
- isConfirmationMode = false
- if (!isFirstTimeSetup) {
- isVerifyOldPasswordMode = true
- }
- }
+ isConfirmationMode = false
passwordState = ""
confirmPasswordState = ""
showMismatchError = false
showLengthError = false
- showInvalidOldPasswordError = false
}
) {
- Text(
- if (isVerifyOldPasswordMode) stringResource(R.string.cancel_button) else stringResource(
- R.string.start_over_button
- )
- )
+ Text(stringResource(R.string.start_over_button))
}
}
}
@@ -303,12 +256,10 @@ fun SetPasswordScreen(
) {
val onKeyClick: (String) -> Unit = { key ->
val currentActivePassword = when {
- isVerifyOldPasswordMode -> passwordState
isConfirmationMode -> confirmPasswordState
else -> passwordState
}
val updatePassword: (String) -> Unit = when {
- isVerifyOldPasswordMode -> { newPass -> passwordState = newPass }
isConfirmationMode -> { newPass -> confirmPasswordState = newPass }
else -> { newPass -> passwordState = newPass }
}
@@ -324,23 +275,11 @@ fun SetPasswordScreen(
}
showMismatchError = false
showLengthError = false
- showInvalidOldPasswordError = false
}
"proceed" -> {
if (currentActivePassword.length >= minLength) {
when {
- isVerifyOldPasswordMode -> {
- if (appLockRepository!!.validatePassword(passwordState)) {
- isVerifyOldPasswordMode = false
- passwordState = ""
- showInvalidOldPasswordError = false
- } else {
- showInvalidOldPasswordError = true
- passwordState = ""
- }
- }
-
!isConfirmationMode -> {
isConfirmationMode = true
showLengthError = false
@@ -349,22 +288,15 @@ fun SetPasswordScreen(
else -> {
if (passwordState == confirmPasswordState) {
appLockRepository?.setPassword(passwordState)
+ appLockRepository?.setLockType(PreferencesRepository.LOCK_TYPE_PIN)
+ generatedRecoveryKey = RecoveryKeyManager.generateRecoveryKey()
+ appLockRepository?.setRecoveryKey(generatedRecoveryKey)
Toast.makeText(
context,
context.getString(R.string.password_set_successfully_toast),
Toast.LENGTH_SHORT
).show()
-
- navController.navigate(Screen.Main.route) {
- popUpTo(Screen.SetPassword.route) {
- inclusive = true
- }
- if (isFirstTimeSetup) {
- popUpTo(Screen.AppIntro.route) {
- inclusive = true
- }
- }
- }
+ showRecoveryDialog = true
} else {
showMismatchError = true
confirmPasswordState = ""
@@ -408,7 +340,7 @@ fun SetPasswordScreen(
icons = listOf(
Backspace,
null,
- if (isConfirmationMode || isVerifyOldPasswordMode) Icons.Default.Check else Icons.AutoMirrored.Rounded.KeyboardArrowRight
+ if (isConfirmationMode) Icons.Default.Check else Icons.AutoMirrored.Rounded.KeyboardArrowRight
),
onKeyClick = onKeyClick,
buttonSize = buttonSize,
@@ -433,7 +365,6 @@ fun SetPasswordScreen(
) {
Text(
text = when {
- isVerifyOldPasswordMode -> stringResource(R.string.enter_current_pin_label)
isConfirmationMode -> stringResource(R.string.confirm_new_pin_label)
else -> stringResource(R.string.create_new_pin_label)
},
@@ -458,17 +389,8 @@ fun SetPasswordScreen(
modifier = Modifier.padding(8.dp)
)
}
- if (showInvalidOldPasswordError) {
- Text(
- text = stringResource(R.string.incorrect_pin_try_again),
- color = MaterialTheme.colorScheme.error,
- style = MaterialTheme.typography.bodyMedium,
- modifier = Modifier.padding(8.dp)
- )
- }
val currentPassword = when {
- isVerifyOldPasswordMode -> passwordState
isConfirmationMode -> confirmPasswordState
else -> passwordState
}
@@ -479,7 +401,6 @@ fun SetPasswordScreen(
Text(
text = when {
- isVerifyOldPasswordMode -> stringResource(R.string.enter_current_pin_label)
isConfirmationMode -> stringResource(R.string.re_enter_new_pin_confirm_label)
else -> stringResource(R.string.tooltip_create_pin_min_length)
},
@@ -490,40 +411,18 @@ fun SetPasswordScreen(
Spacer(modifier = Modifier.weight(1f))
- if (isVerifyOldPasswordMode) {
- TextButton(onClick = { launchDeviceCredentialAuth() }) {
- Text(stringResource(R.string.reset_using_device_password_button))
- }
- }
-
- if (isVerifyOldPasswordMode || isConfirmationMode) {
+ if (isConfirmationMode) {
TextButton(
onClick = {
- if (isVerifyOldPasswordMode) {
- if (navController.previousBackStackEntry != null) {
- navController.popBackStack()
- } else {
- activity?.finish()
- }
- } else {
- isConfirmationMode = false
- if (!isFirstTimeSetup) {
- isVerifyOldPasswordMode = true
- }
- }
+ isConfirmationMode = false
passwordState = ""
confirmPasswordState = ""
showMismatchError = false
showLengthError = false
- showInvalidOldPasswordError = false
},
modifier = Modifier.padding(bottom = 16.dp)
) {
- Text(
- if (isVerifyOldPasswordMode) stringResource(R.string.cancel_button) else stringResource(
- R.string.start_over_button
- )
- )
+ Text(stringResource(R.string.start_over_button))
}
}
@@ -533,9 +432,15 @@ fun SetPasswordScreen(
},
modifier = Modifier.padding(bottom = 16.dp)
) {
- Text(
- stringResource(R.string.use_pattern_button)
- )
+ Text(stringResource(R.string.use_pattern_button))
+ }
+ TextButton(
+ onClick = {
+ navController.navigate(Screen.SetPasswordText.route)
+ },
+ modifier = Modifier.padding(bottom = 16.dp)
+ ) {
+ Text("Use Password")
}
Column(
@@ -545,12 +450,10 @@ fun SetPasswordScreen(
) {
val onKeyClick: (String) -> Unit = { key ->
val currentActivePassword = when {
- isVerifyOldPasswordMode -> passwordState
isConfirmationMode -> confirmPasswordState
else -> passwordState
}
val updatePassword: (String) -> Unit = when {
- isVerifyOldPasswordMode -> { newPass -> passwordState = newPass }
isConfirmationMode -> { newPass -> confirmPasswordState = newPass }
else -> { newPass -> passwordState = newPass }
}
@@ -566,23 +469,11 @@ fun SetPasswordScreen(
}
showMismatchError = false
showLengthError = false
- showInvalidOldPasswordError = false
}
"proceed" -> {
if (currentActivePassword.length >= minLength) {
when {
- isVerifyOldPasswordMode -> {
- if (appLockRepository!!.validatePassword(passwordState)) {
- isVerifyOldPasswordMode = false
- passwordState = ""
- showInvalidOldPasswordError = false
- } else {
- showInvalidOldPasswordError = true
- passwordState = ""
- }
- }
-
!isConfirmationMode -> {
isConfirmationMode = true
showLengthError = false
@@ -592,22 +483,14 @@ fun SetPasswordScreen(
if (passwordState == confirmPasswordState) {
appLockRepository?.setLockType(PreferencesRepository.LOCK_TYPE_PIN)
appLockRepository?.setPassword(passwordState)
+ generatedRecoveryKey = RecoveryKeyManager.generateRecoveryKey()
+ appLockRepository?.setRecoveryKey(generatedRecoveryKey)
Toast.makeText(
context,
context.getString(R.string.password_set_successfully_toast),
Toast.LENGTH_SHORT
).show()
-
- navController.navigate(Screen.Main.route) {
- popUpTo(Screen.SetPassword.route) {
- inclusive = true
- }
- if (isFirstTimeSetup) {
- popUpTo(Screen.AppIntro.route) {
- inclusive = true
- }
- }
- }
+ showRecoveryDialog = true
} else {
showMismatchError = true
confirmPasswordState = ""
@@ -651,7 +534,7 @@ fun SetPasswordScreen(
icons = listOf(
Backspace,
null,
- if (isConfirmationMode || isVerifyOldPasswordMode) Icons.Default.Check else Icons.AutoMirrored.Rounded.KeyboardArrowRight
+ if (isConfirmationMode) Icons.Default.Check else Icons.AutoMirrored.Rounded.KeyboardArrowRight
),
onKeyClick = onKeyClick,
buttonSize = buttonSize,
diff --git a/app/src/main/java/dev/pranav/applock/features/setpassword/ui/SetPasswordTextScreen.kt b/app/src/main/java/dev/pranav/applock/features/setpassword/ui/SetPasswordTextScreen.kt
new file mode 100644
index 0000000..83be728
--- /dev/null
+++ b/app/src/main/java/dev/pranav/applock/features/setpassword/ui/SetPasswordTextScreen.kt
@@ -0,0 +1,170 @@
+@file:OptIn(ExperimentalMaterial3Api::class)
+
+package dev.pranav.applock.features.setpassword.ui
+
+import android.widget.Toast
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.BackHandler
+import androidx.activity.compose.LocalActivity
+import androidx.biometric.BiometricManager
+import androidx.biometric.BiometricPrompt
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Button
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.unit.dp
+import androidx.core.content.ContextCompat
+import androidx.navigation.NavController
+import dev.pranav.applock.AppLockApplication
+import dev.pranav.applock.R
+import dev.pranav.applock.core.navigation.Screen
+import dev.pranav.applock.core.utils.RecoveryKeyManager
+import dev.pranav.applock.data.repository.PreferencesRepository
+
+private fun isStrongPassword(password: String): Boolean {
+ return password.length >= 8 &&
+ password.any { it.isUpperCase() } &&
+ password.any { it.isLowerCase() } &&
+ password.any { it.isDigit() } &&
+ password.any { !it.isLetterOrDigit() }
+}
+
+@Composable
+fun SetPasswordTextScreen(navController: NavController, isFirstTimeSetup: Boolean) {
+ val context = LocalContext.current
+ val activity = LocalActivity.current as? ComponentActivity
+ val repo = remember { (context.applicationContext as? AppLockApplication)?.appLockRepository }
+
+ var newPassword by remember { mutableStateOf("") }
+ var confirmPassword by remember { mutableStateOf("") }
+ var error by remember { mutableStateOf(null) }
+ var showRecoveryDialog by remember { mutableStateOf(false) }
+
+ val fragmentActivity = LocalActivity.current as? androidx.fragment.app.FragmentActivity
+
+ fun launchDeviceCredentialAuth() {
+ if (fragmentActivity == null) return
+ val executor = ContextCompat.getMainExecutor(context)
+ val promptInfo = BiometricPrompt.PromptInfo.Builder()
+ .setTitle(context.getString(R.string.authenticate_to_reset_pin_title))
+ .setSubtitle(context.getString(R.string.use_device_pin_pattern_password_subtitle))
+ .setAllowedAuthenticators(
+ BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL
+ )
+ .build()
+ val biometricPrompt = BiometricPrompt(
+ fragmentActivity, executor,
+ object : BiometricPrompt.AuthenticationCallback() {
+ override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
+ super.onAuthenticationSucceeded(result)
+ // Reset current password requirement or navigate directly
+ // In this context, we just allow them to set a new password without current one
+ }
+ })
+ biometricPrompt.authenticate(promptInfo)
+ }
+
+ BackHandler {
+ if (isFirstTimeSetup) {
+ Toast.makeText(context, R.string.set_pin_to_continue_toast, Toast.LENGTH_SHORT).show()
+ } else if (navController.previousBackStackEntry != null) {
+ navController.popBackStack()
+ } else {
+ activity?.finish()
+ }
+ }
+
+ if (showRecoveryDialog) {
+ RecoveryKeyGeneratedDialog(
+ recoveryKey = repo?.getRecoveryKey().orEmpty(),
+ onDismiss = {
+ showRecoveryDialog = false
+ navController.navigate(Screen.Main.route) {
+ popUpTo(Screen.SetPasswordText.route) { inclusive = true }
+ if (isFirstTimeSetup) {
+ popUpTo(Screen.AppIntro.route) { inclusive = true }
+ }
+ }
+ }
+ )
+ }
+
+ Scaffold(topBar = { TopAppBar(title = { Text(if (isFirstTimeSetup) "Set Password" else "Change Password") }) }) { padding ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(padding)
+ .padding(20.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Text("Password must contain at least 8 characters, 1 uppercase, 1 lowercase, 1 number and 1 symbol.")
+
+ OutlinedTextField(
+ value = newPassword,
+ onValueChange = { newPassword = it; error = null },
+ label = { Text("New password") },
+ visualTransformation = PasswordVisualTransformation(),
+ modifier = Modifier.fillMaxWidth(),
+ singleLine = true
+ )
+ OutlinedTextField(
+ value = confirmPassword,
+ onValueChange = { confirmPassword = it; error = null },
+ label = { Text("Confirm password") },
+ visualTransformation = PasswordVisualTransformation(),
+ modifier = Modifier.fillMaxWidth(),
+ singleLine = true
+ )
+ error?.let { Text(it, color = MaterialTheme.colorScheme.error) }
+ Spacer(Modifier.height(8.dp))
+ Button(onClick = {
+ when {
+ !isStrongPassword(newPassword) -> error = "Password does not meet complexity requirements"
+ newPassword != confirmPassword -> error = "Passwords do not match"
+ else -> {
+ repo?.setLockType(PreferencesRepository.LOCK_TYPE_PASSWORD)
+ repo?.setPassword(newPassword)
+ val recoveryKey = RecoveryKeyManager.generateRecoveryKey()
+ repo?.setRecoveryKey(recoveryKey)
+ showRecoveryDialog = true
+ }
+ }
+ }, modifier = Modifier.fillMaxWidth()) {
+ Text("Save password")
+ }
+
+ if (!isFirstTimeSetup) {
+ TextButton(onClick = { launchDeviceCredentialAuth() }, modifier = Modifier.fillMaxWidth()) {
+ Text(stringResource(R.string.reset_using_device_password_button))
+ }
+ }
+
+ TextButton(onClick = {
+ navController.navigate(if (isFirstTimeSetup) Screen.SetPassword.route else Screen.ChangePasswordPin.route)
+ }) { Text("Use PIN") }
+ TextButton(onClick = {
+ navController.navigate(if (isFirstTimeSetup) Screen.SetPasswordPattern.route else Screen.ChangePasswordPattern.route)
+ }) { Text("Use Pattern") }
+ }
+ }
+}
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..2c4220f 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
@@ -1,27 +1,35 @@
package dev.pranav.applock.features.settings.ui
+import android.Manifest
+import android.annotation.SuppressLint
import android.app.admin.DevicePolicyManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
+import android.os.Build
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.biometric.BiometricManager
+import androidx.biometric.BiometricPrompt
import androidx.compose.animation.*
import androidx.compose.animation.core.Spring
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.rememberScrollState
+import androidx.compose.foundation.selection.selectable
+import androidx.compose.foundation.selection.selectableGroup
+import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.BugReport
-import androidx.compose.material.icons.outlined.Code
import androidx.compose.material.icons.outlined.Security
import androidx.compose.material3.*
import androidx.compose.runtime.*
@@ -31,63 +39,109 @@ 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.content.ContextCompat
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
import dev.pranav.applock.core.navigation.Screen
+import dev.pranav.applock.core.utils.BiometricStatus
import dev.pranav.applock.core.utils.LogUtils
+import dev.pranav.applock.core.utils.getBiometricStatus
import dev.pranav.applock.core.utils.hasUsagePermission
import dev.pranav.applock.core.utils.isAccessibilityServiceEnabled
import dev.pranav.applock.core.utils.openAccessibilitySettings
import dev.pranav.applock.data.repository.AppLockRepository
+import dev.pranav.applock.data.repository.AppThemeMode
import dev.pranav.applock.data.repository.BackendImplementation
+import dev.pranav.applock.data.repository.PreferencesRepository
import dev.pranav.applock.features.admin.AdminDisableActivity
import dev.pranav.applock.services.ExperimentalAppLockService
import dev.pranav.applock.services.ShizukuAppLockService
-import dev.pranav.applock.ui.components.DonateButton
import dev.pranav.applock.ui.icons.*
import rikka.shizuku.Shizuku
import rikka.shizuku.ShizukuProvider
+import java.io.File
import kotlin.math.abs
+@SuppressLint("LocalContextGetResourceValueCall")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
navController: NavController
) {
val context = LocalContext.current
+ val lifecycleOwner = LocalLifecycleOwner.current
val appLockRepository = remember { AppLockRepository(context) }
- var showDialog by remember { mutableStateOf(false) }
+ // 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 showUnlockTimeDialog by remember { mutableStateOf(false) }
val shizukuPermissionLauncher = rememberLauncherForActivityResult(
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()
}
}
+ // Intruder Selfie states
+ var intruderSelfieEnabled by remember { mutableStateOf(appLockRepository.isIntruderSelfieEnabled()) }
+ var intruderSelfieAttempts by remember { mutableIntStateOf(appLockRepository.getIntruderSelfieAttempts()) }
+ var hasSelfies by remember { mutableStateOf(false) }
+
+ LaunchedEffect(Unit) {
+ val selfieDir = File(context.filesDir, "intruder_selfies")
+ hasSelfies = selfieDir.exists() && (selfieDir.listFiles()?.isNotEmpty() ?: false)
+ }
+
+ val cameraPermissionLauncher = rememberLauncherForActivityResult(
+ ActivityResultContracts.RequestPermission()
+ ) { isGranted ->
+ if (isGranted) {
+ intruderSelfieEnabled = true
+ appLockRepository.setIntruderSelfieEnabled(true)
+ } else {
+ Toast.makeText(context, "Camera permission is required for Intruder Selfie", Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ // Reactive states from repository flows
+ val amoledModeEnabled by appLockRepository.amoledModeFlow()
+ .collectAsState(initial = appLockRepository.isAmoledModeEnabled())
+ val dynamicColorEnabled by appLockRepository.dynamicColorFlow()
+ .collectAsState(initial = appLockRepository.isDynamicColorEnabled())
+ val appThemeMode by appLockRepository.appThemeModeFlow()
+ .collectAsState(initial = appLockRepository.getAppThemeMode())
+
var autoUnlock by remember { mutableStateOf(appLockRepository.isAutoUnlockEnabled()) }
var useMaxBrightness by remember { mutableStateOf(appLockRepository.shouldUseMaxBrightness()) }
var useBiometricAuth by remember { mutableStateOf(appLockRepository.isBiometricAuthEnabled()) }
- var unlockTimeDuration by remember { mutableIntStateOf(appLockRepository.getUnlockTimeDuration()) }
+ var unlockTimeDuration by remember { mutableStateOf(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 preventAllAppUninstall by remember { mutableStateOf(appLockRepository.isPreventAllAppUninstallEnabled()) }
+
var disableHapticFeedback by remember { mutableStateOf(appLockRepository.shouldDisableHaptics()) }
var loggingEnabled by remember { mutableStateOf(appLockRepository.isLoggingEnabled()) }
@@ -95,7 +149,74 @@ fun SettingsScreen(
var showDeviceAdminDialog by remember { mutableStateOf(false) }
var showAccessibilityDialog by remember { mutableStateOf(false) }
+ // Sync other states with repository on resume (non-flow states)
+ 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()
+ preventAllAppUninstall = appLockRepository.isPreventAllAppUninstallEnabled()
+ disableHapticFeedback = appLockRepository.shouldDisableHaptics()
+ loggingEnabled = appLockRepository.isLoggingEnabled()
+ intruderSelfieEnabled = appLockRepository.isIntruderSelfieEnabled()
+ intruderSelfieAttempts = appLockRepository.getIntruderSelfieAttempts()
+
+ val selfieDir = File(context.filesDir, "intruder_selfies")
+ hasSelfies = selfieDir.exists() && (selfieDir.listFiles()?.isNotEmpty() ?: false)
+ }
+ }
+ lifecycleOwner.lifecycle.addObserver(observer)
+ onDispose {
+ lifecycleOwner.lifecycle.removeObserver(observer)
+ }
+ }
+
val biometricManager = remember { BiometricManager.from(context) }
+ var isSettingsAuthorized by remember { mutableStateOf(false) }
+
+ LaunchedEffect(Unit) {
+ when (val biometricStatus = getBiometricStatus(context)) {
+ BiometricStatus.Available -> {
+ val activity = context as? androidx.fragment.app.FragmentActivity ?: return@LaunchedEffect
+ val executor = ContextCompat.getMainExecutor(context)
+ val prompt = BiometricPrompt(activity, executor, object : BiometricPrompt.AuthenticationCallback() {
+ override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
+ isSettingsAuthorized = true
+ }
+
+ override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
+ if (!isSettingsAuthorized) navController.popBackStack()
+ }
+ })
+ val promptInfo = BiometricPrompt.PromptInfo.Builder()
+ .setTitle("Settings")
+ .setSubtitle("Biometric authentication required")
+ .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
+ .setNegativeButtonText("Cancel")
+ .build()
+ prompt.authenticate(promptInfo)
+ }
+ is BiometricStatus.Unavailable -> {
+ Toast.makeText(context, biometricStatus.message, Toast.LENGTH_LONG).show()
+ navController.popBackStack()
+ }
+ }
+ }
+
+ if (!isSettingsAuthorized) {
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ Text("Authenticate with biometrics to open Settings")
+ }
+ return
+ }
+
val isBiometricAvailable = remember {
biometricManager.canAuthenticate(
BiometricManager.Authenticators.BIOMETRIC_WEAK or
@@ -103,33 +224,18 @@ fun SettingsScreen(
) == BiometricManager.BIOMETRIC_SUCCESS
}
- if (showDialog) {
- AlertDialog(
- onDismissRequest = { showDialog = false },
- title = { Text(stringResource(R.string.settings_screen_support_development_dialog_title)) },
- text = { Text(stringResource(R.string.support_development_text)) },
- confirmButton = {
- FilledTonalButton(
- onClick = {
- context.startActivity(
- Intent(
- Intent.ACTION_VIEW,
- "https://pranavpurwar.github.io/donate.html".toUri()
- )
- )
- showDialog = false
- }
- ) {
- Text(stringResource(R.string.settings_screen_support_development_donate_button))
- }
- },
- dismissButton = {
- TextButton(onClick = { showDialog = false }) {
- Text(stringResource(R.string.cancel_button))
- }
- },
- containerColor = MaterialTheme.colorScheme.surface
- )
+ // 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 (showUnlockTimeDialog) {
@@ -164,7 +270,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)
@@ -205,250 +311,395 @@ fun SettingsScreen(
)
}
},
- scrollBehavior = scrollBehavior,
- colors = TopAppBarDefaults.topAppBarColors(
- containerColor = MaterialTheme.colorScheme.surface,
- scrolledContainerColor = MaterialTheme.colorScheme.surface
- )
+ scrollBehavior = scrollBehavior
)
- },
- containerColor = MaterialTheme.colorScheme.surface
- ) { innerPadding ->
+ }
+ ) { padding ->
LazyColumn(
- modifier = Modifier.fillMaxSize(),
- contentPadding = PaddingValues(
- start = 16.dp,
- end = 16.dp,
- top = innerPadding.calculateTopPadding(),
- bottom = innerPadding.calculateBottomPadding() + 24.dp
- ),
- verticalArrangement = Arrangement.spacedBy(8.dp)
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(padding)
) {
item {
- val packageInfo = remember {
- try {
- context.packageManager.getPackageInfo(context.packageName, 0)
- } catch (_: Exception) {
- null
- }
+ SectionTitle(text = stringResource(R.string.settings_screen_security_title))
+
+ SettingsCard(index = 0, listSize = 3) {
+ SwitchItem(
+ title = stringResource(R.string.settings_screen_auto_unlock_title),
+ summary = stringResource(R.string.settings_screen_auto_unlock_desc),
+ icon = Icons.Default.LockOpen,
+ checked = autoUnlock,
+ onCheckedChange = {
+ autoUnlock = it
+ appLockRepository.setAutoUnlockEnabled(it)
+ }
+ )
}
- val versionName = packageInfo?.versionName ?: "Unknown"
- Text(
- text = stringResource(R.string.settings_screen_version_template, versionName),
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- modifier = Modifier.padding(start = 16.dp, top = 8.dp, bottom = 8.dp)
- )
- }
- item {
- DonateButton()
- }
+ SettingsCard(index = 1, listSize = 3) {
+ SwitchItem(
+ title = stringResource(R.string.settings_screen_biometric_auth_title),
+ summary = stringResource(R.string.settings_screen_biometric_auth_desc),
+ icon = Icons.Default.Fingerprint,
+ checked = useBiometricAuth,
+ enabled = isBiometricAvailable,
+ onCheckedChange = {
+ useBiometricAuth = it
+ appLockRepository.setBiometricAuthEnabled(it)
+ }
+ )
+ }
- item {
- SectionTitle(text = stringResource(R.string.settings_screen_lock_screen_customization_title))
- }
+ SettingsCard(index = 2, listSize = 4) {
+ ClickableItem(
+ title = "Change Lock",
+ summary = when (appLockRepository.getLockType()) {
+ PreferencesRepository.LOCK_TYPE_PATTERN -> "Update your unlock pattern"
+ PreferencesRepository.LOCK_TYPE_PASSWORD -> "Update your unlock password"
+ else -> "Update your unlock PIN"
+ },
+ icon = Icons.Default.Lock,
+ onClick = {
+ navController.navigate(Screen.ChangePassword.route)
+ }
+ )
+ }
- item {
- SettingsGroup(
- items = listOf(
- ToggleSettingItem(
- icon = BrightnessHigh,
- title = stringResource(R.string.settings_screen_max_brightness_title),
- subtitle = stringResource(R.string.settings_screen_max_brightness_desc),
- checked = useMaxBrightness,
- enabled = true,
- onCheckedChange = { isChecked ->
- useMaxBrightness = isChecked
- appLockRepository.setUseMaxBrightness(isChecked)
- }
- ),
- ToggleSettingItem(
- icon = if (useBiometricAuth) Fingerprint else FingerprintOff,
- title = stringResource(R.string.settings_screen_biometric_auth_title),
- subtitle = if (isBiometricAvailable)
- stringResource(R.string.settings_screen_biometric_auth_desc_available)
- else
- stringResource(R.string.settings_screen_biometric_auth_desc_unavailable),
- checked = useBiometricAuth && isBiometricAvailable,
- enabled = isBiometricAvailable,
- onCheckedChange = { isChecked ->
- useBiometricAuth = isChecked
- appLockRepository.setBiometricAuthEnabled(isChecked)
- }
- ),
- ToggleSettingItem(
- icon = Icons.Default.Vibration,
- title = stringResource(R.string.settings_screen_haptic_feedback_title),
- subtitle = stringResource(R.string.settings_screen_haptic_feedback_desc),
- checked = disableHapticFeedback,
- enabled = true,
- onCheckedChange = { isChecked ->
- disableHapticFeedback = isChecked
- appLockRepository.setDisableHaptics(isChecked)
- }
- ),
- ToggleSettingItem(
- icon = Icons.Default.ShieldMoon,
- title = stringResource(R.string.settings_screen_auto_unlock_title),
- subtitle = stringResource(R.string.settings_screen_auto_unlock_desc),
- checked = autoUnlock,
- enabled = true,
- onCheckedChange = { isChecked ->
- autoUnlock = isChecked
- appLockRepository.setAutoUnlockEnabled(isChecked)
- }
- )
+ SettingsCard(index = 3, listSize = 4) {
+ SwitchItem(
+ title = "Prevent all app uninstall",
+ summary = "Require protection before any app uninstall attempt",
+ icon = Icons.Outlined.Security,
+ checked = preventAllAppUninstall,
+ onCheckedChange = {
+ preventAllAppUninstall = it
+ appLockRepository.setPreventAllAppUninstallEnabled(it)
+ }
)
- )
- }
+ }
- item {
- SectionTitle(text = stringResource(R.string.settings_screen_security_title))
- }
+ SectionTitle(text = "Intruder Selfie")
- item {
- SettingsGroup(
- items = listOf(
- ActionSettingItem(
- icon = Icons.Default.Lock,
- title = stringResource(R.string.settings_screen_change_pin_title),
- subtitle = stringResource(R.string.settings_screen_change_pin_desc),
- onClick = { navController.navigate(Screen.ChangePassword.route) }
- ),
- ActionSettingItem(
- icon = Timer,
- title = stringResource(R.string.settings_screen_unlock_duration_title),
- subtitle = if (unlockTimeDuration > 0) {
- if (unlockTimeDuration > 10_000) "Until screen off"
- else stringResource(
- R.string.settings_screen_unlock_duration_summary_minutes,
- unlockTimeDuration
- )
- } else stringResource(R.string.settings_screen_unlock_duration_summary_immediate),
- onClick = { showUnlockTimeDialog = true }
- ),
- ToggleSettingItem(
- icon = Icons.Default.Lock,
- title = stringResource(R.string.settings_screen_anti_uninstall_title),
- subtitle = stringResource(R.string.settings_screen_anti_uninstall_desc),
- checked = antiUninstallEnabled,
- enabled = true,
- onCheckedChange = { isChecked ->
- if (isChecked) {
- val dpm =
- context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
- val component = ComponentName(context, DeviceAdmin::class.java)
- val hasDeviceAdmin = dpm.isAdminActive(component)
- val hasAccessibility = context.isAccessibilityServiceEnabled()
-
- when {
- !hasDeviceAdmin && !hasAccessibility -> {
- showPermissionDialog = true
- }
- !hasDeviceAdmin -> {
- showDeviceAdminDialog = true
- }
- !hasAccessibility -> {
- showAccessibilityDialog = true
- }
- else -> {
- antiUninstallEnabled = true
- appLockRepository.setAntiUninstallEnabled(true)
- }
- }
+ SettingsCard(index = 0, listSize = 3) {
+ SwitchItem(
+ title = "Enable Intruder Selfie",
+ summary = "Capture a photo after failed unlock attempts",
+ icon = Icons.Default.CameraAlt,
+ checked = intruderSelfieEnabled,
+ onCheckedChange = { enabled ->
+ if (enabled) {
+ val permissionCheck = ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
+ if (permissionCheck == PackageManager.PERMISSION_GRANTED) {
+ intruderSelfieEnabled = true
+ appLockRepository.setIntruderSelfieEnabled(true)
} else {
- context.startActivity(
- Intent(context, AdminDisableActivity::class.java)
- )
+ cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
}
+ } else {
+ intruderSelfieEnabled = false
+ appLockRepository.setIntruderSelfieEnabled(false)
}
- )
+ }
)
- )
- }
+ }
- item {
- SectionTitle(text = stringResource(R.string.settings_screen_advanced_title))
- }
+ SettingsCard(index = 1, listSize = 3) {
+ FailedAttemptsSpinnerItem(
+ selectedAttempts = intruderSelfieAttempts,
+ enabled = intruderSelfieEnabled,
+ onAttemptsSelected = { attempts ->
+ intruderSelfieAttempts = attempts
+ appLockRepository.setIntruderSelfieAttempts(attempts)
+ }
+ )
+ }
- item {
- SettingsGroup(
- items = listOf(
- ActionSettingItem(
- icon = Icons.Outlined.Security,
- title = stringResource(R.string.settings_Screen_export_audit),
- subtitle = stringResource(R.string.settings_screen_export_audit_desc),
- onClick = {
- val uri = LogUtils.exportAuditLogs()
- if (uri != null) {
- val shareIntent = Intent(Intent.ACTION_SEND).apply {
- type = "text/plain"
- putExtra(Intent.EXTRA_STREAM, uri)
- addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
- }
- context.startActivity(
- Intent.createChooser(shareIntent, "Share audit logs")
- )
- } else {
- Toast.makeText(
- context,
- context.getString(R.string.settings_screen_export_logs_error),
- Toast.LENGTH_SHORT
- ).show()
+ SettingsCard(index = 2, listSize = 3) {
+ ClickableItem(
+ title = "Show Intruder Selfies",
+ summary = "View captured photos of intruders",
+ icon = Icons.Default.PhotoLibrary,
+ enabled = hasSelfies,
+ onClick = {
+ navController.navigate(Screen.IntruderSelfies.route)
+ }
+ )
+ }
+
+ SectionTitle(text = stringResource(R.string.settings_screen_anti_uninstall_title))
+
+ SettingsCard(index = 0, listSize = 5) {
+ SwitchItem(
+ title = stringResource(R.string.settings_screen_anti_uninstall_title),
+ summary = stringResource(R.string.settings_screen_anti_uninstall_desc),
+ icon = Icons.Outlined.Security,
+ checked = antiUninstallEnabled,
+ onCheckedChange = {
+ antiUninstallEnabled = it
+ appLockRepository.setAntiUninstallEnabled(it)
+ }
+ )
+ }
+
+ SettingsCard(index = 1, listSize = 5) {
+ SwitchItem(
+ title = stringResource(R.string.permission_warning_device_admin_title),
+ summary = stringResource(R.string.permission_warning_device_admin_desc),
+ icon = Icons.Default.AdminPanelSettings,
+ checked = antiUninstallAdminSettings,
+ enabled = antiUninstallEnabled,
+ onCheckedChange = {
+ if (it) {
+ val dpm =
+ context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
+ val component = ComponentName(context, DeviceAdmin::class.java)
+ if (!dpm.isAdminActive(component)) {
+ showPermissionDialog = true
+ return@SwitchItem
}
}
- ),
- ActionSettingItem(
- icon = Icons.Outlined.BugReport,
- title = stringResource(R.string.settings_screen_export_logs_title),
- subtitle = stringResource(R.string.settings_screen_export_logs_desc),
- onClick = {
- val uri = LogUtils.exportLogs()
- if (uri != null) {
- val shareIntent = Intent(Intent.ACTION_SEND).apply {
- type = "text/plain"
- putExtra(Intent.EXTRA_STREAM, uri)
- addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
- }
- context.startActivity(
- Intent.createChooser(shareIntent, "Share logs")
- )
- } else {
- Toast.makeText(
- context,
- context.getString(R.string.settings_screen_export_logs_error),
- Toast.LENGTH_SHORT
- ).show()
- }
+ antiUninstallAdminSettings = it
+ appLockRepository.setAntiUninstallAdminSettingsEnabled(it)
+ }
+ )
+ }
+
+ SettingsCard(index = 2, listSize = 5) {
+ SwitchItem(
+ title = stringResource(R.string.permission_warning_usage_stats_title),
+ summary = stringResource(R.string.permission_warning_usage_stats_desc),
+ icon = Icons.Default.QueryStats,
+ checked = antiUninstallUsageStats,
+ enabled = antiUninstallEnabled,
+ onCheckedChange = {
+ if (it && !context.hasUsagePermission()) {
+ val intent =
+ Intent(android.provider.Settings.ACTION_USAGE_ACCESS_SETTINGS)
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ context.startActivity(intent)
+ return@SwitchItem
}
- ),
- ToggleSettingItem(
- icon = Icons.Default.Troubleshoot,
- title = "Logging",
- subtitle = "Enable debug logging for troubleshooting",
- checked = loggingEnabled,
- enabled = true,
- onCheckedChange = { isChecked ->
- loggingEnabled = isChecked
- appLockRepository.setLoggingEnabled(isChecked)
- LogUtils.setLoggingEnabled(isChecked)
+ antiUninstallUsageStats = it
+ appLockRepository.setAntiUninstallUsageStatsEnabled(it)
+ }
+ )
+ }
+
+ SettingsCard(index = 3, listSize = 5) {
+ SwitchItem(
+ title = stringResource(R.string.permission_warning_accessibility_title),
+ summary = stringResource(R.string.permission_warning_accessibility_desc),
+ icon = Icons.Default.Accessibility,
+ checked = antiUninstallAccessibility,
+ enabled = antiUninstallEnabled,
+ onCheckedChange = {
+ if (it && !context.isAccessibilityServiceEnabled()) {
+ showAccessibilityDialog = true
+ return@SwitchItem
}
- )
+ antiUninstallAccessibility = it
+ appLockRepository.setAntiUninstallAccessibilityEnabled(it)
+ }
+ )
+ }
+
+ SettingsCard(index = 4, listSize = 5) {
+ SwitchItem(
+ title = stringResource(R.string.permission_warning_overlay_title),
+ summary = stringResource(R.string.permission_warning_overlay_desc),
+ icon = Icons.Default.Layers,
+ checked = antiUninstallOverlay,
+ enabled = antiUninstallEnabled,
+ onCheckedChange = {
+ antiUninstallOverlay = it
+ appLockRepository.setAntiUninstallOverlayEnabled(it)
+ }
)
+ }
+
+ SectionTitle(text = stringResource(R.string.settings_screen_lock_screen_customization_title))
+
+ SettingsCard(index = 0, listSize = 5) {
+ SwitchItem(
+ title = stringResource(R.string.settings_screen_max_brightness_title),
+ summary = stringResource(R.string.settings_screen_max_brightness_desc),
+ icon = Icons.Default.BrightnessHigh,
+ checked = useMaxBrightness,
+ onCheckedChange = {
+ useMaxBrightness = it
+ appLockRepository.setUseMaxBrightness(it)
+ }
+ )
+ }
+
+ SettingsCard(index = 1, listSize = 5) {
+ SwitchItem(
+ title = stringResource(R.string.settings_screen_amoled_mode_title),
+ summary = stringResource(R.string.settings_screen_amoled_mode_desc),
+ icon = Icons.Default.DarkMode,
+ checked = amoledModeEnabled,
+ onCheckedChange = {
+ appLockRepository.setAmoledModeEnabled(it)
+ }
+ )
+ }
+
+ SettingsCard(index = 2, listSize = 5) {
+ SwitchItem(
+ title = "Use Dynamic Theme",
+ summary = "Apply dynamic colors from your wallpaper (Android 12+)",
+ icon = Icons.Default.Palette,
+ checked = dynamicColorEnabled,
+ enabled = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S,
+ onCheckedChange = {
+ appLockRepository.setDynamicColorEnabled(it)
+ }
+ )
+ }
+
+ SettingsCard(index = 3, listSize = 5) {
+ SwitchItem(
+ title = stringResource(R.string.settings_screen_haptic_feedback_title),
+ summary = stringResource(R.string.settings_screen_haptic_feedback_desc),
+ icon = Icons.Default.Vibration,
+ checked = disableHapticFeedback,
+ onCheckedChange = {
+ disableHapticFeedback = it
+ appLockRepository.setDisableHaptics(it)
+ }
+ )
+ }
+
+ SettingsCard(index = 4, listSize = 5) {
+ ClickableItem(
+ title = stringResource(R.string.settings_screen_unlock_duration_title),
+ summary = when (unlockTimeDuration) {
+ 0 -> stringResource(R.string.settings_screen_unlock_duration_dialog_option_immediate)
+ 1 -> stringResource(
+ R.string.settings_screen_unlock_duration_dialog_option_minute,
+ unlockTimeDuration
+ )
+ 60 -> stringResource(R.string.settings_screen_unlock_duration_dialog_option_hour)
+ Integer.MAX_VALUE -> "Until Screen Off"
+ else -> stringResource(
+ R.string.settings_screen_unlock_duration_summary_minutes,
+ unlockTimeDuration
+ )
+ },
+ icon = Icons.Default.Timer,
+ onClick = { showUnlockTimeDialog = true }
+ )
+ }
+
+ SectionTitle(text = stringResource(R.string.settings_screen_app_theme_title))
+ AppThemeModeCard(
+ selectedThemeMode = appThemeMode,
+ onThemeModeSelected = {
+ appLockRepository.setAppThemeMode(it)
+ }
)
- }
- item {
BackendSelectionCard(
appLockRepository = appLockRepository,
context = context,
- shizukuPermissionLauncher = shizukuPermissionLauncher
+ shizukuPermissionLauncher = shizukuPermissionLauncher,
+ shizukuNotRunningMsg = shizukuNotRunningMsg,
+ usagePermissionMsg = usagePermissionMsg
)
- }
- item {
- LinksSection()
+ SectionTitle(text = "App Details")
+
+ SettingsCard(index = 0, listSize = 3) {
+ ClickableItem(
+ title = "App Version",
+ summary = try {
+ context.packageManager.getPackageInfo(
+ context.packageName,
+ 0
+ ).versionName ?: "Unknown"
+ } catch (e: Exception) {
+ "Unknown"
+ },
+ icon = Icons.Default.Info,
+ onClick = {}
+ )
+ }
+
+ SettingsCard(index = 1, listSize = 3) {
+ SwitchItem(
+ title = stringResource(R.string.settings_screen_logging_title),
+ summary = stringResource(R.string.settings_screen_logging_summary),
+ icon = Icons.Outlined.BugReport,
+ checked = loggingEnabled,
+ onCheckedChange = {
+ loggingEnabled = it
+ appLockRepository.setLoggingEnabled(it)
+ }
+ )
+ }
+
+ SettingsCard(index = 2, listSize = 3) {
+ ClickableItem(
+ title = "Export Logs",
+ summary = "Save app logs to your Downloads folder",
+ icon = Icons.Default.Download,
+ onClick = {
+ val appName = context.getString(R.string.app_name)
+ val exportedFile = LogUtils.exportLogsToDownloads(appName)
+ if (exportedFile != null) {
+ Toast.makeText(context, "Logs exported: $exportedFile", Toast.LENGTH_LONG).show()
+ } else {
+ Toast.makeText(context, exportLogsError, Toast.LENGTH_SHORT).show()
+ }
+ }
+ )
+ }
+
+ SectionTitle(text = stringResource(R.string.settings_screen_links_section))
+
+ SettingsCard(index = 0, listSize = 3) {
+ LinkItem(
+ title = stringResource(R.string.settings_screen_developer_email_ap),
+ icon = Icons.Default.Email,
+ onClick = {
+ val intent = Intent(
+ Intent.ACTION_SENDTO,
+ "mailto:ap0803apap@gmail.com".toUri()
+ )
+ context.startActivity(intent)
+ }
+ )
+ }
+
+ SettingsCard(index = 1, listSize = 3) {
+ LinkItem(
+ title = stringResource(R.string.settings_screen_source_code),
+ icon = Icons.Default.Code,
+ onClick = {
+ val intent = Intent(
+ Intent.ACTION_VIEW,
+ "https://github.com/ap0803apap-sketch".toUri()
+ )
+ context.startActivity(intent)
+ }
+ )
+ }
+
+ SettingsCard(index = 2, listSize = 3) {
+ LinkItem(
+ title = stringResource(R.string.settings_screen_report_issue),
+ icon = Icons.Default.BugReport,
+ onClick = {
+ val intent = Intent(
+ Intent.ACTION_VIEW,
+ "https://github.com/ap0803apap-sketch".toUri()
+ )
+ context.startActivity(intent)
+ }
+ )
+ }
+
+ Spacer(modifier = Modifier.height(24.dp))
}
}
}
@@ -458,80 +709,12 @@ fun SettingsScreen(
fun SectionTitle(text: String) {
Text(
text = text,
- style = MaterialTheme.typography.titleSmall,
- fontWeight = FontWeight.SemiBold,
+ style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary,
- modifier = Modifier.padding(start = 16.dp, bottom = 4.dp, top = 4.dp)
+ modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
)
}
-sealed class SettingItemType {
- data class Toggle(
- val icon: ImageVector,
- val title: String,
- val subtitle: String,
- val checked: Boolean,
- val enabled: Boolean,
- val onCheckedChange: (Boolean) -> Unit
- ): SettingItemType()
-
- data class Action(
- val icon: ImageVector,
- val title: String,
- val subtitle: String,
- val onClick: () -> Unit
- ): SettingItemType()
-}
-
-data class ToggleSettingItem(
- val icon: ImageVector,
- val title: String,
- val subtitle: String,
- val checked: Boolean,
- val enabled: Boolean,
- val onCheckedChange: (Boolean) -> Unit
-)
-
-data class ActionSettingItem(
- val icon: ImageVector,
- val title: String,
- val subtitle: String,
- val onClick: () -> Unit
-)
-
-@Composable
-fun SettingsGroup(
- items: List
-) {
- Column {
- items.forEachIndexed { index, item ->
- SettingsCard(index = index, listSize = items.size) {
- when (item) {
- is ToggleSettingItem -> {
- ToggleSettingRow(
- icon = item.icon,
- title = item.title,
- subtitle = item.subtitle,
- checked = item.checked,
- enabled = item.enabled,
- onCheckedChange = item.onCheckedChange
- )
- }
-
- is ActionSettingItem -> {
- ActionSettingRow(
- icon = item.icon,
- title = item.title,
- subtitle = item.subtitle,
- onClick = item.onClick
- )
- }
- }
- }
- }
- }
-}
-
@Composable
fun SettingsCard(
index: Int,
@@ -540,93 +723,67 @@ fun SettingsCard(
) {
val shape = when {
listSize == 1 -> RoundedCornerShape(24.dp)
- index == 0 -> RoundedCornerShape(
- topStart = 24.dp,
- topEnd = 24.dp,
- bottomStart = 6.dp,
- bottomEnd = 6.dp
- )
-
- index == listSize - 1 -> RoundedCornerShape(
- topStart = 6.dp,
- topEnd = 6.dp,
- bottomStart = 24.dp,
- bottomEnd = 24.dp
- )
-
- else -> RoundedCornerShape(6.dp)
+ index == 0 -> RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp, bottomStart = 4.dp, bottomEnd = 4.dp)
+ index == listSize - 1 -> RoundedCornerShape(topStart = 4.dp, topEnd = 4.dp, bottomStart = 24.dp, bottomEnd = 24.dp)
+ else -> RoundedCornerShape(4.dp)
}
- AnimatedVisibility(
- visible = true,
- enter = fadeIn() + scaleIn(
- initialScale = 0.95f,
- animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy)
- ),
- exit = fadeOut() + shrinkVertically()
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 2.dp),
+ shape = shape,
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surfaceContainerHigh
+ )
) {
- Card(
- modifier = Modifier
- .fillMaxWidth()
- .padding(vertical = 1.dp),
- colors = CardDefaults.cardColors(
- containerColor = MaterialTheme.colorScheme.surfaceContainer
- ),
- shape = shape
- ) {
- content()
- }
+ content()
}
}
@Composable
-fun ToggleSettingRow(
- icon: ImageVector,
+fun SwitchItem(
title: String,
- subtitle: String,
+ summary: String,
+ icon: ImageVector,
checked: Boolean,
- enabled: Boolean,
+ enabled: Boolean = true,
onCheckedChange: (Boolean) -> Unit
) {
ListItem(
- modifier = Modifier
- .clickable(enabled = enabled) { if (enabled) onCheckedChange(!checked) }
- .padding(vertical = 2.dp, horizontal = 4.dp),
+ modifier = Modifier.toggleable(
+ value = checked,
+ enabled = enabled,
+ role = Role.Switch,
+ onValueChange = onCheckedChange
+ ),
headlineContent = {
Text(
text = title,
- style = MaterialTheme.typography.titleMedium
+ style = MaterialTheme.typography.titleMedium,
+ color = if (enabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
)
},
supportingContent = {
Text(
- text = subtitle,
- style = MaterialTheme.typography.bodySmall
+ text = summary,
+ style = MaterialTheme.typography.bodySmall,
+ color = if (enabled) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f)
)
},
leadingContent = {
- Box(
- modifier = Modifier.size(24.dp),
- contentAlignment = Alignment.Center
- ) {
- Icon(
- imageVector = icon,
- contentDescription = null,
- modifier = Modifier.size(24.dp),
- tint = MaterialTheme.colorScheme.secondary
- )
- }
+ Icon(
+ imageVector = icon,
+ contentDescription = null,
+ tint = if (enabled) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.primary.copy(alpha = 0.38f)
+ )
},
trailingContent = {
- Box(
- contentAlignment = Alignment.Center
- ) {
- Switch(
- checked = checked,
- onCheckedChange = null,
- enabled = enabled
- )
- }
+ Switch(
+ checked = checked,
+ onCheckedChange = null,
+ enabled = enabled
+ )
},
colors = ListItemDefaults.colors(
containerColor = Color.Transparent
@@ -635,50 +792,138 @@ fun ToggleSettingRow(
}
@Composable
-fun ActionSettingRow(
- icon: ImageVector,
+fun ClickableItem(
title: String,
- subtitle: String,
+ summary: String,
+ icon: ImageVector,
+ enabled: Boolean = true,
onClick: () -> Unit
) {
ListItem(
- modifier = Modifier
- .clickable(onClick = onClick)
- .padding(vertical = 2.dp, horizontal = 4.dp),
+ modifier = Modifier.clickable(enabled = enabled, onClick = onClick),
headlineContent = {
Text(
text = title,
- style = MaterialTheme.typography.titleMedium
+ style = MaterialTheme.typography.titleMedium,
+ color = if (enabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
)
},
supportingContent = {
Text(
- text = subtitle,
- style = MaterialTheme.typography.bodySmall
+ text = summary,
+ style = MaterialTheme.typography.bodySmall,
+ color = if (enabled) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f)
)
},
leadingContent = {
- Box(
- modifier = Modifier.size(24.dp),
- contentAlignment = Alignment.Center
- ) {
+ Icon(
+ imageVector = icon,
+ contentDescription = null,
+ tint = if (enabled) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.primary.copy(alpha = 0.38f)
+ )
+ },
+ trailingContent = {
+ Icon(
+ imageVector = Icons.AutoMirrored.Rounded.KeyboardArrowRight,
+ contentDescription = null,
+ tint = if (enabled) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f)
+ )
+ },
+ colors = ListItemDefaults.colors(
+ containerColor = Color.Transparent
+ )
+ )
+}
+
+@Composable
+fun FailedAttemptsSpinnerItem(
+ selectedAttempts: Int,
+ enabled: Boolean,
+ onAttemptsSelected: (Int) -> Unit
+) {
+ var expanded by remember { mutableStateOf(false) }
+ val attemptsOptions = listOf(1, 2, 3, 4, 5)
+
+ Box {
+ ListItem(
+ modifier = Modifier.clickable(enabled = enabled) { expanded = true },
+ headlineContent = {
+ Text(
+ text = "Failed attempts",
+ style = MaterialTheme.typography.titleMedium,
+ color = if (enabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
+ )
+ },
+ supportingContent = {
+ Text(
+ text = "Take photo after $selectedAttempts failed attempt(s)",
+ style = MaterialTheme.typography.bodySmall,
+ color = if (enabled) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f)
+ )
+ },
+ leadingContent = {
Icon(
- imageVector = icon,
+ imageVector = Icons.Default.Warning,
contentDescription = null,
- modifier = Modifier.size(24.dp),
- tint = MaterialTheme.colorScheme.secondary
+ tint = if (enabled) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.primary.copy(alpha = 0.38f)
)
- }
- },
- trailingContent = {
- Box(
- contentAlignment = Alignment.Center
- ) {
+ },
+ trailingContent = {
Icon(
- imageVector = Icons.AutoMirrored.Rounded.KeyboardArrowRight,
- contentDescription = null
+ imageVector = Icons.Default.ArrowDropDown,
+ contentDescription = null,
+ tint = if (enabled) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f)
+ )
+ },
+ colors = ListItemDefaults.colors(
+ containerColor = Color.Transparent
+ )
+ )
+
+ DropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { expanded = false }
+ ) {
+ attemptsOptions.forEach { option ->
+ DropdownMenuItem(
+ text = { Text("$option attempt(s)") },
+ onClick = {
+ onAttemptsSelected(option)
+ expanded = false
+ }
)
}
+ }
+ }
+}
+
+@Composable
+fun LinkItem(
+ title: String,
+ icon: ImageVector,
+ onClick: () -> Unit
+) {
+ ListItem(
+ modifier = Modifier.clickable(onClick = onClick),
+ headlineContent = {
+ Text(
+ text = title,
+ style = MaterialTheme.typography.titleMedium
+ )
+ },
+ leadingContent = {
+ Icon(
+ imageVector = icon,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary
+ )
+ },
+ trailingContent = {
+ Icon(
+ imageVector = Icons.AutoMirrored.Rounded.KeyboardArrowRight,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onSurfaceVariant
+ )
},
colors = ListItemDefaults.colors(
containerColor = Color.Transparent
@@ -692,30 +937,35 @@ fun UnlockTimeDurationDialog(
onDismiss: () -> Unit,
onConfirm: (Int) -> Unit
) {
- val durations = listOf(0, 1, 5, 15, 30, 60, Integer.MAX_VALUE)
+ val durations = listOf(0, 1, 5, 10, 30, 60, Integer.MAX_VALUE)
var selectedDuration by remember { mutableIntStateOf(currentDuration) }
- if (!durations.contains(selectedDuration)) {
- selectedDuration = durations.minByOrNull { abs(it - currentDuration) } ?: 0
- }
-
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(stringResource(R.string.settings_screen_unlock_duration_dialog_title)) },
text = {
- Column {
- Text(stringResource(R.string.settings_screen_unlock_duration_dialog_description_new))
+ val durationsScrollState = rememberScrollState()
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .verticalScroll(durationsScrollState)
+ .selectableGroup()
+ ) {
durations.forEach { duration ->
Row(
modifier = Modifier
.fillMaxWidth()
- .clickable { selectedDuration = duration }
- .padding(vertical = 8.dp),
+ .selectable(
+ selected = selectedDuration == duration,
+ onClick = { selectedDuration = duration },
+ role = Role.RadioButton
+ )
+ .padding(vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = selectedDuration == duration,
- onClick = { selectedDuration = duration }
+ onClick = null
)
Text(
text = when (duration) {
@@ -754,7 +1004,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 +1033,7 @@ fun BackendSelectionCard(
} else {
Toast.makeText(
context,
- context.getString(R.string.settings_screen_shizuku_not_running_toast),
+ shizukuNotRunningMsg,
Toast.LENGTH_LONG
).show()
}
@@ -803,7 +1055,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
@@ -836,11 +1088,11 @@ fun BackendSelectionCard(
fun BackendSelectionItem(
backend: BackendImplementation,
isSelected: Boolean,
- onClick: () -> Unit
+ onClick: (() -> Unit)?
) {
ListItem(
modifier = Modifier
- .clickable { onClick() }
+ .clickable { onClick?.invoke() }
.padding(vertical = 2.dp, horizontal = 4.dp),
headlineContent = {
Row(
@@ -928,6 +1180,51 @@ private fun getBackendIcon(backend: BackendImplementation): ImageVector {
}
}
+
+@Composable
+fun AppThemeModeCard(
+ selectedThemeMode: AppThemeMode,
+ onThemeModeSelected: (AppThemeMode) -> Unit
+) {
+ val modes = listOf(
+ AppThemeMode.SYSTEM to stringResource(R.string.settings_screen_theme_mode_system),
+ AppThemeMode.LIGHT to stringResource(R.string.settings_screen_theme_mode_light),
+ AppThemeMode.DARK to stringResource(R.string.settings_screen_theme_mode_dark)
+ )
+
+ SettingsCard(index = 0, listSize = 1) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .selectableGroup()
+ ) {
+ modes.forEach { (mode, label) ->
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .selectable(
+ selected = selectedThemeMode == mode,
+ onClick = { onThemeModeSelected(mode) },
+ role = Role.RadioButton
+ )
+ .padding(horizontal = 20.dp, vertical = 12.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ RadioButton(
+ selected = selectedThemeMode == mode,
+ onClick = null
+ )
+ Spacer(modifier = Modifier.width(12.dp))
+ Text(
+ text = label,
+ style = MaterialTheme.typography.bodyLarge
+ )
+ }
+ }
+ }
+ }
+}
+
@Composable
fun PermissionRequiredDialog(
onDismiss: () -> Unit,
@@ -965,13 +1262,12 @@ fun DeviceAdminDialog(
title = { Text(stringResource(R.string.settings_screen_device_admin_dialog_title)) },
text = {
Column {
- Text(stringResource(R.string.settings_screen_device_admin_dialog_text_1))
- Text(stringResource(R.string.settings_screen_device_admin_dialog_text_2))
+ Text(stringResource(R.string.settings_screen_device_admin_dialog_text))
}
},
confirmButton = {
TextButton(onClick = onConfirm) {
- Text(stringResource(R.string.enable_button))
+ Text(stringResource(R.string.grant_permission_button))
}
},
dismissButton = {
@@ -992,14 +1288,12 @@ fun AccessibilityDialog(
title = { Text(stringResource(R.string.settings_screen_accessibility_dialog_title)) },
text = {
Column {
- Text(stringResource(R.string.settings_screen_accessibility_dialog_text_1))
- Text(stringResource(R.string.settings_screen_accessibility_dialog_text_2))
- Text(stringResource(R.string.settings_screen_accessibility_dialog_text_3))
+ Text(stringResource(R.string.settings_screen_accessibility_dialog_text))
}
},
confirmButton = {
TextButton(onClick = onConfirm) {
- Text(stringResource(R.string.enable_button))
+ Text(stringResource(R.string.grant_permission_button))
}
},
dismissButton = {
@@ -1009,101 +1303,3 @@ fun AccessibilityDialog(
}
)
}
-
-@Composable
-fun LinksSection() {
- val context = LocalContext.current
-
- Column {
- SectionTitle(text = "Links")
-
- Column {
- SettingsCard(index = 0, listSize = 3) {
- LinkItem(
- title = "Discord Community",
- icon = Discord,
- onClick = {
- val intent = Intent(
- Intent.ACTION_VIEW,
- "https://discord.gg/46wCMRVAre".toUri()
- )
- context.startActivity(intent)
- }
- )
- }
-
- SettingsCard(index = 1, listSize = 3) {
- LinkItem(
- title = "Source Code",
- icon = Icons.Outlined.Code,
- onClick = {
- val intent = Intent(
- Intent.ACTION_VIEW,
- "https://github.com/aload0/AppLock".toUri()
- )
- context.startActivity(intent)
- }
- )
- }
-
- SettingsCard(index = 2, listSize = 3) {
- LinkItem(
- title = "Report Issue",
- icon = Icons.Outlined.BugReport,
- onClick = {
- val intent = Intent(
- Intent.ACTION_VIEW,
- "https://github.com/aload0/AppLock/issues".toUri()
- )
- context.startActivity(intent)
- }
- )
- }
- }
- }
-}
-
-@Composable
-fun LinkItem(
- title: String,
- icon: ImageVector,
- onClick: () -> Unit
-) {
- ListItem(
- modifier = Modifier
- .clickable(onClick = onClick)
- .padding(vertical = 2.dp, horizontal = 4.dp),
- headlineContent = {
- Text(
- text = title,
- style = MaterialTheme.typography.titleMedium
- )
- },
- leadingContent = {
- Box(
- modifier = Modifier.size(24.dp),
- contentAlignment = Alignment.Center
- ) {
- Icon(
- imageVector = icon,
- contentDescription = null,
- modifier = Modifier.size(24.dp),
- tint = MaterialTheme.colorScheme.secondary
- )
- }
- },
- trailingContent = {
- Box(
- contentAlignment = Alignment.Center
- ) {
- Icon(
- imageVector = Icons.AutoMirrored.Rounded.KeyboardArrowRight,
- contentDescription = null
- )
- }
- },
- colors = ListItemDefaults.colors(
- containerColor = Color.Transparent
- )
- )
-}
diff --git a/app/src/main/java/dev/pranav/applock/services/AppLockAccessibilityService.kt b/app/src/main/java/dev/pranav/applock/services/AppLockAccessibilityService.kt
index 5377dba..cc12009 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
@@ -29,17 +39,21 @@ import rikka.shizuku.Shizuku
class AppLockAccessibilityService : AccessibilityService() {
private val appLockRepository: AppLockRepository by lazy { applicationContext.appLockRepository() }
private val keyboardPackages: List by lazy { getKeyboardPackageNames() }
+ private var launcherPackages: Set = emptySet()
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 +67,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 +82,8 @@ class AppLockAccessibilityService : AccessibilityService() {
AppLockManager.currentBiometricState = BiometricState.IDLE
AppLockManager.isLockScreenShown.set(false)
startPrimaryBackendService()
+ startForegroundService()
+ updateLauncherPackages()
val filter = android.content.IntentFilter().apply {
addAction(Intent.ACTION_SCREEN_OFF)
@@ -80,14 +95,16 @@ 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()
try {
serviceInfo = serviceInfo.apply {
eventTypes = AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED or
- AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED or
AccessibilityEvent.TYPE_WINDOWS_CHANGED
feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC
packageNames = null
@@ -96,114 +113,241 @@ class AppLockAccessibilityService : AccessibilityService() {
Log.d(TAG, "Accessibility service connected")
AppLockManager.resetRestartAttempts(TAG)
appLockRepository.setActiveBackend(BackendImplementation.ACCESSIBILITY)
+ startForegroundService()
+ updateLauncherPackages()
} catch (e: Exception) {
logError("Error in onServiceConnected", e)
}
}
+ private fun updateLauncherPackages() {
+ try {
+ val intent = Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME)
+ launcherPackages = packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
+ .map { it.activityInfo.packageName }
+ .toSet()
+ Log.d(TAG, "Launcher packages updated: $launcherPackages")
+ } catch (e: Exception) {
+ logError("Error updating launcher packages", 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.")
+ 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()
+
+ 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
+ // Only react to major window state changes to avoid triggering on notifications or keyboards
if (event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
- try {
- handleWindowStateChanged(event)
- } catch (e: Exception) {
- logError("Error handling window state change", e)
+ val eventPackage = event.packageName?.toString() ?: return
+
+ // 1. Get the current actual active package from the window manager if possible
+ val activePackageName = rootInActiveWindow?.packageName?.toString() ?: eventPackage
+
+ // 2. Detect if we are on any Launcher/Home screen
+ if (launcherPackages.contains(eventPackage) || launcherPackages.contains(activePackageName)) {
+ val unlockedApp = AppLockManager.temporarilyUnlockedApp
+ if (unlockedApp.isNotEmpty()) {
+ // Record that we left this app to handle stray exit events via grace period
+ AppLockManager.setRecentlyLeftApp(unlockedApp)
+ AppLockManager.clearTemporarilyUnlockedApp()
+ }
+ recentsOpen = false
+ lastForegroundPackage = if (launcherPackages.contains(activePackageName)) activePackageName else eventPackage
return
}
- }
- // Skip processing if recents are open
- if (recentsOpen) {
- LogUtils.d(TAG, "Recents opened, ignoring accessibility event")
- return
+ // 3. Handle Recents and focus transitions
+ handleWindowStateChanged(event)
+
+ if (recentsOpen) {
+ return
+ }
+
+ // 4. Check if it's a valid app package for locking
+ if (!isValidPackageForLocking(eventPackage)) {
+ return
+ }
+
+ // 5. Process locking logic
+ try {
+ processPackageLocking(eventPackage)
+ } catch (e: Exception) {
+ logError("Error processing package locking for $eventPackage", e)
+ }
}
+ }
- // Extract and validate package name
+ private fun handleAntiUninstallBlocking(event: AccessibilityEvent) {
val packageName = event.packageName?.toString() ?: return
-
- // Skip if device is locked or app is excluded
- if (!isValidPackageForLocking(packageName)) {
- return
+
+ val isSettings = packageName.contains("settings") ||
+ packageName.contains("packageinstaller") ||
+ packageName.contains("permissioncontroller") ||
+ packageName == "android"
+
+ if (!isSettings) return
+
+ val className = event.className?.toString() ?: ""
+ val root = rootInActiveWindow
+
+ if (appLockRepository.isPreventAllAppUninstallEnabled()) {
+ if (className.contains("Uninstaller") || className.contains("PackageInstaller") || containsTextRecursive(root, "Uninstall")) {
+ blockAccess("App uninstall is protected.")
+ return
+ }
}
- try {
- processPackageLocking(packageName)
- } catch (e: Exception) {
- logError("Error processing package locking for $packageName", e)
+ if (root != null && (containsTextRecursive(root, "dev.pranav.applock") || containsTextRecursive(root, "APP Lock by AP"))) {
+ if (className.contains("AppDetails") || className.contains("InstalledAppDetails") ||
+ className.contains("Uninstaller") || className.contains("PackageInstaller") ||
+ className.contains("Settings\$AppDetailsActivity")) {
+ blockAccess("APP Lock by AP protection is active.")
+ return
+ }
}
- }
- private fun handleWindowStateChanged(event: AccessibilityEvent) {
- val isRecentlyOpened = isRecentlyOpened(event)
- val isHomeScreen = isHomeScreen(event)
+ 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
+ }
+ }
+ }
- when {
- isRecentlyOpened -> {
- LogUtils.d(TAG, "Entering recents")
- recentsOpen = true
+ 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
+ }
}
+ }
- isHomeScreenTransition(event) && recentsOpen -> {
- LogUtils.d(TAG, "Transitioning to home screen from recents")
- recentsOpen = false
- clearTemporarilyUnlockedAppIfNeeded()
+ 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 (containsTextRecursive(root, "APP Lock by AP")) {
+ blockAccess("Accessibility settings for APP Lock by AP are protected.")
+ return
+ }
}
+ }
- isHomeScreen -> {
- LogUtils.d(TAG, "On home screen")
- recentsOpen = false
- clearTemporarilyUnlockedAppIfNeeded()
+ 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
+ }
}
+ }
+ }
- isAppSwitchedFromRecents(event) -> {
- LogUtils.d(TAG, "App switched from recents")
- recentsOpen = false
- clearTemporarilyUnlockedAppIfNeeded(event.packageName?.toString())
+ 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
}
- @SuppressLint("InlinedApi")
- private fun isRecentlyOpened(event: AccessibilityEvent): Boolean {
- return (event.packageName == getSystemDefaultLauncherPackageName() &&
- event.contentChangeTypes == AccessibilityEvent.CONTENT_CHANGE_TYPE_PANE_APPEARED) ||
- (event.text.toString().lowercase().contains("recent apps"))
+ private fun blockAccess(message: String) {
+ performGlobalAction(GLOBAL_ACTION_HOME)
+ Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}
- private fun isHomeScreen(event: AccessibilityEvent): Boolean {
- return event.packageName == getSystemDefaultLauncherPackageName() &&
- event.className == "com.android.launcher3.uioverrides.QuickstepLauncher" &&
- event.text.toString().lowercase().contains("home screen")
+ private fun handleWindowStateChanged(event: AccessibilityEvent) {
+ if (isRecentlyOpened(event)) {
+ recentsOpen = true
+ } else if (isAppSwitchedFromRecents(event)) {
+ recentsOpen = false
+ clearTemporarilyUnlockedAppIfNeeded(event.packageName?.toString())
+ }
}
@SuppressLint("InlinedApi")
- private fun isHomeScreenTransition(event: AccessibilityEvent): Boolean {
- return event.contentChangeTypes == AccessibilityEvent.CONTENT_CHANGE_TYPE_PANE_DISAPPEARED &&
- event.packageName == getSystemDefaultLauncherPackageName()
+ private fun isRecentlyOpened(event: AccessibilityEvent): Boolean {
+ val packageName = event.packageName?.toString() ?: ""
+ return (launcherPackages.contains(packageName) &&
+ event.contentChangeTypes == AccessibilityEvent.CONTENT_CHANGE_TYPE_PANE_APPEARED) ||
+ (event.text.toString().lowercase().contains("recent apps"))
}
private fun isAppSwitchedFromRecents(event: AccessibilityEvent): Boolean {
- return event.packageName != getSystemDefaultLauncherPackageName() && recentsOpen
+ val packageName = event.packageName?.toString() ?: ""
+ return !launcherPackages.contains(packageName) && recentsOpen
}
private fun clearTemporarilyUnlockedAppIfNeeded(newPackage: String? = null) {
@@ -212,56 +356,58 @@ 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 ||
+ // Don't lock our own app, keyboards, system apps, or launchers
+ if (packageName == applicationContext.packageName ||
packageName in keyboardPackages ||
- packageName in EXCLUDED_APPS
+ packageName in EXCLUDED_APPS ||
+ launcherPackages.contains(packageName)
) {
return false
}
- // Skip known recents classes
return true
}
private fun processPackageLocking(packageName: String) {
val currentForegroundPackage = packageName
+
+ // Attempt to restore state during transitions (handles stray "exit" events)
+ if (AppLockManager.checkAndRestoreRecentlyLeftApp(currentForegroundPackage)) {
+ LogUtils.d(TAG, "Restored state for $currentForegroundPackage via grace period")
+ }
+
val triggeringPackage = lastForegroundPackage
lastForegroundPackage = currentForegroundPackage
- // Skip if triggering package is excluded
+ // "One-way" Entry-based Detection: Only trigger if the package actually changed
+ if (currentForegroundPackage == triggeringPackage) {
+ return
+ }
+
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 +429,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 +448,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 +467,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 +474,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 +494,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 }
@@ -478,54 +504,20 @@ class AppLockAccessibilityService : AccessibilityService() {
}
}
- fun getSystemDefaultLauncherPackageName(): String {
- return try {
- val packageManager = packageManager
- val homeIntent = Intent(Intent.ACTION_MAIN).apply {
- addCategory(Intent.CATEGORY_HOME)
- }
-
- val resolveInfoList: List = packageManager.queryIntentActivities(
- homeIntent,
- PackageManager.MATCH_DEFAULT_ONLY
- )
-
- val systemLauncher = resolveInfoList.find { resolveInfo ->
- val isSystemApp = (resolveInfo.activityInfo.applicationInfo.flags and
- android.content.pm.ApplicationInfo.FLAG_SYSTEM) != 0
- val isOurApp = resolveInfo.activityInfo.packageName == packageName
-
- isSystemApp && !isOurApp
- }
-
- systemLauncher?.activityInfo?.packageName?.also {
- if (it.isEmpty()) {
- Log.w(TAG, "Could not find a clear system launcher package name.")
- }
- } ?: ""
- } catch (e: Exception) {
- logError("Error getting system default launcher package", e)
- ""
- }
- }
-
private fun startPrimaryBackendService() {
try {
AppLockManager.stopAllOtherServices(this, AppLockAccessibilityService::class.java)
when (appLockRepository.getBackendImplementation()) {
BackendImplementation.SHIZUKU -> {
- Log.d(TAG, "Starting Shizuku service as primary backend")
startService(Intent(this, ShizukuAppLockService::class.java))
}
BackendImplementation.USAGE_STATS -> {
- Log.d(TAG, "Starting Experimental service as primary backend")
startService(Intent(this, ExperimentalAppLockService::class.java))
}
else -> {
- Log.d(TAG, "Accessibility service is the primary backend.")
}
}
} catch (e: Exception) {
@@ -533,17 +525,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,
+ "APP Lock by AP Accessibility Service",
+ NotificationManager.IMPORTANCE_LOW
+ )
+ notificationManager.createNotificationChannel(serviceChannel)
+ }
+ }
+
+ private fun createNotification(): Notification {
+ return NotificationCompat.Builder(this, CHANNEL_ID)
+ .setContentTitle("APP Lock by AP")
+ .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 +604,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 +617,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..e6715b2 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(
@@ -29,14 +30,67 @@ object AppLockConstants {
"com.google.android.googlequicksearchbox",
"android",
"com.google.android.gms",
- "com.google.android.webview"
+ "com.google.android.webview",
+ "com.google.android.packageinstaller",
+ "com.android.packageinstaller",
+ "com.samsung.android.incallui",
+ "com.android.incallui",
+ "com.google.android.apps.nexuslauncher",
+ "com.sec.android.app.launcher",
+ "com.miui.home",
+ "com.huawei.android.launcher",
+ "com.oppo.launcher"
)
val ACCESSIBILITY_SETTINGS_CLASSES = setOf(
"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.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 +103,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 {
@@ -67,7 +123,7 @@ object AppLockManager {
// Grace period tracking
private var recentlyLeftApp: String = ""
private var recentlyLeftTime: Long = 0L
- private const val GRACE_PERIOD_MS = 300L
+ private const val GRACE_PERIOD_MS = 2000L // Increased to 2 seconds for better navigation stability
fun setRecentlyLeftApp(packageName: String) {
recentlyLeftApp = packageName
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..9af87f3 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.")
@@ -262,7 +269,7 @@ class ExperimentalAppLockService : Service() {
private fun createNotificationChannel() {
val serviceChannel = NotificationChannel(
CHANNEL_ID,
- "AppLock Service (Usage Stats)",
+ "APP Lock by AP Service (Usage Stats)",
NotificationManager.IMPORTANCE_DEFAULT
)
notificationManager.createNotificationChannel(serviceChannel)
@@ -270,7 +277,7 @@ class ExperimentalAppLockService : Service() {
private fun createNotification(): Notification {
return NotificationCompat.Builder(this, CHANNEL_ID)
- .setContentTitle("App Lock")
+ .setContentTitle("APP Lock by AP")
.setContentText("Protecting your apps")
.setSmallIcon(R.drawable.baseline_shield_24)
.setPriority(NotificationCompat.PRIORITY_MIN)
diff --git a/app/src/main/java/dev/pranav/applock/services/ShizukuAppLockService.kt b/app/src/main/java/dev/pranav/applock/services/ShizukuAppLockService.kt
index 7fd0534..a1cd292 100644
--- a/app/src/main/java/dev/pranav/applock/services/ShizukuAppLockService.kt
+++ b/app/src/main/java/dev/pranav/applock/services/ShizukuAppLockService.kt
@@ -142,7 +142,7 @@ class ShizukuAppLockService : Service() {
private fun createNotificationChannel() {
val serviceChannel = NotificationChannel(
CHANNEL_ID,
- "AppLock Service",
+ "APP Lock by AP Service",
NotificationManager.IMPORTANCE_DEFAULT
)
notificationManager.createNotificationChannel(serviceChannel)
@@ -150,7 +150,7 @@ class ShizukuAppLockService : Service() {
private fun createNotification(): Notification {
return NotificationCompat.Builder(this, CHANNEL_ID)
- .setContentTitle("AppLock")
+ .setContentTitle("APP Lock by AP")
.setContentText("Protecting your apps with Shizuku")
.setSmallIcon(R.drawable.baseline_shield_24)
.setPriority(NotificationCompat.PRIORITY_MIN)
diff --git a/app/src/main/java/dev/pranav/applock/ui/theme/Theme.kt b/app/src/main/java/dev/pranav/applock/ui/theme/Theme.kt
index c795e35..3e0a2f7 100644
--- a/app/src/main/java/dev/pranav/applock/ui/theme/Theme.kt
+++ b/app/src/main/java/dev/pranav/applock/ui/theme/Theme.kt
@@ -10,27 +10,142 @@ import androidx.compose.material3.Shapes
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
-import androidx.compose.material3.expressiveLightColorScheme
+import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
+import dev.pranav.applock.data.repository.AppLockRepository
+import dev.pranav.applock.data.repository.AppThemeMode
+
+// Purple/Lavender Palette
+private val PurplePrimary = Color(0xFF7B50A1)
+private val PurpleOnPrimary = Color(0xFFFFFFFF)
+private val PurplePrimaryContainer = Color(0xFFF3E8FF)
+private val PurpleOnPrimaryContainer = Color(0xFF2D0050)
+private val PurpleSecondary = Color(0xFF655A70)
+private val PurpleOnSecondary = Color(0xFFFFFFFF)
+private val PurpleSecondaryContainer = Color(0xFFEBDFF7)
+private val PurpleOnSecondaryContainer = Color(0xFF20182B)
+private val PurpleBackground = Color(0xFFFFF7FF)
+private val PurpleOnBackground = Color(0xFF1D1B1F)
+private val PurpleSurface = Color(0xFFFFF7FF)
+private val PurpleOnSurface = Color(0xFF1D1B1F)
+
+private val PurplePrimaryDark = Color(0xFFE1B6FF)
+private val PurpleOnPrimaryDark = Color(0xFF491E6F)
+private val PurplePrimaryContainerDark = Color(0xFF613787)
+private val PurpleOnPrimaryContainerDark = Color(0xFFF3E8FF)
+private val PurpleSecondaryDark = Color(0xFFD0C1DA)
+private val PurpleOnSecondaryDark = Color(0xFF362D3F)
+private val PurpleSecondaryContainerDark = Color(0xFF4D4357)
+private val PurpleOnSecondaryContainerDark = Color(0xFFEBDFF7)
+private val PurpleBackgroundDark = Color(0xFF1D1B1F)
+private val PurpleOnBackgroundDark = Color(0xFFE6E1E6)
+private val PurpleSurfaceDark = Color(0xFF1D1B1F)
+private val PurpleOnSurfaceDark = Color(0xFFE6E1E6)
+
+private val CustomLightColorScheme = lightColorScheme(
+ primary = PurplePrimary,
+ onPrimary = PurpleOnPrimary,
+ primaryContainer = PurplePrimaryContainer,
+ onPrimaryContainer = PurpleOnPrimaryContainer,
+ secondary = PurpleSecondary,
+ onSecondary = PurpleOnSecondary,
+ secondaryContainer = PurpleSecondaryContainer,
+ onSecondaryContainer = PurpleOnSecondaryContainer,
+ background = PurpleBackground,
+ onBackground = PurpleOnBackground,
+ surface = PurpleSurface,
+ onSurface = PurpleOnSurface,
+ surfaceContainer = Color(0xFFF7F2FA),
+ surfaceContainerLow = Color(0xFFF3EDF7),
+ surfaceContainerHigh = Color(0xFFECE6F0),
+)
+
+private val CustomDarkColorScheme = darkColorScheme(
+ primary = PurplePrimaryDark,
+ onPrimary = PurpleOnPrimaryDark,
+ primaryContainer = PurplePrimaryContainerDark,
+ onPrimaryContainer = PurpleOnPrimaryContainerDark,
+ secondary = PurpleSecondaryDark,
+ onSecondary = PurpleOnSecondaryDark,
+ secondaryContainer = PurpleSecondaryContainerDark,
+ onSecondaryContainer = PurpleOnSecondaryContainerDark,
+ background = PurpleBackgroundDark,
+ onBackground = PurpleOnBackgroundDark,
+ surface = PurpleSurfaceDark,
+ onSurface = PurpleOnSurfaceDark,
+ surfaceContainer = Color(0xFF211F26),
+ surfaceContainerLow = Color(0xFF1D1B20),
+ surfaceContainerHigh = Color(0xFF2B2930),
+)
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun AppLockTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
- // Dynamic color is available on Android 12+
- dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
+ val context = LocalContext.current
+ val appLockRepository = remember(context) { AppLockRepository(context) }
+
+ val themeMode by appLockRepository.appThemeModeFlow()
+ .collectAsState(initial = appLockRepository.getAppThemeMode())
+ val amoledModeEnabled by appLockRepository.amoledModeFlow()
+ .collectAsState(initial = appLockRepository.isAmoledModeEnabled())
+ val dynamicColorEnabled by appLockRepository.dynamicColorFlow()
+ .collectAsState(initial = appLockRepository.isDynamicColorEnabled())
+
+ val resolvedDarkTheme = when (themeMode) {
+ AppThemeMode.SYSTEM -> darkTheme
+ AppThemeMode.LIGHT -> false
+ AppThemeMode.DARK -> true
+ }
+
val colorScheme = when {
- dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
- val context = LocalContext.current
- if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+ dynamicColorEnabled && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+ val base = if (resolvedDarkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+ if (resolvedDarkTheme && amoledModeEnabled) {
+ base.copy(
+ background = Color.Black,
+ surface = Color.Black,
+ surfaceVariant = Color.Black,
+ surfaceContainer = Color.Black,
+ surfaceContainerLow = Color.Black,
+ surfaceContainerHigh = Color(0xFF121212),
+ surfaceContainerLowest = Color.Black,
+ surfaceContainerHighest = Color(0xFF1A1A1A),
+ )
+ } else base
+ }
+
+ resolvedDarkTheme -> {
+ if (amoledModeEnabled) {
+ CustomDarkColorScheme.copy(
+ background = Color.Black,
+ surface = Color.Black,
+ surfaceVariant = Color.Black,
+ surfaceContainer = Color.Black,
+ surfaceContainerLow = Color.Black,
+ surfaceContainerHigh = Color(0xFF121212), // Slightly lighter for cards in amoled
+ surfaceContainerLowest = Color.Black,
+ surfaceContainerHighest = Color(0xFF1A1A1A),
+ onBackground = Color.White,
+ onSurface = Color.White,
+ onSurfaceVariant = Color(0xFFE0E0E0),
+ secondaryContainer = Color(0xFF121212),
+ onSecondaryContainer = Color.White,
+ )
+ } else {
+ CustomDarkColorScheme
+ }
}
- darkTheme -> darkColorScheme()
- else -> expressiveLightColorScheme()
+ else -> CustomLightColorScheme
}
val shapes = Shapes(largeIncreased = RoundedCornerShape(36.0.dp))
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
index 3ba4e35..65291b9 100644
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -1,5 +1,6 @@
-
-
-
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
index 3ba4e35..65291b9 100644
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -1,5 +1,6 @@
-
-
-
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..208569c
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
index ea86d36..be2edde 100644
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
index 2725374..8b1a3bb 100644
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..dff7d28
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
index dd1c2f2..d8ffd32 100644
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
index 8e420db..fd3ea8f 100644
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..0e230dc
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
index 8f229aa..69e2951 100644
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
index a058183..dafa162 100644
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..4ec6f9c
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
index f4ab3ca..7d185a7 100644
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
index 0771c88..3a71eec 100644
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..df11eb0
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
index 5eeec77..4fb732e 100644
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
index ad5db81..2ea1984 100644
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 0000000..443b48b
--- /dev/null
+++ b/app/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #FCFCFC
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 87a4760..59180f7 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,31 +1,31 @@
- App Lock
+ APP Lock by AP
Prevents unauthorized people from uninstalling the app.
- Allows App Lock to detect when protected apps are opened and show password verification.
+ Allows APP Lock by AP to detect when protected apps are opened and show password verification.
Enter your password to disable admin permission.
Incorrect PIN. Please try again.
Password verified, you can now disable admin permission
Choose App Detection Method
- Select how you want AppLock to detect when protected apps are launched.
+ Select how you want APP Lock by AP to detect when protected apps are launched.
Notification Permission
- AppLock needs permission to show notifications to keep you informed and keep running in background properly. Tap \'Next\' to grant permission.
+ APP Lock by AP needs permission to show notifications to keep you informed and keep running in background properly. Tap \'Next\' to grant permission.
Notification permission is automatically granted on your Android version.
- Welcome to AppLock
- Protect your apps and privacy with AppLock. We\'ll guide you through a quick setup.
+ Welcome to APP Lock by AP
+ Protect your apps and privacy with APP Lock by AP. We\'ll guide you through a quick setup.
Secure Your Apps
Keep your private apps protected with advanced locking mechanisms
Display Over Other Apps
- AppLock needs permission to display over other apps to show the lock screen. Tap \'Next\' and enable the permission.
+ APP Lock by AP needs permission to display over other apps to show the lock screen. Tap \'Next\' and enable the permission.
- Notification permission is required for AppLock to function properly.
+ Notification permission is required for APP Lock by AP to function properly.
Shizuku permission granted
Shizuku permission is required for advanced features.
- Please allow AppLock to display over other apps.
+ Please allow APP Lock by AP to display over other apps.
Disable Battery Optimization
- To ensure AppLock runs reliably in the background, please disable battery optimizations for the app. Tap \'Next\' to open settings.
+ To ensure APP Lock by AP runs reliably in the background, please disable battery optimizations for the app. Tap \'Next\' to open settings.
Accessibility Service
Standard method that works on most devices. Requires accessibility permission. May have slight delays on some OEMs.
Usage Stats
@@ -34,9 +34,9 @@
Advanced method with better performance and superior experience. Requires Shizuku app installed and enabled via ADB.
You can change this later in settings
-Accessibility service is required for AppLock to function properly.
+Accessibility service is required for APP Lock by AP to function properly.
-If you get the message \"Restricted Setting\", please manually go to Settings > Apps > App Lock > Upper Right menu, and press \"Allow restricted settings\".
+If you get the message \"Restricted Setting\", please manually go to Settings > Apps > APP Lock by AP > Upper Right menu, and press \"Allow restricted settings\".
Tap \'Next\' to enable it.
@@ -44,7 +44,7 @@ Tap \'Next\' to enable it.
This permission is required to detect when locked apps are launched.
-If you get the message \"Restricted Setting\", please manually go to Settings > Apps > App Lock > Upper Right menu, and press \"Allow restricted settings\".
+If you get the message \"Restricted Setting\", please manually go to Settings > Apps > APP Lock by AP > Upper Right menu, and press \"Allow restricted settings\".
Tap \'Next\' to enable it.
@@ -53,7 +53,7 @@ Shizuku provides advanced features like locking system apps and more.
Make sure you have Shizuku installed and enabled via ADB. Tap \'Next\' to grant permission.
Complete Privacy
- Your data never leaves your device. AppLock protects your privacy at all times.
+ Your data never leaves your device. APP Lock by AP protects your privacy at all times.
All permissions are required to proceed.
Next
Get Started
@@ -98,7 +98,7 @@ Make sure you have Shizuku installed and enabled via ADB. Tap \'Next\' to grant
Please grant the app permission to draw overlays in settings. This is required to allow app to show lock screen.
Open Settings
Please grant Shizuku permission manually
- App Lock requires Device Admin permission to prevent uninstallation. It will not affect your device\'s functionality.
+ APP Lock by AP requires Device Admin permission to prevent uninstallation. It will not affect your device\'s functionality.
Join the Community
Join our discord community for updates and support.
Join Discord
@@ -112,25 +112,39 @@ Make sure you have Shizuku installed and enabled via ADB. Tap \'Next\' to grant
Search
No apps found
Try adjusting your search
+ App Protection
+ Are you sure you want to turn ON app protection?
+ Are you sure you want to turn OFF app protection?
Settings
Back
Support Development
- Hi, I\'m Pranav, the developer of App Lock. I\'m a student developer passionate about creating useful apps. If you find it helpful, please consider supporting its development with a small donation. Any amount is greatly appreciated and helps me continue to improve the app and work on new features. Thank you for your support!
+ Hi, I\'m Pranav, the developer of APP Lock by AP. I\'m a student developer passionate about creating useful apps. If you find it helpful, please consider supporting its development with a small donation. Any amount is greatly appreciated and helps me continue to improve the app and work on new features. Thank you for your support!
Donate
Permission Required
- To enable Anti-Uninstall, App Lock requires two permissions: Device Administrator and Accessibility Service. These are necessary to prevent unauthorized uninstallation and ensure robust protection. Would you like to proceed with granting these permissions?
+ To enable Anti-Uninstall, APP Lock by AP requires two permissions: Device Administrator and Accessibility Service. These are necessary to prevent unauthorized uninstallation and ensure robust protection. Would you like to proceed with granting these permissions?
Proceed
Device Administrator Permission
- App Lock needs Device Administrator permission to prevent unauthorized uninstallation. Please grant this permission in the next screen. This will not affect your device\'s normal functionality.
+ APP Lock by AP needs Device Administrator permission to prevent unauthorized uninstallation. Please grant this permission in the next screen. This will not affect your device\'s normal functionality.
Grant Permission
Accessibility Service Permission
- App Lock also needs Accessibility Service permission to monitor app uninstallation attempts and re-activate Device Administrator if disabled. This ensures the Anti-Uninstall feature remains effective. Please enable the Accessibility Service for App Lock in the settings.
+ APP Lock by AP also needs Accessibility Service permission to monitor app uninstallation attempts and re-activate Device Administrator if disabled. This ensures the Anti-Uninstall feature remains effective. Please enable the Accessibility Service for APP Lock by AP in the settings.
Open Settings
+ App Theme
+ System
+ Light
+ Dark
+ Developer Info
+ Developer Name: AP
+ Email: ap0803apap@gmail.com
+ Source Code
+ Report Issue
Lock Screen Customization
Maximum Brightness
Display lock screen at maximum brightness for clarity
+ AMOLED Mode
+ When off (default), dynamic colors are used in lock screen customization. When on, dark mode primary color becomes AMOLED black across the app.
Biometric Authentication
Use fingerprint or face unlock if available
Biometric authentication is not available on this device.
@@ -152,11 +166,11 @@ Make sure you have Shizuku installed and enabled via ADB. Tap \'Next\' to grant
Change PIN
Update your current PIN
Anti-Uninstall Protection
- Prevent unauthorized uninstallation of App Lock
+ Prevent unauthorized uninstallation of APP Lock by AP
Haptic Feedback
Disable haptic feedback on key presses
Backend Implementation
- Select the method AppLock uses to detect foreground apps
+ Select the method APP Lock by AP uses to detect foreground apps
Accessibility Service
Standard method, may have slight delays on some OEMs.
Usage Stats
@@ -174,6 +188,10 @@ Make sure you have Shizuku installed and enabled via ADB. Tap \'Next\' to grant
Support Development
Help support the development of this app
+ App Logging
+ Enable internal logging for debugging purposes
+ Links & Social
+
Please allow our app to ignore battery optimizations.
Could not find specific settings. Please remove app from battery restrictions.
@@ -205,7 +223,7 @@ Make sure you have Shizuku installed and enabled via ADB. Tap \'Next\' to grant
Enable Accessibility Service
- App Lock needs accessibility permission to detect when protected apps are launched.
+ APP Lock by AP needs accessibility permission to detect when protected apps are launched.
Please follow these steps:
1. Tap \'Open Settings\' below\n
@@ -234,20 +252,20 @@ Make sure you have Shizuku installed and enabled via ADB. Tap \'Next\' to grant
%1$d minute
1 hour
To enable Anti-Uninstall
- protection, App Lock needs two permissions:
+ protection, APP Lock by AP needs two permissions:
1. Device Administrator -
Prevents uninstallation\n2. Accessibility Service - Monitors app usage\n\nThese permissions
help protect the app from being removed without your knowledge.
- App Lock needs Device Administrator
+ APP Lock by AP needs Device Administrator
permission to prevent uninstallation.
This permission allows the app to:\n•
Prevent itself from being uninstalled\n• Protect your app lock settings\n\nIt will not
affect your device\'s normal functionality.
- App Lock needs Accessibility Service permission to monitor app usage.
+ APP Lock by AP needs Accessibility Service permission to monitor app usage.
This permission allows the app to:\n•
Detect when apps are opened\n• Show lock screen when needed\n• Provide seamless app
@@ -285,7 +303,7 @@ Make sure you have Shizuku installed and enabled via ADB. Tap \'Next\' to grant
Shizuku Permission
Securely detects when protected apps are launched.
Device Admin
- Prevents AppLock from being uninstalled.
+ Prevents APP Lock by AP from being uninstalled.
Required
Join Community
@@ -294,12 +312,12 @@ Make sure you have Shizuku installed and enabled via ADB. Tap \'Next\' to grant
Maybe Later
Donate
- Keep AppLock free and ad-free forever
+ Keep APP Lock by AP free and ad-free forever
Support Development
- Hi, I’m Pranav. I’m a student developer building AppLock in my spare time to keep it private and ad-free for everyone.
+ Hi, I’m Pranav. I’m a student developer building APP Lock by AP in my spare time to keep it private and ad-free for everyone.
Juggling this project with my studies is a challenge, but your support makes it possible.
- If AppLock has helped you, please consider a small donation—any amount means the world to me and keeps this project alive.
+ If APP Lock by AP has helped you, please consider a small donation—any amount means the world to me and keeps this project alive.
Thank you for believing in my work!
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 @@
-
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 8d533d6..83e9f4e 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,5 +1,5 @@
[versions]
-agp = "9.0.1"
+agp = "9.1.0"
annotation = "1.9.1"
refine = "4.4.0"
hiddenapibypass = "6.1"
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index f70ea9e..8f63698 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
#Sat Aug 02 11:42:34 IST 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/settings.gradle.kts b/settings.gradle.kts
index a456f82..1e9637d 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -16,10 +16,11 @@ dependencyResolutionManagement {
repositories {
google()
mavenCentral()
+ maven { url = uri("https://jitpack.io") }
}
}
-rootProject.name = "App Lock"
+rootProject.name = "APP Lock by AP"
include(":app")
include(":appintro")
include(":hidden-api")