Biometric option added (#552)

This commit is contained in:
Yogesh Choudhary Paliyal
2023-05-18 08:19:35 +05:30
committed by GitHub
parent c7ac89a826
commit 2ddaa919c5
11 changed files with 220 additions and 7 deletions

View File

@@ -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")
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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