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

@@ -22,7 +22,7 @@ android {
minSdk = 21 minSdk = 21
targetSdk = 34 targetSdk = 34
versionCode = 28 versionCode = 28
versionName = "5.3" versionName = "5.3.1"
multiDexEnabled = false multiDexEnabled = false
} }

View File

@@ -1,131 +1,117 @@
package com.bintianqi.owndroid package com.bintianqi.owndroid
import android.annotation.SuppressLint
import android.content.Context 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.BiometricManager
import androidx.biometric.BiometricPrompt import androidx.biometric.BiometricPrompt
import androidx.biometric.BiometricPrompt.AuthenticationCallback import androidx.biometric.BiometricPrompt.AuthenticationCallback
import androidx.biometric.BiometricPrompt.PromptInfo.Builder import androidx.biometric.BiometricPrompt.PromptInfo.Builder
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.compose.ui.res.stringResource
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope import com.bintianqi.owndroid.ui.Animations
import com.bintianqi.owndroid.ui.theme.OwnDroidTheme
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch 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 @Composable
fun Auth(activity: Fragment, promptInfo: Builder, callback: AuthenticationCallback, canStartAuth: MutableState<Boolean>) { fun AuthScreen(activity: FragmentActivity, showAuth: MutableState<Boolean>){
Column( val context = activity.applicationContext
horizontalAlignment = Alignment.CenterHorizontally, val coroutineScope = rememberCoroutineScope()
verticalArrangement = Arrangement.Center, var canStartAuth by remember{ mutableStateOf(true) }
modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background) var fallback by remember{ mutableStateOf(false) }
){ var startFade by remember{ mutableStateOf(false) }
Text( val alpha by animateFloatAsState(
text = stringResource(R.string.authenticate), targetValue = if(startFade) 0F else 1F,
style = MaterialTheme.typography.headlineLarge, label = "AuthScreenFade",
color = MaterialTheme.colorScheme.onBackground animationSpec = Animations.authScreenFade
) )
LaunchedEffect(Unit){ val onAuthSucceed = {
startFade = true
coroutineScope.launch {
delay(300) delay(300)
startAuth(activity, promptInfo, callback) showAuth.value = false
canStartAuth.value = false
} }
Button( }
onClick = { val promptInfo = Builder()
startAuth(activity, promptInfo, callback) .setTitle(context.getText(R.string.authenticate))
canStartAuth.value = false .setSubtitle(context.getText(R.string.auth_with_bio))
}, .setConfirmationRequired(true)
enabled = canStartAuth.value 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){ private fun startAuth(activity: FragmentActivity, basicPromptInfo: Builder, callback: AuthenticationCallback){
val context = activity.requireContext() val context = activity.applicationContext
val promptInfo = basicPromptInfo
val bioManager = BiometricManager.from(context) val bioManager = BiometricManager.from(context)
val sharedPref = context.getSharedPreferences("data", Context.MODE_PRIVATE) val sharedPref = context.getSharedPreferences("data", Context.MODE_PRIVATE)
if(sharedPref.getBoolean("bio_auth", false)){ if(sharedPref.getBoolean("bio_auth", false)){

View File

@@ -6,11 +6,9 @@ import android.content.ComponentName
import android.content.Context import android.content.Context
import android.os.Build.VERSION import android.os.Build.VERSION
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@@ -29,14 +27,12 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
@@ -51,67 +47,43 @@ import java.util.Locale
var backToHome = false var backToHome = false
@ExperimentalMaterial3Api @ExperimentalMaterial3Api
class MainActivity : FragmentActivity() { class MainActivity : FragmentActivity() {
private var auth = false private val showAuth = mutableStateOf(false)
@SuppressLint("UnrememberedMutableState") @SuppressLint("UnrememberedMutableState")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
registerActivityResult(this) registerActivityResult(this)
enableEdgeToEdge() enableEdgeToEdge()
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.base)
val sharedPref = applicationContext.getSharedPreferences("data", Context.MODE_PRIVATE) val sharedPref = applicationContext.getSharedPreferences("data", Context.MODE_PRIVATE)
val fragmentManager = supportFragmentManager
val transaction = fragmentManager.beginTransaction()
transaction.add(R.id.base, HomeFragment(), "home")
if(sharedPref.getBoolean("auth", false)){ if(sharedPref.getBoolean("auth", false)){
transaction.add(R.id.base, AuthFragment(), "auth") showAuth.value = true
} }
transaction.commit()
val locale = applicationContext.resources?.configuration?.locale val locale = applicationContext.resources?.configuration?.locale
zhCN = locale==Locale.SIMPLIFIED_CHINESE||locale==Locale.CHINESE||locale==Locale.CHINA zhCN = locale==Locale.SIMPLIFIED_CHINESE||locale==Locale.CHINESE||locale==Locale.CHINA
setContent {
val materialYou = mutableStateOf(sharedPref.getBoolean("material_you",true))
val blackTheme = mutableStateOf(sharedPref.getBoolean("black_theme", false))
OwnDroidTheme(materialYou.value, blackTheme.value){
Home(materialYou, blackTheme)
if(showAuth.value){
AuthScreen(this, showAuth)
}
}
}
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
if(auth){ val sharedPref = applicationContext.getSharedPreferences("data", Context.MODE_PRIVATE)
val sharedPref = applicationContext.getSharedPreferences("data", Context.MODE_PRIVATE) if(
if( sharedPref.getBoolean("auth", false) &&
sharedPref.getBoolean("auth", false) && sharedPref.getBoolean("lock_in_background", false)
sharedPref.getBoolean("lock_in_background", false) ){
){ showAuth.value = true
val fragmentManager = supportFragmentManager
val fragment = fragmentManager.findFragmentByTag("auth")
if(fragment == null){
val transaction = fragmentManager.beginTransaction()
transaction.setCustomAnimations(R.anim.enter, R.anim.exit)
transaction.add(R.id.base, AuthFragment(), "auth").commit()
}
}
auth = false
} }
} }
override fun onRestart() {
super.onRestart()
auth = true
}
}
class HomeFragment: Fragment() {
@OptIn(ExperimentalMaterial3Api::class)
@SuppressLint("UnrememberedMutableState")
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val sharedPref = context?.getSharedPreferences("data", Context.MODE_PRIVATE)!!
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){
Home(materialYou, blackTheme)
}
}
}
}
} }
@SuppressLint("UnrememberedMutableState") @SuppressLint("UnrememberedMutableState")

View File

@@ -0,0 +1,84 @@
package com.bintianqi.owndroid
import android.content.Context
import android.os.Bundle
import android.widget.Toast
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.view.WindowCompat
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import com.bintianqi.owndroid.ui.theme.OwnDroidTheme
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class ManageSpaceActivity: FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
WindowCompat.setDecorFitsSystemWindows(window, false)
super.onCreate(savedInstanceState)
val sharedPref = applicationContext.getSharedPreferences("data", Context.MODE_PRIVATE)
val materialYou = sharedPref.getBoolean("material_you",true)
val blackTheme = sharedPref.getBoolean("black_theme", false)
setContent {
OwnDroidTheme(materialYou, blackTheme) {
ManageSpace()
}
}
}
@Composable
fun ManageSpace() {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background)
) {
val sharedPref = applicationContext.getSharedPreferences("data", Context.MODE_PRIVATE)
val protected = sharedPref.getBoolean("protect_storage", false)
if(protected){
Text(
text = stringResource(R.string.storage_is_protected),
color = MaterialTheme.colorScheme.onBackground,
style = MaterialTheme.typography.titleMedium
)
Text(
text = stringResource(R.string.you_cant_clear_storage),
color = MaterialTheme.colorScheme.onBackground
)
}
Button(
onClick = {
lifecycleScope.launch {
sharedPref.edit().clear().apply()
delay(2000)
finishAndRemoveTask()
}
Toast.makeText(applicationContext, R.string.clear_storage_success, Toast.LENGTH_SHORT).show()
},
enabled = !protected,
colors = ButtonDefaults.buttonColors(
contentColor = MaterialTheme.colorScheme.onError,
containerColor = MaterialTheme.colorScheme.error
),
modifier = Modifier.padding(top = 10.dp)
) {
Text(text = stringResource(R.string.clear_storage))
}
}
}
}

View File

@@ -112,7 +112,7 @@ private fun AuthSettings(){
{ sharedPref.edit().putBoolean("bio_auth",it).apply() } { sharedPref.edit().putBoolean("bio_auth",it).apply() }
) )
SwitchItem( SwitchItem(
R.string.lock_in_background, "", null, R.string.lock_in_background, stringResource(R.string.developing), null,
{ sharedPref.getBoolean("lock_in_background",false) }, { sharedPref.getBoolean("lock_in_background",false) },
{ sharedPref.edit().putBoolean("lock_in_background",it).apply() } { sharedPref.edit().putBoolean("lock_in_background",it).apply() }
) )

View File

@@ -11,7 +11,9 @@ object Animations{
private val bezier = CubicBezierEasing(0.20f, 0.85f, 0.0f, 1f) private val bezier = CubicBezierEasing(0.20f, 0.85f, 0.0f, 1f)
private val tween: FiniteAnimationSpec<IntOffset> = tween(durationMillis = 650, easing = bezier, delayMillis = 50) private val tween: FiniteAnimationSpec<IntOffset> = tween(durationMillis = 550, easing = bezier, delayMillis = 50)
val authScreenFade: FiniteAnimationSpec<Float> = tween(durationMillis = 200, easing = LinearEasing)
val navHostEnterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition = { val navHostEnterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition = {
fadeIn(tween(100, easing = LinearEasing)) + fadeIn(tween(100, easing = LinearEasing)) +

View File

@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<scale
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:fromXScale="0.95"
android:toXScale="1.0"
android:fromYScale="0.95"
android:toYScale="1.0"
android:pivotX="50%"
android:pivotY="50%"
android:fillAfter="false"
android:duration="200" />
<alpha
android:duration="150"
android:fromAlpha="0.0"
android:interpolator="@android:anim/accelerate_interpolator"
android:toAlpha="1.0" />
</set>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:duration="250"
android:fromAlpha="1.0"
android:interpolator="@android:anim/accelerate_interpolator"
android:toAlpha="0.0" />
</set>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/base"
android:layout_width="match_parent"
android:layout_height="match_parent">
</FrameLayout>

View File

@@ -38,7 +38,7 @@
<string name="apply">Apply</string> <string name="apply">Apply</string>
<string name="decide_by_user">Decide by user</string> <string name="decide_by_user">Decide by user</string>
<string name="unsupported">Unsupported</string> <string name="unsupported">Unsupported</string>
<string name="developing">Developing</string> <string name="developing">Developing function</string>
<string name="unknown_effect">Unknown effect</string> <string name="unknown_effect">Unknown effect</string>
<string name="options">Options</string> <string name="options">Options</string>
<string name="copy_command">Copy Command</string> <string name="copy_command">Copy Command</string>