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(

View File

@@ -557,12 +557,7 @@
<string name="lock_owndroid">Заблокировать OwnDroid</string>
<string name="enable_bio_auth">Аутентификация по биометрии</string>
<string name="authenticate">Аутентифицировать</string>
<string name="use_password">Использовать пароль</string>
<string name="auth_with_password">Аутентифицировать OwnDroid с помощью пароля</string>
<string name="auth_with_bio">Аутентифицировать OwnDroid с помощью биометрии</string>
<string name="lock_in_background">Блокировать при переключении в фоновый режим</string>
<string name="protect_storage">Защитить хранилище</string>
<string name="storage_is_protected">Хранилище защищено, вы не можете очистить хранилище OwnDroid</string>
<string name="clear_storage">Очистить хранилище</string>
<string name="automation_api">API автоматизации</string>

View File

@@ -552,12 +552,7 @@
<string name="lock_owndroid">OwnDroid\'u kilitle</string>
<string name="enable_bio_auth">Biyometri ile doğrulama</string>
<string name="authenticate">Doğrula</string>
<string name="use_password">Şifre kullan</string>
<string name="auth_with_password">OwnDroid\'u şifre ile doğrula</string>
<string name="auth_with_bio">OwnDroid\'u biyometri ile doğrula</string>
<string name="lock_in_background">Arka plana geçince kilitle</string>
<string name="protect_storage">Depolamayı koru</string>
<string name="storage_is_protected">Depolama korunuyor</string> <!--TODO-->
<string name="clear_storage">Depolamayı temizle</string>
<string name="automation_api">Automation API</string> <!--TODO-->

View File

@@ -543,12 +543,7 @@
<string name="lock_owndroid">锁定OwnDroid</string>
<string name="enable_bio_auth">使用生物识别</string>
<string name="authenticate">验证</string>
<string name="use_password">使用密码</string>
<string name="auth_with_password">使用密码进行验证</string>
<string name="auth_with_bio">使用生物识别进行验证</string>
<string name="lock_in_background">处于后台时锁定</string>
<string name="protect_storage">保护存储空间</string>
<string name="storage_is_protected">存储空间受到保护你不能清除OwnDroid的存储空间</string>
<string name="clear_storage">清除存储空间</string>
<string name="automation_api">自动化API</string>

View File

@@ -557,12 +557,7 @@
<string name="lock_owndroid">Lock OwnDroid</string>
<string name="enable_bio_auth">Auth with biometrics</string>
<string name="authenticate">Authenticate</string>
<string name="use_password">Use password</string>
<string name="auth_with_password">Authenticate OwnDroid with password</string>
<string name="auth_with_bio">Authenticate OwnDroid with biometrics</string>
<string name="lock_in_background">Lock when switch to background</string>
<string name="protect_storage">Protect storage</string>
<string name="storage_is_protected">Storage is protected, you can\'t clear storage of OwnDroid</string>
<string name="clear_storage">Clear storage</string>
<string name="automation_api">Automation API</string>

View File

@@ -2,8 +2,8 @@
agp = "8.7.3"
kotlin = "2.0.21"
navigation-compose = "2.8.4"
composeBom = "2024.11.00"
navigation-compose = "2.8.5"
composeBom = "2024.12.01"
accompanist-drawablepainter = "0.35.0-alpha"
shizuku = "13.1.5"
biometric = "1.2.0-alpha05"