From 507be14e5ab63c00bace1d3054fcd1a40aaec334 Mon Sep 17 00:00:00 2001 From: Yogesh Choudhary Paliyal Date: Sun, 30 Apr 2023 00:44:43 +0530 Subject: [PATCH] KEYPASS-472 | Add custom auth screen (#476) * add custom auth screen * minor improvements --- app/src/main/AndroidManifest.xml | 3 +- .../keypass/ui/auth/AuthenticationActivity.kt | 275 +++++++++++------- .../ui/home/components/AccountsList.kt | 26 +- .../ui/nav/DashboardComposeActivity.kt | 11 +- app/src/main/res/values-hi/strings.xml | 7 + app/src/main/res/values-pt-rBR/strings.xml | 7 + app/src/main/res/values-zh-rCN/strings.xml | 7 + app/src/main/res/values/strings.xml | 7 + common/build.gradle | 2 - .../common/utils/SharedPreferenceUtils.kt | 15 + 10 files changed, 244 insertions(+), 116 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 334553dd..a95e4563 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -32,7 +32,8 @@ android:exported="true" /> + android:exported="true" + android:windowSoftInputMode="adjustResize"> diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/auth/AuthenticationActivity.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/auth/AuthenticationActivity.kt index 32a74a35..0f6e0541 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/auth/AuthenticationActivity.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/auth/AuthenticationActivity.kt @@ -1,136 +1,215 @@ package com.yogeshpaliyal.keypass.ui.auth +import android.content.Context import android.content.Intent -import android.os.Build import android.os.Bundle -import android.provider.Settings -import android.util.Log -import android.widget.Toast +import androidx.activity.compose.BackHandler +import androidx.activity.compose.setContent +import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity -import androidx.biometric.BiometricManager -import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG -import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK -import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL -import androidx.biometric.BiometricPrompt -import androidx.core.content.ContextCompat +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Visibility +import androidx.compose.material.icons.rounded.VisibilityOff +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +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 +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +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.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.yogeshpaliyal.common.utils.getKeyPassPassword +import com.yogeshpaliyal.common.utils.setKeyPassPassword import com.yogeshpaliyal.keypass.R -import com.yogeshpaliyal.keypass.databinding.ActivityAuthenticationBinding import com.yogeshpaliyal.keypass.ui.nav.DashboardComposeActivity +import com.yogeshpaliyal.keypass.ui.style.KeyPassTheme import dagger.hilt.android.AndroidEntryPoint -import java.util.concurrent.Executor +import kotlinx.coroutines.launch private const val AUTHENTICATION_RESULT = 707 +sealed class AuthState(@StringRes val title: Int) { + object CreatePassword : AuthState(R.string.create_password) + class ConfirmPassword(val password: String) : AuthState(R.string.confirm_password) + object Login : AuthState(R.string.login_to_enter_keypass) +} + @AndroidEntryPoint class AuthenticationActivity : AppCompatActivity() { - private lateinit var binding: ActivityAuthenticationBinding - - private lateinit var executor: Executor - private lateinit var biometricPrompt: BiometricPrompt - private lateinit var promptInfo: BiometricPrompt.PromptInfo - - private val biometricManager by lazy { - BiometricManager.from(this) + @Preview(showSystemUi = true) + @Composable + fun AuthScreenPreview() { + AuthScreen() } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - binding = ActivityAuthenticationBinding.inflate(layoutInflater) - setContentView(binding.root) + @Composable + fun AuthScreen() { + val context = LocalContext.current - executor = ContextCompat.getMainExecutor(this) - biometricPrompt = BiometricPrompt( - this, - executor, - object : BiometricPrompt.AuthenticationCallback() { - override fun onAuthenticationError( - errorCode: Int, - errString: CharSequence - ) { - super.onAuthenticationError(errorCode, errString) - Toast.makeText( - applicationContext, - "Authentication error: $errString", - Toast.LENGTH_SHORT - ) - .show() - // finish() - } + val coroutineScope = rememberCoroutineScope() - override fun onAuthenticationSucceeded( - result: BiometricPrompt.AuthenticationResult - ) { - super.onAuthenticationSucceeded(result) + val (state, setState) = remember { + mutableStateOf(AuthState.Login) + } - onAuthenticated() - } + val (password, setPassword) = remember(state) { + mutableStateOf("") + } - override fun onAuthenticationFailed() { - super.onAuthenticationFailed() - Toast.makeText( - applicationContext, - getString(R.string.authentication_failed), - Toast.LENGTH_SHORT - ) - .show() + val (passwordVisible, setPasswordVisible) = remember(state) { mutableStateOf(false) } + + val (passwordError, setPasswordError) = remember(state, password) { + mutableStateOf(null) + } + + BackHandler(state is AuthState.ConfirmPassword) { + setState(AuthState.CreatePassword) + } + + LaunchedEffect(key1 = Unit, block = { + coroutineScope.launch { + val mPassword = context.getKeyPassPassword() + if (mPassword == null) { + setState(AuthState.CreatePassword) } } - ) + }) - promptInfo = BiometricPrompt.PromptInfo.Builder() - .setTitle(getString(R.string.app_name)) - .setSubtitle(getString(R.string.login_to_enter_keypass)) - .setAllowedAuthenticators(DEVICE_CREDENTIAL or BIOMETRIC_WEAK or BIOMETRIC_STRONG) - .build() + KeyPassTheme { + Column( + modifier = Modifier + .padding(32.dp) + .fillMaxSize(1f) + .verticalScroll(rememberScrollState()), + Arrangement.SpaceEvenly, + Alignment.CenterHorizontally + ) { + Image( + painter = painterResource(R.drawable.ic_undraw_unlock_24mb), + contentDescription = "" + ) - // Prompt appears when user clicks "Log in". - // Consider integrating with the keystore to unlock cryptographic operations, - // if needed by your app. + Text(text = stringResource(id = state.title)) - biometricPrompt.authenticate(promptInfo) - - binding.btnRetry.setOnClickListener { - val allowedAuths = DEVICE_CREDENTIAL or BIOMETRIC_WEAK or BIOMETRIC_STRONG - val canAuthentication = - biometricManager.canAuthenticate(allowedAuths) - when (canAuthentication) { - BiometricManager.BIOMETRIC_SUCCESS -> { - Log.d("MY_APP_TAG", "App can authenticate using biometrics.") - biometricPrompt.authenticate(promptInfo) - } - BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE, - BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE, - BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> { - Log.e( - "MY_APP_TAG", - "$canAuthentication Biometric features are currently unavailable." - ) - // 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 + OutlinedTextField( + modifier = Modifier.fillMaxWidth(1f), + value = password, + singleLine = true, + placeholder = { + Text(text = stringResource(id = R.string.enter_password)) + }, + visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), + onValueChange = setPassword, + isError = passwordError != null, + supportingText = { + if (passwordError != null) { + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(id = passwordError), + color = MaterialTheme.colorScheme.error ) } - startActivityForResult(enrollIntent, AUTHENTICATION_RESULT) - } else { - Toast.makeText( - this, - "Please set password for your device first from phone settings", - Toast.LENGTH_SHORT - ).show() + }, + trailingIcon = { + val image = if (passwordVisible) { + Icons.Rounded.Visibility + } else Icons.Rounded.VisibilityOff + + // Please provide localized description for accessibility services + val description = if (passwordVisible) "Hide password" else "Show password" + + IconButton(onClick = { setPasswordVisible(!passwordVisible) }) { + Icon(imageVector = image, description) + } + } + ) + + Row(modifier = Modifier.fillMaxWidth(1f), Arrangement.SpaceEvenly) { + AnimatedVisibility(state is AuthState.ConfirmPassword) { + Button(onClick = { + setState(AuthState.CreatePassword) + }) { + Text(text = stringResource(id = R.string.back)) + } + } + + Button(onClick = { + when (state) { + is AuthState.CreatePassword -> { + if (password.isBlank()) { + setPasswordError(R.string.enter_password) + } else { + setState(AuthState.ConfirmPassword(password)) + } + } + + is AuthState.ConfirmPassword -> { + if (state.password == password) { + coroutineScope.launch { + context.setKeyPassPassword(password) + onAuthComplete(context) + } + } else { + setPasswordError(R.string.password_no_match) + } + } + + is AuthState.Login -> { + coroutineScope.launch { + val savedPassword = context.getKeyPassPassword() + if (savedPassword == password) { + onAuthComplete(context) + } else { + setPasswordError(R.string.incorrect_password) + } + } + } + } + }) { + Text(text = stringResource(id = R.string.str_continue)) } } } } } - private fun onAuthenticated() { + private fun onAuthComplete(context: Context) { // binding.passCodeView.isVisible = false - val dashboardIntent = Intent(this, DashboardComposeActivity::class.java) + val dashboardIntent = Intent(context, DashboardComposeActivity::class.java) startActivity(dashboardIntent) finish() } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + AuthScreen() + } + } } diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/home/components/AccountsList.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/home/components/AccountsList.kt index 5fa99d36..00e3dadc 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/home/components/AccountsList.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/home/components/AccountsList.kt @@ -1,5 +1,10 @@ package com.yogeshpaliyal.keypass.ui.home.components +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -209,20 +214,19 @@ private fun getPassword(model: AccountModel): String { @Composable fun WrapWithProgress(accountModel: AccountModel) { - val (progress, setProgress) = remember { mutableStateOf(0f) } - - LaunchedEffect(Unit) { - if (accountModel.type == AccountType.TOTP) { - while (true) { - val newProgress = accountModel.getTOtpProgress().toFloat() / 30 - setProgress(newProgress) - delay(1.seconds) - } - } + if (accountModel.type != AccountType.TOTP) { + return } + val infiniteTransition = rememberInfiniteTransition(accountModel.uniqueId ?: accountModel.title ?: "") + val rotationAnimation = infiniteTransition.animateFloat( + initialValue = 1f - (accountModel.getTOtpProgress().toFloat() / 30), + targetValue = 1f, + animationSpec = infiniteRepeatable(tween(30000, easing = LinearEasing)) + ) + CircularProgressIndicator( modifier = Modifier.fillMaxSize(), - progress = progress + progress = rotationAnimation.value ) } 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 bd72b6a6..8dcade06 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 @@ -46,6 +46,7 @@ import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnitType import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidViewBinding +import com.yogeshpaliyal.keypass.BuildConfig import com.yogeshpaliyal.keypass.databinding.LayoutMySettingsFragmentBinding import com.yogeshpaliyal.keypass.ui.detail.AccountDetailPage import com.yogeshpaliyal.keypass.ui.home.Homepage @@ -73,10 +74,12 @@ class DashboardComposeActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - window.setFlags( - WindowManager.LayoutParams.FLAG_SECURE, - WindowManager.LayoutParams.FLAG_SECURE - ) + if (BuildConfig.DEBUG.not()) { + window.setFlags( + WindowManager.LayoutParams.FLAG_SECURE, + WindowManager.LayoutParams.FLAG_SECURE + ) + } setContent { KeyPassTheme { StoreProvider(store = KeyPassRedux.createStore()) { diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 48e361c3..e21c87bb 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -84,5 +84,12 @@ क्या आप बैकअप के लिए अपना खुद का कीफ्रेज बनाना चाहते हैं या मुझे आपके लिए जनरेट करना चाहिए? आरोही अवरोही + जारी रखना + पीछे + कृपया पासवर्ड दर्ज करें + पासवर्ड बनाएं + पासवर्ड की पुष्टि कीजिये + गलत पासवर्ड + पासवर्ड मेल नहीं खाता \ 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 572870e8..e5df14e0 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -82,5 +82,12 @@ Deseja criar sua própria frase-chave para backups ou devo gerar para você? Ascendente descendente + Continuar + Voltar + Digite a senha + Criar senha + Confirme sua senha + Senha incorreta + A senha não corresponde diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 8d35e9c3..e8d4c56e 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -83,5 +83,12 @@ 您想创建自己的备份关键字还是我应该为您生成? 上升 降序 + 继续 + 后退 + 请输入密码 + 创建密码 + 确认密码 + 密码错误 + 密码不匹配 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d32ad7d7..6a6d452a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -82,5 +82,12 @@ Ascending Descending + Continue + Back + Enter Password + Create Password + Confirm Password + Incorrect Password + The password did not match diff --git a/common/build.gradle b/common/build.gradle index 6002e2a9..fa15a10a 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -69,8 +69,6 @@ dependencies { api "androidx.datastore:datastore-preferences:1.0.0" - api "androidx.biometric:biometric:1.1.0" - // ViewModel api "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" // LiveData 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 d9d9e0ee..b845d6f5 100644 --- a/common/src/main/java/com/yogeshpaliyal/common/utils/SharedPreferenceUtils.kt +++ b/common/src/main/java/com/yogeshpaliyal/common/utils/SharedPreferenceUtils.kt @@ -39,6 +39,20 @@ suspend fun Context.getOrCreateBackupKey(reset: Boolean = false): Pair