mirror of
https://github.com/yogeshpaliyal/KeyPass.git
synced 2025-12-31 00:59:51 -06:00
KEYPASS-472 | Add custom auth screen (#476)
* add custom auth screen * minor improvements
This commit is contained in:
committed by
GitHub
parent
2e6df18677
commit
507be14e5a
@@ -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" />
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user