mirror of
https://github.com/yogeshpaliyal/KeyPass.git
synced 2026-01-01 09:20:31 -06:00
Biometric option added (#552)
This commit is contained in:
committed by
GitHub
parent
c7ac89a826
commit
2ddaa919c5
@@ -166,4 +166,7 @@ dependencies {
|
||||
|
||||
implementation("org.reduxkotlin:redux-kotlin-compose-jvm:0.6.0")
|
||||
implementation("me.saket.cascade:cascade-compose:2.0.0-rc02")
|
||||
|
||||
implementation("androidx.biometric:biometric:1.1.0")
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package com.yogeshpaliyal.keypass.ui.auth
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
|
||||
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -20,14 +23,22 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.yogeshpaliyal.common.utils.getKeyPassPassword
|
||||
import com.yogeshpaliyal.common.utils.isBiometricEnable
|
||||
import com.yogeshpaliyal.keypass.R
|
||||
import com.yogeshpaliyal.keypass.ui.auth.components.ButtonBar
|
||||
import com.yogeshpaliyal.keypass.ui.auth.components.PasswordInputField
|
||||
import com.yogeshpaliyal.keypass.ui.redux.actions.Action
|
||||
import com.yogeshpaliyal.keypass.ui.redux.actions.NavigationAction
|
||||
import com.yogeshpaliyal.keypass.ui.redux.actions.ToastAction
|
||||
import com.yogeshpaliyal.keypass.ui.redux.actions.ToastActionStr
|
||||
import com.yogeshpaliyal.keypass.ui.redux.states.AuthState
|
||||
import com.yogeshpaliyal.keypass.ui.redux.states.HomeState
|
||||
import kotlinx.coroutines.launch
|
||||
import org.reduxkotlin.compose.rememberDispatcher
|
||||
import org.reduxkotlin.compose.rememberTypedDispatcher
|
||||
|
||||
@Composable
|
||||
fun AuthScreen(state: AuthState) {
|
||||
@@ -47,6 +58,14 @@ fun AuthScreen(state: AuthState) {
|
||||
mutableStateOf<Int?>(null)
|
||||
}
|
||||
|
||||
val (biometricEnable, setBiometricEnable) = remember(state) { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(key1 = context, state) {
|
||||
if (state is AuthState.Login) {
|
||||
setBiometricEnable(context.isBiometricEnable())
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler(state is AuthState.ConfirmPassword) {
|
||||
dispatchAction(NavigationAction(AuthState.CreatePassword))
|
||||
}
|
||||
@@ -87,4 +106,55 @@ fun AuthScreen(state: AuthState) {
|
||||
dispatchAction(it)
|
||||
}
|
||||
}
|
||||
|
||||
BiometricPrompt(show = biometricEnable)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BiometricPrompt(show: Boolean) {
|
||||
if (!show) {
|
||||
return
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
val dispatch = rememberTypedDispatcher<Action>()
|
||||
|
||||
LaunchedEffect(key1 = context) {
|
||||
val fragmentActivity = context as? FragmentActivity ?: return@LaunchedEffect
|
||||
val executor = ContextCompat.getMainExecutor(fragmentActivity)
|
||||
val biometricPrompt = BiometricPrompt(
|
||||
fragmentActivity,
|
||||
executor,
|
||||
object : BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationError(
|
||||
errorCode: Int,
|
||||
errString: CharSequence
|
||||
) {
|
||||
super.onAuthenticationError(errorCode, errString)
|
||||
dispatch(ToastActionStr(context.getString(R.string.authentication_error, errString)))
|
||||
}
|
||||
|
||||
override fun onAuthenticationSucceeded(
|
||||
result: BiometricPrompt.AuthenticationResult
|
||||
) {
|
||||
super.onAuthenticationSucceeded(result)
|
||||
dispatch(NavigationAction(HomeState(), true))
|
||||
}
|
||||
|
||||
override fun onAuthenticationFailed() {
|
||||
super.onAuthenticationFailed()
|
||||
dispatch(ToastAction(R.string.authentication_failed))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
val promptInfo = BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(context.getString(R.string.app_name))
|
||||
.setSubtitle(context.getString(R.string.login_to_enter_keypass))
|
||||
.setAllowedAuthenticators(BIOMETRIC_STRONG or BIOMETRIC_WEAK)
|
||||
.setNegativeButtonText(context.getText(R.string.cancel))
|
||||
.build()
|
||||
|
||||
biometricPrompt.authenticate(promptInfo)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.view.WindowManager
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -77,7 +78,7 @@ import org.reduxkotlin.compose.selectState
|
||||
import java.util.Locale
|
||||
|
||||
@AndroidEntryPoint
|
||||
class DashboardComposeActivity : ComponentActivity() {
|
||||
class DashboardComposeActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.yogeshpaliyal.keypass.ui.redux.actions.GoBackAction
|
||||
import com.yogeshpaliyal.keypass.ui.redux.actions.NavigationAction
|
||||
import com.yogeshpaliyal.keypass.ui.redux.actions.StateUpdateAction
|
||||
import com.yogeshpaliyal.keypass.ui.redux.actions.ToastAction
|
||||
import com.yogeshpaliyal.keypass.ui.redux.actions.ToastActionStr
|
||||
import com.yogeshpaliyal.keypass.ui.redux.actions.UpdateContextAction
|
||||
import com.yogeshpaliyal.keypass.ui.redux.middlewares.intentNavigationMiddleware
|
||||
import com.yogeshpaliyal.keypass.ui.redux.states.BottomSheetState
|
||||
@@ -73,6 +74,13 @@ object KeyPassRedux {
|
||||
state
|
||||
}
|
||||
|
||||
is ToastActionStr -> {
|
||||
state.context?.let {
|
||||
Toast.makeText(it, action.text, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
state
|
||||
}
|
||||
|
||||
is GoBackAction -> {
|
||||
val lastItem = arrPages.removeLastOrNull()
|
||||
if (lastItem != null) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.yogeshpaliyal.keypass.ui.redux.actions
|
||||
import androidx.annotation.StringRes
|
||||
|
||||
data class ToastAction(@StringRes val text: Int) : Action
|
||||
data class ToastActionStr(val text: String) : Action
|
||||
data class CopyToClipboard(val password: String) : Action
|
||||
|
||||
object GoBackAction : Action
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
package com.yogeshpaliyal.keypass.ui.settings
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
|
||||
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -16,6 +22,8 @@ import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Feedback
|
||||
import androidx.compose.material.icons.rounded.Fingerprint
|
||||
import androidx.compose.material.icons.rounded.Password
|
||||
import androidx.compose.material.icons.rounded.Share
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Divider
|
||||
@@ -25,6 +33,7 @@ import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
@@ -39,6 +48,8 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.yogeshpaliyal.common.utils.BACKUP_KEY_LENGTH
|
||||
import com.yogeshpaliyal.common.utils.email
|
||||
import com.yogeshpaliyal.common.utils.isBiometricEnable
|
||||
import com.yogeshpaliyal.common.utils.setBiometricEnable
|
||||
import com.yogeshpaliyal.keypass.R
|
||||
import com.yogeshpaliyal.keypass.ui.home.DashboardViewModel
|
||||
import com.yogeshpaliyal.keypass.ui.redux.actions.Action
|
||||
@@ -158,10 +169,14 @@ fun MySettingCompose() {
|
||||
}
|
||||
PreferenceItem(
|
||||
title = R.string.change_app_password,
|
||||
summary = R.string.change_app_password
|
||||
summary = R.string.change_app_password,
|
||||
icon = Icons.Rounded.Password
|
||||
) {
|
||||
dispatchAction(NavigationAction(ChangeAppPasswordState()))
|
||||
}
|
||||
|
||||
BiometricsOption()
|
||||
|
||||
Divider(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(1f)
|
||||
@@ -188,6 +203,85 @@ fun MySettingCompose() {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BiometricsOption() {
|
||||
val context = LocalContext.current
|
||||
val (canAuthenticate, setCanAuthenticate) = remember {
|
||||
mutableStateOf(BiometricManager.BIOMETRIC_STATUS_UNKNOWN)
|
||||
}
|
||||
|
||||
val (isBiometricEnable, setIsBiometricEnable) = remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
val (subtitle, setSubtitle) = remember {
|
||||
mutableStateOf<Int?>(null)
|
||||
}
|
||||
|
||||
val dispatch = rememberTypedDispatcher<Action>()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(key1 = context) {
|
||||
setIsBiometricEnable(context.isBiometricEnable())
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = context) {
|
||||
val biometricManager = BiometricManager.from(context)
|
||||
setCanAuthenticate(biometricManager.canAuthenticate(BIOMETRIC_STRONG))
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = canAuthenticate, isBiometricEnable) {
|
||||
when (canAuthenticate) {
|
||||
BiometricManager.BIOMETRIC_SUCCESS ->
|
||||
if (isBiometricEnable) {
|
||||
setSubtitle(R.string.enabled)
|
||||
} else {
|
||||
setSubtitle(R.string.disabled)
|
||||
}
|
||||
|
||||
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE ->
|
||||
setSubtitle(R.string.biometric_error_no_hardware)
|
||||
|
||||
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE ->
|
||||
setSubtitle(R.string.biometric_error_hw_unavailable)
|
||||
|
||||
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
|
||||
setSubtitle(R.string.biometric_error_none_enrolled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PreferenceItem(
|
||||
title = R.string.unlock_with_biometric,
|
||||
summary = subtitle,
|
||||
icon = Icons.Rounded.Fingerprint
|
||||
) {
|
||||
when (canAuthenticate) {
|
||||
BiometricManager.BIOMETRIC_SUCCESS -> {
|
||||
coroutineScope.launch {
|
||||
context.setBiometricEnable(!isBiometricEnable)
|
||||
setIsBiometricEnable(!isBiometricEnable)
|
||||
}
|
||||
}
|
||||
|
||||
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
|
||||
// Prompts the user to create credentials that your app accepts.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val enrollIntent = Intent(Settings.ACTION_BIOMETRIC_ENROLL).apply {
|
||||
putExtra(
|
||||
Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED,
|
||||
BIOMETRIC_STRONG or DEVICE_CREDENTIAL
|
||||
)
|
||||
}
|
||||
context.startActivity(enrollIntent)
|
||||
} else {
|
||||
dispatch(ToastAction(R.string.password_set_from_settings))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PreferenceItem(
|
||||
@StringRes title: Int? = null,
|
||||
@@ -200,11 +294,11 @@ fun PreferenceItem(
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(1f)
|
||||
.widthIn(48.dp)
|
||||
.padding(horizontal = 16.dp)
|
||||
.clickable(onClickItem != null) {
|
||||
.clickable(onClickItem != null, onClick = {
|
||||
onClickItem?.invoke()
|
||||
},
|
||||
})
|
||||
.widthIn(48.dp)
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(modifier = Modifier.width(56.dp), Alignment.CenterStart) {
|
||||
|
||||
@@ -30,6 +30,11 @@
|
||||
<string name="numbers">संख्याएं</string>
|
||||
<string name="copied_to_clipboard">क्लिपबोर्ड पर कॉपी किया गया</string>
|
||||
<string name="login_to_enter_keypass">KeyPass में प्रवेश करने के लिए लॉगिन करें</string>
|
||||
<string name="biometric_error_no_hardware">इस डिवाइस पर कोई बायोमेट्रिक सुविधा उपलब्ध नहीं है।</string>
|
||||
<string name="biometric_error_hw_unavailable">बायोमेट्रिक सुविधाएँ वर्तमान में अनुपलब्ध हैं।</string>
|
||||
<string name="biometric_error_none_enrolled">अपने डिवाइस पर बायोमेट्रिक सेटअप करें।</string>
|
||||
<string name="unlock_with_biometric">बायोमेट्रिक से अनलॉक करें</string>
|
||||
<string name="password_set_from_settings">कृपया फोन सेटिंग से पहले अपने डिवाइस के लिए पासवर्ड सेट करें</string>
|
||||
<string name="authentication_failed">प्रमाणीकरण विफल होना</string>
|
||||
|
||||
<string name="last_backup_date">पिछला बैकअप: %s</string>
|
||||
@@ -100,5 +105,6 @@
|
||||
<string name="blank_new_password">कृपया अपना नया पासवर्ड दर्ज करें</string>
|
||||
<string name="blank_confirm_password">कृपया पुष्टि पासवर्ड दर्ज करें</string>
|
||||
<string name="password_change_success">पासवर्ड सफलतापूर्वक बदला गया</string>
|
||||
<string name="authentication_error">प्रमाणीकरण त्रुटि %s</string>
|
||||
|
||||
</resources>
|
||||
@@ -30,6 +30,11 @@
|
||||
<string name="numbers">Números</string>
|
||||
<string name="copied_to_clipboard">Copiado para à área de transferência</string>
|
||||
<string name="login_to_enter_keypass">Autenticação para entrar no KeyPass</string>
|
||||
<string name="biometric_error_no_hardware">Nenhum recurso biométrico disponível neste dispositivo.</string>
|
||||
<string name="biometric_error_hw_unavailable">Os recursos biométricos estão indisponíveis no momento.</string>
|
||||
<string name="biometric_error_none_enrolled">Configure a biometria no seu dispositivo.</string>
|
||||
<string name="unlock_with_biometric">Desbloqueie com biometria</string>
|
||||
<string name="password_set_from_settings">Por favor, defina a senha para o seu dispositivo primeiro nas configurações do telefone</string>
|
||||
<string name="authentication_failed">Falha na autenticação</string>
|
||||
|
||||
<string name="last_backup_date">Último backup: %s</string>
|
||||
@@ -98,5 +103,6 @@
|
||||
<string name="blank_new_password">Por favor, digite sua nova senha</string>
|
||||
<string name="blank_confirm_password">Por favor, digite a senha de confirmação</string>
|
||||
<string name="password_change_success">Senha alterada com sucesso</string>
|
||||
<string name="authentication_error">Erro de autenticação %s</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -30,6 +30,11 @@
|
||||
<string name="numbers">数字</string>
|
||||
<string name="copied_to_clipboard">已复制到剪切板</string>
|
||||
<string name="login_to_enter_keypass">输入密码以进入 KeyPass</string>
|
||||
<string name="biometric_error_no_hardware">此设备上没有可用的生物识别功能。</string>
|
||||
<string name="biometric_error_hw_unavailable">生物识别功能目前不可用。</string>
|
||||
<string name="biometric_error_none_enrolled">在您的设备上设置生物识别。</string>
|
||||
<string name="unlock_with_biometric">使用生物特征解锁</string>
|
||||
<string name="password_set_from_settings">请先从手机设置中为您的设备设置密码</string>
|
||||
<string name="authentication_failed">身份认证失败</string>
|
||||
|
||||
<string name="last_backup_date">上次备份: %s</string>
|
||||
@@ -99,5 +104,6 @@
|
||||
<string name="blank_new_password">请输入您的新密码</string>
|
||||
<string name="blank_confirm_password">请输入确认密码</string>
|
||||
<string name="password_change_success">密码修改成功</string>
|
||||
<string name="authentication_error">身份验证错误 %s</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
<string name="numbers">Numbers</string>
|
||||
<string name="copied_to_clipboard">Copied to clipboard</string>
|
||||
<string name="login_to_enter_keypass">Login to enter KeyPass</string>
|
||||
<string name="authentication_failed">Authentication failed</string>
|
||||
|
||||
<string name="last_backup_date">Last backup : %s</string>
|
||||
<string name="coming_soon">Coming Soon</string>
|
||||
@@ -99,4 +98,12 @@
|
||||
<string name="blank_confirm_password">Please enter confirm password</string>
|
||||
<string name="password_change_success">Password changed successfully</string>
|
||||
|
||||
<string name="biometric_error_no_hardware">No biometric features available on this device.</string>
|
||||
<string name="biometric_error_hw_unavailable">Biometric features are currently unavailable.</string>
|
||||
<string name="biometric_error_none_enrolled">Setup biometric on your device.</string>
|
||||
<string name="unlock_with_biometric">Unlock with biometric</string>
|
||||
<string name="password_set_from_settings">Please set password for your device first from phone settings</string>
|
||||
<string name="authentication_failed">Authentication Failed</string>
|
||||
<string name="authentication_error">Authentication Error %s</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -58,6 +58,16 @@ suspend fun Context.isKeyPresent(): Boolean {
|
||||
return sp.contains(BACKUP_KEY)
|
||||
}
|
||||
|
||||
suspend fun Context.isBiometricEnable(): Boolean {
|
||||
return this.dataStore.data.first()[BIOMETRIC_ENABLE] ?: false
|
||||
}
|
||||
|
||||
suspend fun Context.setBiometricEnable(isEnable: Boolean) {
|
||||
dataStore.edit {
|
||||
it[BIOMETRIC_ENABLE] = isEnable
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun Context.saveKeyphrase(keyphrase: String) {
|
||||
dataStore.edit {
|
||||
it[BACKUP_KEY] = keyphrase
|
||||
@@ -111,6 +121,7 @@ suspend fun Context?.getBackupTime(): Long {
|
||||
}
|
||||
|
||||
private val BACKUP_KEY = stringPreferencesKey("backup_key")
|
||||
private val BIOMETRIC_ENABLE = booleanPreferencesKey("biometric_enable")
|
||||
private val KEYPASS_PASSWORD = stringPreferencesKey("keypass_password")
|
||||
private val BACKUP_DIRECTORY = stringPreferencesKey("backup_directory")
|
||||
private val BACKUP_DATE_TIME = longPreferencesKey("backup_date_time")
|
||||
|
||||
Reference in New Issue
Block a user