Skip to content

Commit

Permalink
Fix some bugs of authentication
Browse files Browse the repository at this point in the history
Display authentication screen in NavHost
Remove "Protect storage", authenticate to clear storage instead
Force enable biometrics on if using password alone is not supported
  • Loading branch information
BinTianqi committed Dec 14, 2024
1 parent f7b18d1 commit 8676688
Show file tree
Hide file tree
Showing 10 changed files with 120 additions and 146 deletions.
107 changes: 27 additions & 80 deletions app/src/main/java/com/bintianqi/owndroid/Auth.kt
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -59,81 +38,49 @@ 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))
}
}
}
}

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())
}
51 changes: 34 additions & 17 deletions app/src/main/java/com/bintianqi/owndroid/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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()
Expand All @@ -156,23 +159,14 @@ class MainActivity : FragmentActivity() {
lifecycleScope.launch { delay(5000); setDefaultAffiliationID(context) }
setContent {
OwnDroidTheme(vm) {
Home(vm)
if(showAuth.value) {
AuthScreen(this, showAuth)
}
Home(this, vm)
}
}
}

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 }
Expand All @@ -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()
Expand All @@ -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 }
}
Expand Down Expand Up @@ -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)
Expand Down
56 changes: 41 additions & 15 deletions app/src/main/java/com/bintianqi/owndroid/ManageSpaceActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 = {
Expand All @@ -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))
}
Expand Down
Loading

0 comments on commit 8676688

Please sign in to comment.