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..36784c6 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
@@ -20,8 +20,10 @@ 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.lockscreen.ui.PasswordOverlayScreen
+import dev.pranav.applock.features.lockscreen.ui.AlphanumericPasswordOverlayScreen
+import dev.pranav.applock.features.lockscreen.ui.PinPasswordOverlayScreen
import dev.pranav.applock.features.lockscreen.ui.PatternLockScreen
+import dev.pranav.applock.features.setpassword.ui.AlphanumericSetPasswordScreen
import dev.pranav.applock.features.setpassword.ui.PatternSetPasswordScreen
import dev.pranav.applock.features.setpassword.ui.SetPasswordScreen
import dev.pranav.applock.features.settings.ui.SettingsScreen
@@ -52,10 +54,16 @@ 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 -> {
+ AlphanumericSetPasswordScreen(navController, false)
+ }
+ else -> {
+ SetPasswordScreen(navController, isFirstTimeSetup = false)
+ }
}
}
@@ -63,6 +71,10 @@ fun AppNavHost(navController: NavHostController, startDestination: String) {
PatternSetPasswordScreen(navController, isFirstTimeSetup = true)
}
+ composable(Screen.SetPasswordAlphanumeric.route) {
+ AlphanumericSetPasswordScreen(navController, isFirstTimeSetup = true)
+ }
+
composable(Screen.Main.route) {
MainScreen(navController)
}
@@ -88,8 +100,21 @@ fun AppNavHost(navController: NavHostController, startDestination: String) {
)
}
+ PreferencesRepository.LOCK_TYPE_PASSWORD -> {
+ AlphanumericPasswordOverlayScreen(
+ showBiometricButton = application.appLockRepository.isBiometricAuthEnabled(),
+ fromMainActivity = true,
+ onBiometricAuth = {
+ handleBiometricAuthentication(context, navController)
+ },
+ onAuthSuccess = {
+ handleAuthenticationSuccess(navController)
+ }
+ )
+ }
+
else -> {
- PasswordOverlayScreen(
+ PinPasswordOverlayScreen(
showBiometricButton = application.appLockRepository.isBiometricAuthEnabled(),
fromMainActivity = true,
onBiometricAuth = {
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..09cb474 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,6 +4,7 @@ sealed class Screen(val route: String) {
object AppIntro : Screen("app_intro")
object SetPassword : Screen("set_password")
object SetPasswordPattern : Screen("set_password_pattern")
+ object SetPasswordAlphanumeric : Screen("set_password_alphanumeric")
object ChangePassword : Screen("change_password")
object Main : Screen("main")
object PasswordOverlay : Screen("password_overlay")
diff --git a/app/src/main/java/dev/pranav/applock/core/utils/SecurityUtils.kt b/app/src/main/java/dev/pranav/applock/core/utils/SecurityUtils.kt
new file mode 100644
index 0000000..b68f170
--- /dev/null
+++ b/app/src/main/java/dev/pranav/applock/core/utils/SecurityUtils.kt
@@ -0,0 +1,69 @@
+package dev.pranav.applock.core.utils
+
+import android.util.Base64
+import java.security.MessageDigest
+import java.security.SecureRandom
+
+object SecurityUtils {
+
+ private const val HASH_ALGORITHM = "SHA-256"
+ private const val SALT_LENGTH = 16
+ private const val MAX_PASSWORD_LENGTH = 64
+
+ /**
+ * Sanitizes the input string by filtering out control characters and null bytes.
+ * Also limits the length to [MAX_PASSWORD_LENGTH].
+ */
+ fun sanitizePassword(input: String): String {
+ return input.filter { it.code >= 32 && it.code != 127 }
+ .take(MAX_PASSWORD_LENGTH)
+ }
+
+ /**
+ * Generates a random cryptographic salt.
+ */
+ fun generateSalt(): ByteArray {
+ val random = SecureRandom()
+ val salt = ByteArray(SALT_LENGTH)
+ random.nextBytes(salt)
+ return salt
+ }
+
+ /**
+ * Hashes the password using SHA-256 with the provided salt.
+ * Returns a Base64 encoded string containing the salt and the hash.
+ * Format: salt:hash
+ */
+ fun hashPassword(password: String, salt: ByteArray): String {
+ val md = MessageDigest.getInstance(HASH_ALGORITHM)
+ md.update(salt)
+ val hashedPassword = md.digest(password.toByteArray(Charsets.UTF_8))
+
+ val saltBase64 = Base64.encodeToString(salt, Base64.NO_WRAP)
+ val hashBase64 = Base64.encodeToString(hashedPassword, Base64.NO_WRAP)
+
+ return "$saltBase64:$hashBase64"
+ }
+
+ /**
+ * Verifies an input password against a stored salted hash string.
+ */
+ fun verifyPassword(inputPassword: String, storedSaltedHash: String): Boolean {
+ return try {
+ val parts = storedSaltedHash.split(":")
+ if (parts.size != 2) return false
+
+ val salt = Base64.decode(parts[0], Base64.DEFAULT)
+ val storedHash = parts[1]
+
+ val md = MessageDigest.getInstance(HASH_ALGORITHM)
+ md.update(salt)
+ val inputHashed = md.digest(inputPassword.toByteArray(Charsets.UTF_8))
+ val inputHashBase64 = Base64.encodeToString(inputHashed, Base64.NO_WRAP)
+
+ inputHashBase64 == storedHash
+ } catch (e: Exception) {
+ false
+ }
+ }
+}
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..73c3edc 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
@@ -1,5 +1,6 @@
package dev.pranav.applock.data.repository
+import dev.pranav.applock.core.utils.SecurityUtils
import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
@@ -17,7 +18,9 @@ class PreferencesRepository(context: Context) {
context.getSharedPreferences(PREFS_NAME_SETTINGS, Context.MODE_PRIVATE)
fun setPassword(password: String) {
- appLockPrefs.edit { putString(KEY_PASSWORD, password) }
+ val salt = SecurityUtils.generateSalt()
+ val saltedHash = SecurityUtils.hashPassword(password, salt)
+ appLockPrefs.edit(commit = true) { putString(KEY_PASSWORD, saltedHash) }
}
fun getPassword(): String? {
@@ -25,12 +28,17 @@ class PreferencesRepository(context: Context) {
}
fun validatePassword(inputPassword: String): Boolean {
- val storedPassword = getPassword()
- return storedPassword != null && inputPassword == storedPassword
+ val storedSaltedHash = getPassword() ?: return false
+
+ // If it's a legacy plain text password (doesn't contain ':'), migrate it or validate as is
+ // For simplicity and since this is a new feature, we assume all passwords should be hashed.
+ // If there's an existing plain text password, this will fail validation and require reset.
+
+ return SecurityUtils.verifyPassword(inputPassword, storedSaltedHash)
}
fun setPattern(pattern: String) {
- appLockPrefs.edit { putString(KEY_PATTERN, pattern) }
+ appLockPrefs.edit(commit = true) { putString(KEY_PATTERN, pattern) }
}
fun getPattern(): String? {
@@ -43,7 +51,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 {
@@ -180,5 +188,6 @@ class PreferencesRepository(context: Context) {
const val LOCK_TYPE_PIN = "pin"
const val LOCK_TYPE_PATTERN = "pattern"
+ const val LOCK_TYPE_PASSWORD = "password"
}
}
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..16e9cc7 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
@@ -7,26 +7,27 @@ import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedCallback
import androidx.activity.compose.setContent
-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.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Visibility
+import androidx.compose.material.icons.filled.VisibilityOff
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import dev.pranav.applock.R
import dev.pranav.applock.core.broadcast.DeviceAdmin
+import dev.pranav.applock.core.utils.SecurityUtils
import dev.pranav.applock.core.utils.appLockRepository
import dev.pranav.applock.data.repository.AppLockRepository
import dev.pranav.applock.data.repository.PreferencesRepository
@@ -99,6 +100,44 @@ class AdminDisableActivity : ComponentActivity() {
)
}
+ PreferencesRepository.LOCK_TYPE_PASSWORD -> {
+ AdminDisablePasswordScreen(
+ modifier = Modifier.padding(padding),
+ onPasswordVerified = {
+ val deviceAdmin = DeviceAdmin()
+ deviceAdmin.setPasswordVerified(this@AdminDisableActivity, true)
+
+ Toast.makeText(
+ this@AdminDisableActivity,
+ R.string.password_verified_admin,
+ Toast.LENGTH_SHORT
+ ).show()
+ appLockRepository.setAntiUninstallEnabled(false)
+ finish()
+ },
+ onCancel = {
+ val deviceAdmin = DeviceAdmin()
+ deviceAdmin.setPasswordVerified(
+ this@AdminDisableActivity,
+ false
+ )
+ finish()
+ },
+ validatePassword = { inputPassword ->
+ appLockRepository.validatePassword(inputPassword)
+ .also { isValid ->
+ if (!isValid) {
+ Toast.makeText(
+ this@AdminDisableActivity,
+ R.string.incorrect_password_try_again,
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+ }
+ )
+ }
+
else -> {
AdminDisableScreen(
modifier = Modifier.padding(padding),
@@ -212,6 +251,107 @@ fun AdminDisableScreen(
}
}
+@Composable
+fun AdminDisablePasswordScreen(
+ modifier: Modifier = Modifier,
+ onPasswordVerified: () -> Unit,
+ onCancel: () -> Unit,
+ validatePassword: (String) -> Boolean
+) {
+ Surface(
+ modifier = modifier.fillMaxSize(),
+ color = MaterialTheme.colorScheme.background
+ ) {
+ var passwordState by remember { mutableStateOf("") }
+ var showError by remember { mutableStateOf(false) }
+ var passwordVisible by remember { mutableStateOf(false) }
+ val focusRequester = remember { FocusRequester() }
+
+ LaunchedEffect(Unit) {
+ focusRequester.requestFocus()
+ }
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(24.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Text(
+ text = stringResource(R.string.unlock_to_disable_admin),
+ style = MaterialTheme.typography.titleMedium,
+ textAlign = TextAlign.Center
+ )
+
+ Spacer(modifier = Modifier.height(32.dp))
+
+ OutlinedTextField(
+ value = passwordState,
+ onValueChange = { input ->
+ passwordState = SecurityUtils.sanitizePassword(input)
+ showError = false
+ },
+ modifier = Modifier.fillMaxWidth().focusRequester(focusRequester),
+ label = { Text(stringResource(R.string.password_hint)) },
+ visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
+ keyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Password,
+ imeAction = ImeAction.Done
+ ),
+ trailingIcon = {
+ val image = if (passwordVisible)
+ Icons.Filled.Visibility
+ else Icons.Filled.VisibilityOff
+
+ IconButton(onClick = { passwordVisible = !passwordVisible }) {
+ Icon(imageVector = image, contentDescription = null)
+ }
+ },
+ isError = showError,
+ singleLine = true
+ )
+
+ if (showError) {
+ Text(
+ text = stringResource(R.string.incorrect_password_try_again),
+ color = MaterialTheme.colorScheme.error,
+ style = MaterialTheme.typography.labelMedium,
+ modifier = Modifier.padding(top = 4.dp).align(Alignment.Start)
+ )
+ }
+
+ Spacer(modifier = Modifier.height(32.dp))
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ TextButton(
+ onClick = onCancel,
+ modifier = Modifier.weight(1f)
+ ) {
+ Text(stringResource(R.string.cancel_button))
+ }
+
+ Button(
+ onClick = {
+ if (validatePassword(passwordState)) {
+ onPasswordVerified()
+ } else {
+ showError = true
+ passwordState = ""
+ }
+ },
+ modifier = Modifier.weight(1f)
+ ) {
+ Text(stringResource(R.string.verify_button))
+ }
+ }
+ }
+ }
+}
+
@Composable
fun AdminDisablePatternScreen(
modifier: Modifier = Modifier,
@@ -253,6 +393,15 @@ fun AdminDisablePatternScreen(
isValid
}
)
+
+ Spacer(modifier = Modifier.height(24.dp))
+
+ TextButton(
+ onClick = onCancel,
+ modifier = Modifier.padding(bottom = 16.dp)
+ ) {
+ Text(stringResource(R.string.cancel_button))
+ }
}
}
}
diff --git a/app/src/main/java/dev/pranav/applock/features/lockscreen/ui/AlphanumericPasswordOverlayScreen.kt b/app/src/main/java/dev/pranav/applock/features/lockscreen/ui/AlphanumericPasswordOverlayScreen.kt
new file mode 100644
index 0000000..73190c4
--- /dev/null
+++ b/app/src/main/java/dev/pranav/applock/features/lockscreen/ui/AlphanumericPasswordOverlayScreen.kt
@@ -0,0 +1,224 @@
+package dev.pranav.applock.features.lockscreen.ui
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material.icons.filled.Visibility
+import androidx.compose.material.icons.filled.VisibilityOff
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.text.input.VisualTransformation
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import dev.pranav.applock.R
+import dev.pranav.applock.core.utils.SecurityUtils
+import dev.pranav.applock.core.utils.appLockRepository
+import dev.pranav.applock.ui.icons.Fingerprint
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Composable
+fun AlphanumericPasswordOverlayScreen(
+ modifier: Modifier = Modifier,
+ showBiometricButton: Boolean = false,
+ fromMainActivity: Boolean = false,
+ showCloseButton: Boolean = false,
+ onClose: () -> Unit = {},
+ onBiometricAuth: () -> Unit = {},
+ onAuthSuccess: () -> Unit,
+ lockedAppName: String? = null,
+ triggeringPackageName: String? = null,
+ onPasswordAttempt: ((password: String) -> Boolean)? = null
+) {
+ val appLockRepository = LocalContext.current.appLockRepository()
+ var passwordState by remember { mutableStateOf("") }
+ var showError by remember { mutableStateOf(false) }
+ var passwordVisible by remember { mutableStateOf(false) }
+
+ val focusRequester = remember { FocusRequester() }
+
+ LaunchedEffect(Unit) {
+ focusRequester.requestFocus()
+ }
+
+ val minLength = 4
+
+ Surface(
+ modifier = modifier.fillMaxSize(),
+ color = MaterialTheme.colorScheme.surfaceContainer
+ ) {
+ Box(modifier = Modifier.fillMaxSize()) {
+ if (showCloseButton) {
+ IconButton(
+ onClick = onClose,
+ modifier = Modifier
+ .statusBarsPadding()
+ .padding(start = 8.dp, top = 8.dp)
+ .align(Alignment.TopStart)
+ ) {
+ Icon(
+ imageVector = Icons.Default.Close,
+ contentDescription = "Close",
+ tint = MaterialTheme.colorScheme.onSurface
+ )
+ }
+ }
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(24.dp)
+ .statusBarsPadding()
+ .navigationBarsPadding(),
+ 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.headlineMediumEmphasized,
+ textAlign = TextAlign.Center
+ )
+
+ Spacer(modifier = Modifier.height(32.dp))
+
+ OutlinedTextField(
+ value = passwordState,
+ onValueChange = { input ->
+ passwordState = SecurityUtils.sanitizePassword(input)
+ showError = false
+ },
+ modifier = Modifier.fillMaxWidth().focusRequester(focusRequester),
+ label = { Text(stringResource(R.string.password_hint)) },
+ visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
+ keyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Password,
+ imeAction = ImeAction.Done
+ ),
+ keyboardActions = KeyboardActions(
+ onDone = {
+ if (passwordState.length >= minLength) {
+ performVerification(
+ passwordState,
+ fromMainActivity,
+ appLockRepository,
+ onAuthSuccess,
+ onPasswordAttempt,
+ onIncorrect = {
+ passwordState = ""
+ showError = true
+ }
+ )
+ }
+ }
+ ),
+ trailingIcon = {
+ val image = if (passwordVisible)
+ Icons.Filled.Visibility
+ else Icons.Filled.VisibilityOff
+
+ IconButton(onClick = { passwordVisible = !passwordVisible }) {
+ Icon(imageVector = image, contentDescription = null)
+ }
+ },
+ isError = showError,
+ singleLine = true
+ )
+
+ if (showError) {
+ Text(
+ text = stringResource(R.string.incorrect_password_try_again),
+ color = MaterialTheme.colorScheme.error,
+ style = MaterialTheme.typography.labelMedium,
+ modifier = Modifier.padding(top = 4.dp).align(Alignment.Start)
+ )
+ }
+
+ Spacer(modifier = Modifier.height(32.dp))
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ if (showBiometricButton) {
+ FilledTonalIconButton(
+ onClick = onBiometricAuth,
+ modifier = Modifier.size(56.dp),
+ shape = MaterialTheme.shapes.medium,
+ ) {
+ Icon(
+ imageVector = Fingerprint,
+ modifier = Modifier.size(24.dp),
+ contentDescription = stringResource(R.string.biometric_authentication_cd),
+ tint = MaterialTheme.colorScheme.primary
+ )
+ }
+ }
+
+ Button(
+ onClick = {
+ if (passwordState.length >= minLength) {
+ performVerification(
+ passwordState,
+ fromMainActivity,
+ appLockRepository,
+ onAuthSuccess,
+ onPasswordAttempt,
+ onIncorrect = {
+ passwordState = ""
+ showError = true
+ }
+ )
+ }
+ },
+ modifier = Modifier.weight(1f),
+ shape = MaterialTheme.shapes.medium
+ ) {
+ Text(stringResource(R.string.verify_button))
+ }
+ }
+ }
+ }
+ }
+
+ BackHandler { }
+}
+
+private fun performVerification(
+ passwordState: String,
+ fromMainActivity: Boolean,
+ appLockRepository: dev.pranav.applock.data.repository.AppLockRepository,
+ onAuthSuccess: () -> Unit,
+ onPasswordAttempt: ((password: String) -> Boolean)?,
+ onIncorrect: () -> Unit
+) {
+ if (fromMainActivity) {
+ if (appLockRepository.validatePassword(passwordState)) {
+ onAuthSuccess()
+ } else {
+ onIncorrect()
+ }
+ } else {
+ onPasswordAttempt?.let { attempt ->
+ if (attempt(passwordState)) {
+ onAuthSuccess()
+ } else {
+ onIncorrect()
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/dev/pranav/applock/features/lockscreen/ui/LockScreenOverlayManager.kt b/app/src/main/java/dev/pranav/applock/features/lockscreen/ui/LockScreenOverlayManager.kt
index f473293..5667f49 100644
--- a/app/src/main/java/dev/pranav/applock/features/lockscreen/ui/LockScreenOverlayManager.kt
+++ b/app/src/main/java/dev/pranav/applock/features/lockscreen/ui/LockScreenOverlayManager.kt
@@ -12,6 +12,8 @@ import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.*
+import dev.pranav.applock.features.lockscreen.ui.AlphanumericPasswordOverlayScreen
+import dev.pranav.applock.features.lockscreen.ui.PinPasswordOverlayScreen
import androidx.savedstate.SavedStateRegistry
import androidx.savedstate.SavedStateRegistryController
import androidx.savedstate.SavedStateRegistryOwner
@@ -99,46 +101,80 @@ class LockScreenOverlayManager(private val context: Context):
val lockType = appLockRepository.getLockType()
- if (lockType == PreferencesRepository.LOCK_TYPE_PATTERN) {
- PatternLockScreen(
- fromMainActivity = false,
- showCloseButton = true,
- onClose = {
- onExit()
- removeOverlay()
- },
- lockedAppName = appName,
- triggeringPackageName = triggeringPackageName,
- onPatternAttempt = onPatternAttemptCallback
- )
- } else {
- PasswordOverlayScreen(
- showBiometricButton = appLockRepository.isBiometricAuthEnabled(),
- fromMainActivity = false,
- showCloseButton = true,
- onClose = {
- onExit()
- removeOverlay()
- },
- lockedAppName = appName,
- triggeringPackageName = triggeringPackageName,
- onAuthSuccess = {
- onUnlock()
- removeOverlay()
- },
- onBiometricAuth = {
- val intent = Intent(
- context,
- TransparentBiometricActivity::class.java
- ).apply {
- flags =
- Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_ANIMATION
- putExtra("locked_package", lockedPackageName)
- }
- context.startActivity(intent)
- },
- onPinAttempt = onPinAttemptCallback
- )
+ when (lockType) {
+ PreferencesRepository.LOCK_TYPE_PATTERN -> {
+ PatternLockScreen(
+ fromMainActivity = false,
+ showCloseButton = true,
+ onClose = {
+ onExit()
+ removeOverlay()
+ },
+ lockedAppName = appName,
+ triggeringPackageName = triggeringPackageName,
+ onPatternAttempt = onPatternAttemptCallback
+ )
+ }
+
+ PreferencesRepository.LOCK_TYPE_PASSWORD -> {
+ AlphanumericPasswordOverlayScreen(
+ showBiometricButton = appLockRepository.isBiometricAuthEnabled(),
+ fromMainActivity = false,
+ showCloseButton = true,
+ onClose = {
+ onExit()
+ removeOverlay()
+ },
+ lockedAppName = appName,
+ triggeringPackageName = triggeringPackageName,
+ onAuthSuccess = {
+ onUnlock()
+ removeOverlay()
+ },
+ onBiometricAuth = {
+ val intent = Intent(
+ context,
+ TransparentBiometricActivity::class.java
+ ).apply {
+ flags =
+ Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_ANIMATION
+ putExtra("locked_package", lockedPackageName)
+ }
+ context.startActivity(intent)
+ },
+ onPasswordAttempt = onPinAttemptCallback
+ )
+ }
+
+ else -> {
+ PinPasswordOverlayScreen(
+ showBiometricButton = appLockRepository.isBiometricAuthEnabled(),
+ fromMainActivity = false,
+ showCloseButton = true,
+ onClose = {
+ onExit()
+ removeOverlay()
+ },
+ lockedAppName = appName,
+ triggeringPackageName = triggeringPackageName,
+ onAuthSuccess = {
+ onUnlock()
+ removeOverlay()
+ },
+ onBiometricAuth = {
+ val intent = Intent(
+ context,
+ TransparentBiometricActivity::class.java
+ ).apply {
+ flags =
+ Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_ANIMATION
+ putExtra("locked_package", lockedPackageName)
+ }
+ context.startActivity(intent)
+ },
+ onPinAttempt = onPinAttemptCallback
+ )
+ }
}
}
}
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 7f5f5ae..c485bd3 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,5 +1,6 @@
package dev.pranav.applock.features.lockscreen.ui
+import dev.pranav.applock.features.lockscreen.ui.AlphanumericPasswordOverlayScreen
import android.content.Context
import android.content.res.Configuration
import android.os.Build
@@ -84,7 +85,7 @@ class PasswordOverlayActivity: FragmentActivity() {
enableEdgeToEdge()
- appLockRepository = AppLockRepository(applicationContext)
+ appLockRepository = applicationContext.appLockRepository()
onBackPressedDispatcher.addCallback(
this,
@@ -193,7 +194,7 @@ class PasswordOverlayActivity: FragmentActivity() {
modifier = Modifier.fillMaxSize(),
contentColor = MaterialTheme.colorScheme.primaryContainer
) { innerPadding ->
- val lockType = remember { appLockRepository.getLockType() }
+ val lockType = appLockRepository.getLockType()
when (lockType) {
PreferencesRepository.LOCK_TYPE_PATTERN -> {
PatternLockScreen(
@@ -205,8 +206,23 @@ class PasswordOverlayActivity: FragmentActivity() {
)
}
+ PreferencesRepository.LOCK_TYPE_PASSWORD -> {
+ AlphanumericPasswordOverlayScreen(
+ modifier = Modifier.padding(innerPadding),
+ showBiometricButton = appLockRepository.isBiometricAuthEnabled(),
+ fromMainActivity = false,
+ onBiometricAuth = { triggerBiometricPrompt() },
+ onAuthSuccess = {},
+ lockedAppName = appName,
+ triggeringPackageName = triggeringPackageNameFromIntent,
+ onPasswordAttempt = onPinAttemptCallback,
+ showCloseButton = true,
+ onClose = { finish() }
+ )
+ }
+
else -> {
- PasswordOverlayScreen(
+ PinPasswordOverlayScreen(
modifier = Modifier.padding(innerPadding),
showBiometricButton = appLockRepository.isBiometricAuthEnabled(),
fromMainActivity = false,
@@ -335,7 +351,7 @@ class PasswordOverlayActivity: FragmentActivity() {
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalAnimationApi::class)
@Composable
-fun PasswordOverlayScreen(
+fun PinPasswordOverlayScreen(
modifier: Modifier = Modifier,
showBiometricButton: Boolean = false,
fromMainActivity: Boolean = false,
diff --git a/app/src/main/java/dev/pranav/applock/features/setpassword/ui/AlphanumericSetPasswordScreen.kt b/app/src/main/java/dev/pranav/applock/features/setpassword/ui/AlphanumericSetPasswordScreen.kt
new file mode 100644
index 0000000..51a1350
--- /dev/null
+++ b/app/src/main/java/dev/pranav/applock/features/setpassword/ui/AlphanumericSetPasswordScreen.kt
@@ -0,0 +1,343 @@
+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.*
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Visibility
+import androidx.compose.material.icons.filled.VisibilityOff
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.text.input.VisualTransformation
+import androidx.compose.ui.text.style.TextAlign
+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.SecurityUtils
+import dev.pranav.applock.data.repository.PreferencesRepository
+
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
+@Composable
+fun AlphanumericSetPasswordScreen(
+ navController: NavController,
+ isFirstTimeSetup: Boolean
+) {
+ var passwordState by remember { mutableStateOf("") }
+ var confirmPasswordState by remember { mutableStateOf("") }
+ var isConfirmationMode by remember { mutableStateOf(false) }
+ var isVerifyOldPasswordMode by remember { mutableStateOf(!isFirstTimeSetup) }
+
+ var passwordVisible by remember { mutableStateOf(false) }
+
+ var showMismatchError by remember { mutableStateOf(false) }
+ var showLengthError by remember { mutableStateOf(false) }
+ var showMaxLengthError by remember { mutableStateOf(false) }
+ var showInvalidOldPasswordError by remember { mutableStateOf(false) }
+
+ val minLength = 8
+ val maxLength = 64
+ val context = LocalContext.current
+ val activity = LocalActivity.current as? ComponentActivity
+ val appLockRepository = remember {
+ (context.applicationContext as? AppLockApplication)?.appLockRepository
+ }
+
+ val focusRequester = remember { FocusRequester() }
+
+ LaunchedEffect(Unit) {
+ focusRequester.requestFocus()
+ }
+
+ 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() {
+ 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
+ }
+ })
+ biometricPrompt.authenticate(promptInfo)
+ }
+
+ fun switchToPinMethod() {
+ navController.navigate(Screen.SetPassword.route) {
+ popUpTo(Screen.SetPasswordAlphanumeric.route) { inclusive = true }
+ }
+ }
+
+ fun submitPassword() {
+ val currentInput = if (isConfirmationMode) confirmPasswordState else passwordState
+
+ if (currentInput.length < minLength) {
+ showLengthError = true
+ return
+ }
+
+ if (currentInput.length > maxLength) {
+ showMaxLengthError = true
+ return
+ }
+
+ when {
+ isVerifyOldPasswordMode -> {
+ if (appLockRepository!!.validatePassword(passwordState)) {
+ isVerifyOldPasswordMode = false
+ passwordState = ""
+ showInvalidOldPasswordError = false
+ } else {
+ showInvalidOldPasswordError = true
+ passwordState = ""
+ }
+ }
+
+ !isConfirmationMode -> {
+ isConfirmationMode = true
+ showLengthError = false
+ showMaxLengthError = false
+ }
+
+ else -> {
+ if (passwordState == confirmPasswordState) {
+ appLockRepository?.setLockType(PreferencesRepository.LOCK_TYPE_PASSWORD)
+ appLockRepository?.setPassword(passwordState)
+ 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 }
+ }
+ }
+ } else {
+ showMismatchError = true
+ confirmPasswordState = ""
+ }
+ }
+ }
+ }
+
+ Scaffold(
+ containerColor = MaterialTheme.colorScheme.surfaceContainer,
+ topBar = {
+ TopAppBar(
+ title = {
+ Text(
+ text = when {
+ isVerifyOldPasswordMode -> stringResource(R.string.enter_current_password_label)
+ isConfirmationMode -> stringResource(R.string.confirm_alphanumeric_password_label)
+ else -> stringResource(R.string.set_alphanumeric_password_title)
+ },
+ style = MaterialTheme.typography.titleMediumEmphasized,
+ )
+ },
+ colors = TopAppBarDefaults.topAppBarColors(
+ containerColor = MaterialTheme.colorScheme.surfaceContainer,
+ titleContentColor = MaterialTheme.colorScheme.onSurface
+ )
+ )
+ }
+ ) { innerPadding ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding)
+ .padding(24.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Top
+ ) {
+ Text(
+ text = when {
+ isVerifyOldPasswordMode -> stringResource(R.string.enter_current_password_label)
+ isConfirmationMode -> stringResource(R.string.confirm_alphanumeric_password_label)
+ else -> stringResource(R.string.create_alphanumeric_password_label)
+ },
+ style = MaterialTheme.typography.headlineSmall,
+ textAlign = TextAlign.Center
+ )
+
+ Spacer(modifier = Modifier.height(32.dp))
+
+ OutlinedTextField(
+ value = if (isConfirmationMode) confirmPasswordState else passwordState,
+ onValueChange = { input ->
+ val sanitized = SecurityUtils.sanitizePassword(input)
+ if (isConfirmationMode) confirmPasswordState = sanitized else passwordState = sanitized
+ showMismatchError = false
+ showLengthError = false
+ showMaxLengthError = false
+ showInvalidOldPasswordError = false
+ },
+ modifier = Modifier.fillMaxWidth().focusRequester(focusRequester),
+ label = { Text(stringResource(R.string.password_hint)) },
+ visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
+ keyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Password,
+ imeAction = ImeAction.Done
+ ),
+ trailingIcon = {
+ val image = if (passwordVisible)
+ Icons.Filled.Visibility
+ else Icons.Filled.VisibilityOff
+
+ IconButton(onClick = { passwordVisible = !passwordVisible }) {
+ Icon(imageVector = image, contentDescription = null)
+ }
+ },
+ isError = showMismatchError || showLengthError || showMaxLengthError || showInvalidOldPasswordError,
+ singleLine = true
+ )
+
+ if (showMismatchError) {
+ Text(
+ text = stringResource(R.string.passwords_dont_match_error),
+ color = MaterialTheme.colorScheme.error,
+ style = MaterialTheme.typography.labelMedium,
+ modifier = Modifier.padding(top = 4.dp).align(Alignment.Start)
+ )
+ }
+
+ if (showLengthError) {
+ Text(
+ text = stringResource(R.string.password_too_short_error),
+ color = MaterialTheme.colorScheme.error,
+ style = MaterialTheme.typography.labelMedium,
+ modifier = Modifier.padding(top = 4.dp).align(Alignment.Start)
+ )
+ }
+
+ if (showMaxLengthError) {
+ Text(
+ text = stringResource(R.string.password_too_long_error),
+ color = MaterialTheme.colorScheme.error,
+ style = MaterialTheme.typography.labelMedium,
+ modifier = Modifier.padding(top = 4.dp).align(Alignment.Start)
+ )
+ }
+
+ if (showInvalidOldPasswordError) {
+ Text(
+ text = stringResource(R.string.incorrect_password_try_again),
+ color = MaterialTheme.colorScheme.error,
+ style = MaterialTheme.typography.labelMedium,
+ modifier = Modifier.padding(top = 4.dp).align(Alignment.Start)
+ )
+ }
+
+ Spacer(modifier = Modifier.height(32.dp))
+
+ Button(
+ onClick = { submitPassword() },
+ modifier = Modifier.fillMaxWidth(),
+ shape = MaterialTheme.shapes.medium
+ ) {
+ Text(stringResource(R.string.next_button))
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ if (!isVerifyOldPasswordMode && !isConfirmationMode) {
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ modifier = Modifier.padding(bottom = 16.dp)
+ ) {
+ TextButton(onClick = { switchToPinMethod() }) {
+ Text(stringResource(R.string.use_pin_instead))
+ }
+ TextButton(onClick = { navController.navigate(Screen.SetPasswordPattern.route) }) {
+ Text(stringResource(R.string.use_pattern_button))
+ }
+ }
+
+ }
+
+ if (isVerifyOldPasswordMode) {
+ TextButton(onClick = { launchDeviceCredentialAuth() }) {
+ Text(stringResource(R.string.reset_using_device_password_button))
+ }
+ }
+
+ if (isVerifyOldPasswordMode || isConfirmationMode) {
+ TextButton(
+ onClick = {
+ if (isVerifyOldPasswordMode) {
+ if (navController.previousBackStackEntry != null) {
+ navController.popBackStack()
+ } else {
+ activity?.finish()
+ }
+ } else {
+ isConfirmationMode = false
+ if (!isFirstTimeSetup) {
+ isVerifyOldPasswordMode = true
+ }
+ }
+ passwordState = ""
+ confirmPasswordState = ""
+ showMismatchError = false
+ showLengthError = false
+ showMaxLengthError = false
+ showInvalidOldPasswordError = false
+ }
+ ) {
+ Text(
+ if (isVerifyOldPasswordMode) stringResource(R.string.cancel_button)
+ else stringResource(R.string.start_over_button)
+ )
+ }
+ }
+ }
+ }
+}
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..9153a3e 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
@@ -267,8 +267,13 @@ fun PatternSetPasswordScreen(
}
if (isFirstTimeSetup && !isVerifyOldPasswordMode && !isConfirmationMode) {
- TextButton(onClick = { switchToPinMethod() }) {
- Text("Use PIN Instead")
+ Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ TextButton(onClick = { switchToPinMethod() }) {
+ Text(stringResource(R.string.use_pin_instead))
+ }
+ TextButton(onClick = { navController.navigate(Screen.SetPasswordAlphanumeric.route) }) {
+ Text(stringResource(R.string.use_password_button))
+ }
}
}
@@ -448,8 +453,13 @@ fun PatternSetPasswordScreen(
Spacer(modifier = Modifier.height(16.dp))
if (!isVerifyOldPasswordMode && !isConfirmationMode) {
- TextButton(onClick = { switchToPinMethod() }) {
- Text("Use PIN Instead")
+ Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ TextButton(onClick = { switchToPinMethod() }) {
+ Text(stringResource(R.string.use_pin_instead))
+ }
+ TextButton(onClick = { navController.navigate(Screen.SetPasswordAlphanumeric.route) }) {
+ Text(stringResource(R.string.use_password_button))
+ }
}
}
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..19b9fe0 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
@@ -527,15 +527,29 @@ fun SetPasswordScreen(
}
}
- TextButton(
- onClick = {
- navController.navigate(Screen.SetPasswordPattern.route)
- },
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.padding(bottom = 16.dp)
) {
- Text(
- stringResource(R.string.use_pattern_button)
- )
+ TextButton(
+ onClick = {
+ navController.navigate(Screen.SetPasswordPattern.route)
+ }
+ ) {
+ Text(
+ stringResource(R.string.use_pattern_button)
+ )
+ }
+
+ TextButton(
+ onClick = {
+ navController.navigate(Screen.SetPasswordAlphanumeric.route)
+ }
+ ) {
+ Text(
+ stringResource(R.string.use_password_button)
+ )
+ }
}
Column(
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 87a4760..1873486 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -6,6 +6,7 @@
Enter your password to disable admin permission.
Incorrect PIN. Please try again.
+ Incorrect password. 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.
@@ -90,6 +91,19 @@ Make sure you have Shizuku installed and enabled via ADB. Tap \'Next\' to grant
Start Over
Password set successfully
Use Pattern
+ Use Password
+ Use PIN
+
+
+ Set Password
+ Create a password
+ Confirm your password
+ Enter current password
+ Enter password
+ Password must be at least 4 characters
+ Password must be at most 64 characters
+ Passwords don\'t match
+ Incorrect password. Please try again.
Anti-uninstall is enabled