KEYPASS-472 | Add custom auth screen (#476)

* add custom auth screen

* minor improvements
This commit is contained in:
Yogesh Choudhary Paliyal
2023-04-30 00:44:43 +05:30
committed by GitHub
parent 2e6df18677
commit 507be14e5a
10 changed files with 244 additions and 116 deletions

View File

@@ -32,7 +32,8 @@
android:exported="true" />
<activity
android:name=".ui.auth.AuthenticationActivity"
android:exported="true">
android:exported="true"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View File

@@ -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>(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<Int?>(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()
}
}
}

View File

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

View File

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

View File

@@ -84,5 +84,12 @@
<string name="custom_generated_keyphrase_info">क्या आप बैकअप के लिए अपना खुद का कीफ्रेज बनाना चाहते हैं या मुझे आपके लिए जनरेट करना चाहिए?</string>
<string name="ascending">आरोही</string>
<string name="descending">अवरोही</string>
<string name="str_continue">जारी रखना</string>
<string name="back">पीछे</string>
<string name="enter_password">कृपया पासवर्ड दर्ज करें</string>
<string name="create_password">पासवर्ड बनाएं</string>
<string name="confirm_password">पासवर्ड की पुष्टि कीजिये</string>
<string name="incorrect_password">गलत पासवर्ड</string>
<string name="password_no_match">पासवर्ड मेल नहीं खाता</string>
</resources>

View File

@@ -82,5 +82,12 @@
<string name="custom_generated_keyphrase_info">Deseja criar sua própria frase-chave para backups ou devo gerar para você?</string>
<string name="ascending">Ascendente</string>
<string name="descending">descendente</string>
<string name="str_continue">Continuar</string>
<string name="back">Voltar</string>
<string name="enter_password">Digite a senha</string>
<string name="create_password">Criar senha</string>
<string name="confirm_password">Confirme sua senha</string>
<string name="incorrect_password">Senha incorreta</string>
<string name="password_no_match">A senha não corresponde</string>
</resources>

View File

@@ -83,5 +83,12 @@
<string name="custom_generated_keyphrase_info">您想创建自己的备份关键字还是我应该为您生成?</string>
<string name="ascending">上升</string>
<string name="descending">降序</string>
<string name="str_continue">继续</string>
<string name="back">后退</string>
<string name="enter_password">请输入密码</string>
<string name="create_password">创建密码</string>
<string name="confirm_password">确认密码</string>
<string name="incorrect_password">密码错误</string>
<string name="password_no_match">密码不匹配</string>
</resources>

View File

@@ -82,5 +82,12 @@
<string name="ascending">Ascending</string>
<string name="descending">Descending</string>
<string name="str_continue">Continue</string>
<string name="back">Back</string>
<string name="enter_password">Enter Password</string>
<string name="create_password">Create Password</string>
<string name="confirm_password">Confirm Password</string>
<string name="incorrect_password">Incorrect Password</string>
<string name="password_no_match">The password did not match</string>
</resources>

View File

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

View File

@@ -39,6 +39,20 @@ suspend fun Context.getOrCreateBackupKey(reset: Boolean = false): Pair<Boolean,
}
}
suspend fun Context.getKeyPassPassword(): String? {
return dataStore.data.first().get(KEYPASS_PASSWORD)
}
suspend fun Context.setKeyPassPassword(password: String?) {
dataStore.edit {
if (password == null) {
it.remove(KEYPASS_PASSWORD)
} else {
it[KEYPASS_PASSWORD] = password
}
}
}
suspend fun Context.isKeyPresent(): Boolean {
val sp = dataStore.data.first()
return sp.contains(BACKUP_KEY)
@@ -97,6 +111,7 @@ suspend fun Context?.getBackupTime(): Long {
}
private val BACKUP_KEY = stringPreferencesKey("backup_key")
private val KEYPASS_PASSWORD = stringPreferencesKey("keypass_password")
private val BACKUP_DIRECTORY = stringPreferencesKey("backup_directory")
private val BACKUP_DATE_TIME = longPreferencesKey("backup_date_time")
private val AUTO_BACKUP = booleanPreferencesKey("auto_backup")