fix a serious bug in v5.3 (#20)

use composable function instead of fragment to display authenticate screen
fix authenticate screen is transparent in light theme
This commit is contained in:
BinTianqi
2024-05-19 20:35:52 +08:00
parent 074b8d919b
commit 32b78a9738
10 changed files with 196 additions and 186 deletions

View File

@@ -1,131 +1,117 @@
package com.bintianqi.owndroid
import android.annotation.SuppressLint
import android.content.Context
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.biometric.BiometricPrompt.AuthenticationCallback
import androidx.biometric.BiometricPrompt.PromptInfo.Builder
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import com.bintianqi.owndroid.ui.theme.OwnDroidTheme
import kotlinx.coroutines.Dispatchers
import androidx.fragment.app.FragmentActivity
import com.bintianqi.owndroid.ui.Animations
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class AuthFragment: Fragment() {
@SuppressLint("UnrememberedMutableState")
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val context = requireContext()
val sharedPref = context.getSharedPreferences("data", Context.MODE_PRIVATE)!!
val canStartAuth = mutableStateOf(true)
val onAuthSucceed = {
val fragmentManager = this.parentFragmentManager
val transaction = fragmentManager.beginTransaction()
transaction.setCustomAnimations(R.anim.enter, R.anim.exit)
transaction.remove(this@AuthFragment).commit()
}
val promptInfo = Builder()
.setTitle(context.getText(R.string.authenticate))
.setSubtitle(context.getText(R.string.auth_with_bio))
.setConfirmationRequired(true)
var fallback = false
val callback = object: AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
onAuthSucceed()
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
when(errorCode){
BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL -> onAuthSucceed()
BiometricPrompt.ERROR_NEGATIVE_BUTTON -> fallback = true
else -> canStartAuth.value = true
}
Log.e("OwnDroid", errString.toString())
}
}
lifecycleScope.launch(Dispatchers.Main) {
while(true){
if(fallback){
val fallbackPromptInfo = Builder()
.setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL)
.setTitle(context.getText(R.string.authenticate))
.setSubtitle(context.getText(R.string.auth_with_password))
.setConfirmationRequired(true)
.build()
val executor = ContextCompat.getMainExecutor(requireContext())
val biometricPrompt = BiometricPrompt(requireActivity(), executor, callback)
biometricPrompt.authenticate(fallbackPromptInfo)
break
}
delay(50)
}
}
return ComposeView(requireContext()).apply {
setContent {
val materialYou = mutableStateOf(sharedPref.getBoolean("material_you",true))
val blackTheme = mutableStateOf(sharedPref.getBoolean("black_theme", false))
OwnDroidTheme(materialYou.value, blackTheme.value) {
Auth(this@AuthFragment, promptInfo, callback, canStartAuth)
}
}
}
}
}
@Composable
fun Auth(activity: Fragment, promptInfo: Builder, callback: AuthenticationCallback, canStartAuth: MutableState<Boolean>) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background)
){
Text(
text = stringResource(R.string.authenticate),
style = MaterialTheme.typography.headlineLarge,
color = MaterialTheme.colorScheme.onBackground
)
LaunchedEffect(Unit){
fun AuthScreen(activity: FragmentActivity, showAuth: MutableState<Boolean>){
val context = activity.applicationContext
val coroutineScope = rememberCoroutineScope()
var canStartAuth by remember{ mutableStateOf(true) }
var fallback by remember{ mutableStateOf(false) }
var startFade by remember{ mutableStateOf(false) }
val alpha by animateFloatAsState(
targetValue = if(startFade) 0F else 1F,
label = "AuthScreenFade",
animationSpec = Animations.authScreenFade
)
val onAuthSucceed = {
startFade = true
coroutineScope.launch {
delay(300)
startAuth(activity, promptInfo, callback)
canStartAuth.value = false
showAuth.value = false
}
Button(
onClick = {
startAuth(activity, promptInfo, callback)
canStartAuth.value = false
},
enabled = canStartAuth.value
}
val promptInfo = Builder()
.setTitle(context.getText(R.string.authenticate))
.setSubtitle(context.getText(R.string.auth_with_bio))
.setConfirmationRequired(true)
val callback = object: AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
onAuthSucceed()
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
when(errorCode){
BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL -> onAuthSucceed()
BiometricPrompt.ERROR_NEGATIVE_BUTTON -> fallback = true
else -> canStartAuth = true
}
}
}
LaunchedEffect(fallback) {
if(fallback){
val fallbackPromptInfo = promptInfo
.setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL)
.setSubtitle(context.getText(R.string.auth_with_password))
.build()
val executor = ContextCompat.getMainExecutor(context)
val biometricPrompt = BiometricPrompt(activity, executor, callback)
biometricPrompt.authenticate(fallbackPromptInfo)
}
}
Surface(
modifier = Modifier
.fillMaxSize()
.alpha(alpha)
.background(if(isSystemInDarkTheme()) Color.Black else Color.White)
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background)
){
Text(text = stringResource(R.string.start))
LaunchedEffect(Unit){
delay(300)
startAuth(activity, promptInfo, callback)
canStartAuth = false
}
Text(
text = stringResource(R.string.authenticate),
style = MaterialTheme.typography.headlineLarge,
color = MaterialTheme.colorScheme.onBackground
)
Button(
onClick = {
startAuth(activity, promptInfo, callback)
canStartAuth = false
},
enabled = canStartAuth
){
Text(text = stringResource(R.string.start))
}
}
}
}
private fun startAuth(activity: Fragment, promptInfo: Builder, callback: AuthenticationCallback){
val context = activity.requireContext()
private fun startAuth(activity: FragmentActivity, basicPromptInfo: Builder, callback: AuthenticationCallback){
val context = activity.applicationContext
val promptInfo = basicPromptInfo
val bioManager = BiometricManager.from(context)
val sharedPref = context.getSharedPreferences("data", Context.MODE_PRIVATE)
if(sharedPref.getBoolean("bio_auth", false)){