Fix some bugs of authentication

Display authentication screen in NavHost
Remove "Protect storage", authenticate to clear storage instead
Force enable biometrics on if using password alone is not supported
This commit is contained in:
BinTianqi
2024-12-14 22:03:39 +08:00
parent f7b18d1a31
commit 867668832e
10 changed files with 120 additions and 146 deletions

View File

@@ -1,55 +1,34 @@
package com.bintianqi.owndroid
import android.content.Context
import androidx.activity.compose.BackHandler
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.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.FragmentActivity
import com.bintianqi.owndroid.ui.Animations
import androidx.navigation.NavHostController
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@Composable
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)
showAuth.value = false
}
}
val promptInfo = Builder()
.setTitle(context.getText(R.string.authenticate))
.setSubtitle(context.getText(R.string.auth_with_bio))
.setConfirmationRequired(true)
fun Authenticate(activity: FragmentActivity, navCtrl: NavHostController) {
BackHandler { activity.moveTaskToBack(true) }
var status by rememberSaveable { mutableIntStateOf(0) } // 0:Prompt automatically, 1:Authenticating, 2:Prompt manually
val onAuthSucceed = { navCtrl.navigateUp() }
val callback = object: AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
@@ -59,49 +38,33 @@ fun AuthScreen(activity: FragmentActivity, showAuth: MutableState<Boolean>) {
super.onAuthenticationError(errorCode, errString)
when(errorCode) {
BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL -> onAuthSucceed()
BiometricPrompt.ERROR_NEGATIVE_BUTTON -> fallback = true
else -> canStartAuth = true
else -> status = 2
}
}
}
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)
LaunchedEffect(Unit) {
if(status == 0) {
delay(300)
startAuth(activity, callback)
status = 1
}
}
Surface(
modifier = Modifier
.fillMaxSize()
.alpha(alpha)
.background(if(isSystemInDarkTheme()) Color.Black else Color.White)
) {
Scaffold { paddingValues ->
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background)
modifier = Modifier.fillMaxSize().padding(paddingValues)
) {
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
startAuth(activity, callback)
status = 1
},
enabled = canStartAuth
enabled = status != 1
) {
Text(text = stringResource(R.string.start))
}
@@ -109,31 +72,15 @@ fun AuthScreen(activity: FragmentActivity, showAuth: MutableState<Boolean>) {
}
}
private fun startAuth(activity: FragmentActivity, basicPromptInfo: Builder, callback: AuthenticationCallback) {
fun startAuth(activity: FragmentActivity, 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)) {
when(BiometricManager.BIOMETRIC_SUCCESS) {
bioManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) ->
promptInfo
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
.setNegativeButtonText(context.getText(R.string.use_password))
bioManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) ->
promptInfo
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_WEAK)
.setNegativeButtonText(context.getText(R.string.use_password))
else -> promptInfo
.setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL)
.setSubtitle(context.getText(R.string.auth_with_password))
}
}else{
promptInfo
.setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL)
.setSubtitle(context.getText(R.string.auth_with_password))
val promptInfo = Builder().setTitle(context.getText(R.string.authenticate))
if(sharedPref.getInt("biometrics_auth", 0) != 0) {
promptInfo.setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL or BiometricManager.Authenticators.BIOMETRIC_WEAK)
} else {
promptInfo.setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL)
}
val executor = ContextCompat.getMainExecutor(context)
val biometricPrompt = BiometricPrompt(activity, executor, callback)
biometricPrompt.authenticate(promptInfo.build())
BiometricPrompt(activity, executor, callback).authenticate(promptInfo.build())
}

View File

@@ -8,6 +8,9 @@ import android.widget.Toast
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures
@@ -30,6 +33,7 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@@ -48,6 +52,9 @@ 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.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
@@ -77,7 +84,6 @@ import com.bintianqi.owndroid.dpm.Keyguard
import com.bintianqi.owndroid.dpm.LockScreenInfo
import com.bintianqi.owndroid.dpm.LockTaskMode
import com.bintianqi.owndroid.dpm.MTEPolicy
import com.bintianqi.owndroid.dpm.WorkProfile
import com.bintianqi.owndroid.dpm.NearbyStreamingPolicy
import com.bintianqi.owndroid.dpm.Network
import com.bintianqi.owndroid.dpm.NetworkLogging
@@ -115,6 +121,7 @@ import com.bintianqi.owndroid.dpm.WifiAuthKeypair
import com.bintianqi.owndroid.dpm.WifiSecurityLevel
import com.bintianqi.owndroid.dpm.WifiSsidPolicy
import com.bintianqi.owndroid.dpm.WipeData
import com.bintianqi.owndroid.dpm.WorkProfile
import com.bintianqi.owndroid.dpm.dhizukuErrorStatus
import com.bintianqi.owndroid.dpm.dhizukuPermissionGranted
import com.bintianqi.owndroid.dpm.getDPM
@@ -134,20 +141,16 @@ import kotlinx.coroutines.launch
import org.lsposed.hiddenapibypass.HiddenApiBypass
import java.util.Locale
var backToHomeStateFlow = MutableStateFlow(false)
val backToHomeStateFlow = MutableStateFlow(false)
@ExperimentalMaterial3Api
class MainActivity : FragmentActivity() {
private val showAuth = mutableStateOf(false)
override fun onCreate(savedInstanceState: Bundle?) {
registerActivityResult(this)
enableEdgeToEdge()
WindowCompat.setDecorFitsSystemWindows(window, false)
super.onCreate(savedInstanceState)
val context = applicationContext
val sharedPref = context.getSharedPreferences("data", MODE_PRIVATE)
if (VERSION.SDK_INT >= 28) HiddenApiBypass.setHiddenApiExemptions("")
if(sharedPref.getBoolean("auth", false)) showAuth.value = true
val locale = context.resources?.configuration?.locale
zhCN = locale == Locale.SIMPLIFIED_CHINESE || locale == Locale.CHINESE || locale == Locale.CHINA
toggleInstallAppActivity()
@@ -156,10 +159,7 @@ class MainActivity : FragmentActivity() {
lifecycleScope.launch { delay(5000); setDefaultAffiliationID(context) }
setContent {
OwnDroidTheme(vm) {
Home(vm)
if(showAuth.value) {
AuthScreen(this, showAuth)
}
Home(this, vm)
}
}
}
@@ -167,12 +167,6 @@ class MainActivity : FragmentActivity() {
override fun onResume() {
super.onResume()
val sharedPref = applicationContext.getSharedPreferences("data", MODE_PRIVATE)
if(
sharedPref.getBoolean("auth", false) &&
sharedPref.getBoolean("lock_in_background", false)
) {
showAuth.value = true
}
if (sharedPref.getBoolean("dhizuku", false)) {
if (Dhizuku.init(applicationContext)) {
if (!dhizukuPermissionGranted()) { dhizukuErrorStatus.value = 2 }
@@ -187,7 +181,7 @@ class MainActivity : FragmentActivity() {
@ExperimentalMaterial3Api
@Composable
fun Home(vm: MyViewModel) {
fun Home(activity: FragmentActivity, vm: MyViewModel) {
val navCtrl = rememberNavController()
val context = LocalContext.current
val dpm = context.getDPM()
@@ -196,6 +190,7 @@ fun Home(vm: MyViewModel) {
val focusMgr = LocalFocusManager.current
val dialogStatus = remember { mutableIntStateOf(0) }
val backToHome by backToHomeStateFlow.collectAsState()
val lifecycleOwner = LocalLifecycleOwner.current
LaunchedEffect(backToHome) {
if(backToHome) { navCtrl.navigateUp(); backToHomeStateFlow.value = false }
}
@@ -308,6 +303,28 @@ fun Home(vm: MyViewModel) {
composable(route = "About") { About(navCtrl) }
composable(route = "PackageSelector") { PackageSelector(navCtrl) }
composable(
route = "Authenticate",
enterTransition = { fadeIn(animationSpec = tween(200)) },
popExitTransition = { fadeOut(animationSpec = tween(400)) }
) { Authenticate(activity, navCtrl) }
}
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if(
(event == Lifecycle.Event.ON_RESUME &&
sharedPref.getBoolean("auth", false) &&
sharedPref.getBoolean("lock_in_background", false)) ||
(event == Lifecycle.Event.ON_CREATE && sharedPref.getBoolean("auth", false))
) {
navCtrl.navigate("Authenticate") { launchSingleTop = true }
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
LaunchedEffect(Unit) {
val profileInitialized = sharedPref.getBoolean("ManagedProfileActivated", false)

View File

@@ -5,9 +5,15 @@ import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.biometric.BiometricPrompt
import androidx.biometric.BiometricPrompt.AuthenticationCallback
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
import androidx.core.view.WindowCompat
import androidx.fragment.app.FragmentActivity
@@ -20,17 +26,40 @@ class ManageSpaceActivity: FragmentActivity() {
WindowCompat.setDecorFitsSystemWindows(window, false)
super.onCreate(savedInstanceState)
val sharedPref = applicationContext.getSharedPreferences("data", MODE_PRIVATE)
val protected = sharedPref.getBoolean("protect_storage", false)
val authenticate = sharedPref.getBoolean("auth", false)
val vm by viewModels<MyViewModel>()
if(!vm.initialized) vm.initialize(applicationContext)
fun clearStorage() {
filesDir.deleteRecursively()
cacheDir.deleteRecursively()
codeCacheDir.deleteRecursively()
if(Build.VERSION.SDK_INT >= 24) {
dataDir.resolve("shared_prefs").deleteRecursively()
} else {
sharedPref.edit().clear().apply()
}
finish()
exitProcess(0)
}
setContent {
var authenticating by remember { mutableStateOf(false) }
val callback = object: AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
clearStorage()
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
when(errorCode) {
BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL -> clearStorage()
else -> authenticating = false
}
}
}
OwnDroidTheme(vm) {
AlertDialog(
title = {
Text(stringResource(R.string.clear_storage))
},
text = {
if(protected) Text(stringResource(R.string.storage_is_protected))
Text(stringResource(R.string.clear_storage))
},
onDismissRequest = { finish() },
dismissButton = {
@@ -39,19 +68,16 @@ class ManageSpaceActivity: FragmentActivity() {
}
},
confirmButton = {
if(!protected) TextButton(
TextButton(
onClick = {
filesDir.deleteRecursively()
cacheDir.deleteRecursively()
codeCacheDir.deleteRecursively()
if(Build.VERSION.SDK_INT >= 24) {
dataDir.resolve("shared_prefs").deleteRecursively()
if(authenticate) {
authenticating = true
startAuth(this, callback)
} else {
sharedPref.edit().clear().apply()
clearStorage()
}
finish()
exitProcess(0)
}
},
enabled = !authenticating
) {
Text(stringResource(R.string.confirm))
}

View File

@@ -5,6 +5,7 @@ import android.content.Intent
import android.net.Uri
import android.os.Build.VERSION
import android.widget.Toast
import androidx.biometric.BiometricManager
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
@@ -19,7 +20,9 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@@ -117,7 +120,8 @@ fun Appearance(navCtrl: NavHostController, vm: MyViewModel) {
@Composable
fun AuthSettings(navCtrl: NavHostController) {
val sharedPref = LocalContext.current.getSharedPreferences("data", Context.MODE_PRIVATE)
val context = LocalContext.current
val sharedPref = context.getSharedPreferences("data", Context.MODE_PRIVATE)
var auth by remember{ mutableStateOf(sharedPref.getBoolean("auth",false)) }
MyScaffold(R.string.security, 0.dp, navCtrl) {
SwitchItem(
@@ -128,22 +132,24 @@ fun AuthSettings(navCtrl: NavHostController) {
}
)
if(auth) {
var bioAuth by remember { mutableIntStateOf(sharedPref.getInt("biometrics_auth", 0)) } // 0:Disabled, 1:Enabled 2:Force enabled
LaunchedEffect(Unit) {
val bioManager = BiometricManager.from(context)
if(bioManager.canAuthenticate(BiometricManager.Authenticators.DEVICE_CREDENTIAL) != BiometricManager.BIOMETRIC_SUCCESS) {
bioAuth = 2
sharedPref.edit().putInt("biometrics_auth", 2).apply()
}
}
SwitchItem(
R.string.enable_bio_auth, "", null,
{ sharedPref.getBoolean("bio_auth", false) },
{ sharedPref.edit().putBoolean("bio_auth", it).apply() }
R.string.enable_bio_auth, "", null, bioAuth != 0,
{ bioAuth = if(it) 1 else 0; sharedPref.edit().putInt("biometrics_auth", bioAuth).apply() }, bioAuth != 2
)
SwitchItem(
R.string.lock_in_background, stringResource(R.string.developing), null,
R.string.lock_in_background, "", null,
{ sharedPref.getBoolean("lock_in_background", false) },
{ sharedPref.edit().putBoolean("lock_in_background", it).apply() }
)
}
SwitchItem(
R.string.protect_storage, "", null,
{ sharedPref.getBoolean("protect_storage", false) },
{ sharedPref.edit().putBoolean("protect_storage", it).apply() }
)
}
}

View File

@@ -13,8 +13,6 @@ object Animations {
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 = {
fadeIn(tween(100, easing = LinearEasing)) +
slideIntoContainer(