From 2ddaa919c5cd4284e6994613f7fb6d7d7bbdc41e Mon Sep 17 00:00:00 2001 From: Yogesh Choudhary Paliyal Date: Thu, 18 May 2023 08:19:35 +0530 Subject: [PATCH] Biometric option added (#552) --- app/build.gradle.kts | 3 + .../keypass/ui/auth/AuthScreen.kt | 70 ++++++++++++ .../ui/nav/DashboardComposeActivity.kt | 3 +- .../keypass/ui/redux/KeyPassRedux.kt | 8 ++ .../keypass/ui/redux/actions/UtilityAction.kt | 1 + .../keypass/ui/settings/MySettingsFragment.kt | 104 +++++++++++++++++- app/src/main/res/values-hi/strings.xml | 6 + app/src/main/res/values-pt-rBR/strings.xml | 6 + app/src/main/res/values-zh-rCN/strings.xml | 6 + app/src/main/res/values/strings.xml | 9 +- .../common/utils/SharedPreferenceUtils.kt | 11 ++ 11 files changed, 220 insertions(+), 7 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index bd6121ee..70fce7f2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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") + } diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/auth/AuthScreen.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/auth/AuthScreen.kt index 8e42d149..e24edd44 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/auth/AuthScreen.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/auth/AuthScreen.kt @@ -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(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() + + 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) + } } diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/DashboardComposeActivity.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/DashboardComposeActivity.kt index 7669ff82..5ab3ecf3 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/DashboardComposeActivity.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/DashboardComposeActivity.kt @@ -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) diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/KeyPassRedux.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/KeyPassRedux.kt index ddc9ed32..1cdf184c 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/KeyPassRedux.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/KeyPassRedux.kt @@ -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) { diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/actions/UtilityAction.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/actions/UtilityAction.kt index 9ee0f839..acbbcd3a 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/actions/UtilityAction.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/actions/UtilityAction.kt @@ -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 diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/settings/MySettingsFragment.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/settings/MySettingsFragment.kt index f357174a..9103854e 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/settings/MySettingsFragment.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/settings/MySettingsFragment.kt @@ -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(null) + } + + val dispatch = rememberTypedDispatcher() + 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) { diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 61de1882..c238812a 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -30,6 +30,11 @@ संख्याएं क्लिपबोर्ड पर कॉपी किया गया KeyPass में प्रवेश करने के लिए लॉगिन करें + इस डिवाइस पर कोई बायोमेट्रिक सुविधा उपलब्ध नहीं है। + बायोमेट्रिक सुविधाएँ वर्तमान में अनुपलब्ध हैं। + अपने डिवाइस पर बायोमेट्रिक सेटअप करें। + बायोमेट्रिक से अनलॉक करें + कृपया फोन सेटिंग से पहले अपने डिवाइस के लिए पासवर्ड सेट करें प्रमाणीकरण विफल होना पिछला बैकअप: %s @@ -100,5 +105,6 @@ कृपया अपना नया पासवर्ड दर्ज करें कृपया पुष्टि पासवर्ड दर्ज करें पासवर्ड सफलतापूर्वक बदला गया + प्रमाणीकरण त्रुटि %s \ No newline at end of file diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 1e317d48..aecaa80c 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -30,6 +30,11 @@ Números Copiado para à área de transferência Autenticação para entrar no KeyPass + Nenhum recurso biométrico disponível neste dispositivo. + Os recursos biométricos estão indisponíveis no momento. + Configure a biometria no seu dispositivo. + Desbloqueie com biometria + Por favor, defina a senha para o seu dispositivo primeiro nas configurações do telefone Falha na autenticação Último backup: %s @@ -98,5 +103,6 @@ Por favor, digite sua nova senha Por favor, digite a senha de confirmação Senha alterada com sucesso + Erro de autenticação %s diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 37ebfcb6..d5c4c704 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -30,6 +30,11 @@ 数字 已复制到剪切板 输入密码以进入 KeyPass + 此设备上没有可用的生物识别功能。 + 生物识别功能目前不可用。 + 在您的设备上设置生物识别。 + 使用生物特征解锁 + 请先从手机设置中为您的设备设置密码 身份认证失败 上次备份: %s @@ -99,5 +104,6 @@ 请输入您的新密码 请输入确认密码 密码修改成功 + 身份验证错误 %s diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 776df4a6..d4f30189 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -30,7 +30,6 @@ Numbers Copied to clipboard Login to enter KeyPass - Authentication failed Last backup : %s Coming Soon @@ -99,4 +98,12 @@ Please enter confirm password Password changed successfully + No biometric features available on this device. + Biometric features are currently unavailable. + Setup biometric on your device. + Unlock with biometric + Please set password for your device first from phone settings + Authentication Failed + Authentication Error %s + diff --git a/common/src/main/java/com/yogeshpaliyal/common/utils/SharedPreferenceUtils.kt b/common/src/main/java/com/yogeshpaliyal/common/utils/SharedPreferenceUtils.kt index b845d6f5..5c53a08e 100644 --- a/common/src/main/java/com/yogeshpaliyal/common/utils/SharedPreferenceUtils.kt +++ b/common/src/main/java/com/yogeshpaliyal/common/utils/SharedPreferenceUtils.kt @@ -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")