From 867668832e052da16b26bb31e06febcc61d103f8 Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Sat, 14 Dec 2024 22:03:39 +0800 Subject: [PATCH] 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 --- .../main/java/com/bintianqi/owndroid/Auth.kt | 107 +++++------------- .../com/bintianqi/owndroid/MainActivity.kt | 51 ++++++--- .../bintianqi/owndroid/ManageSpaceActivity.kt | 56 ++++++--- .../java/com/bintianqi/owndroid/Settings.kt | 26 +++-- .../com/bintianqi/owndroid/ui/Animations.kt | 2 - app/src/main/res/values-ru/strings.xml | 5 - app/src/main/res/values-tr/strings.xml | 5 - app/src/main/res/values-zh-rCN/strings.xml | 5 - app/src/main/res/values/strings.xml | 5 - gradle/libs.versions.toml | 4 +- 10 files changed, 120 insertions(+), 146 deletions(-) diff --git a/app/src/main/java/com/bintianqi/owndroid/Auth.kt b/app/src/main/java/com/bintianqi/owndroid/Auth.kt index 39d1352..25a7d58 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Auth.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Auth.kt @@ -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) { - 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) { 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) { } } -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()) } diff --git a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt index c3bee73..b3fe7fe 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt @@ -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) diff --git a/app/src/main/java/com/bintianqi/owndroid/ManageSpaceActivity.kt b/app/src/main/java/com/bintianqi/owndroid/ManageSpaceActivity.kt index 100bbe4..c9e9f14 100644 --- a/app/src/main/java/com/bintianqi/owndroid/ManageSpaceActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/ManageSpaceActivity.kt @@ -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() 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)) } diff --git a/app/src/main/java/com/bintianqi/owndroid/Settings.kt b/app/src/main/java/com/bintianqi/owndroid/Settings.kt index 44f3b18..19f1924 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Settings.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Settings.kt @@ -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() } - ) } } diff --git a/app/src/main/java/com/bintianqi/owndroid/ui/Animations.kt b/app/src/main/java/com/bintianqi/owndroid/ui/Animations.kt index ba79fb1..90c364a 100644 --- a/app/src/main/java/com/bintianqi/owndroid/ui/Animations.kt +++ b/app/src/main/java/com/bintianqi/owndroid/ui/Animations.kt @@ -13,8 +13,6 @@ object Animations { private val tween: FiniteAnimationSpec = tween(durationMillis = 550, easing = bezier, delayMillis = 50) - val authScreenFade: FiniteAnimationSpec = tween(durationMillis = 200, easing = LinearEasing) - val navHostEnterTransition: AnimatedContentTransitionScope.() -> EnterTransition = { fadeIn(tween(100, easing = LinearEasing)) + slideIntoContainer( diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 523ba40..4dd6219 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -557,12 +557,7 @@ Заблокировать OwnDroid Аутентификация по биометрии Аутентифицировать - Использовать пароль - Аутентифицировать OwnDroid с помощью пароля - Аутентифицировать OwnDroid с помощью биометрии Блокировать при переключении в фоновый режим - Защитить хранилище - Хранилище защищено, вы не можете очистить хранилище OwnDroid Очистить хранилище API автоматизации diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 797be1a..ab987bb 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -552,12 +552,7 @@ OwnDroid\'u kilitle Biyometri ile doğrulama Doğrula - Şifre kullan - OwnDroid\'u şifre ile doğrula - OwnDroid\'u biyometri ile doğrula Arka plana geçince kilitle - Depolamayı koru - Depolama korunuyor Depolamayı temizle Automation API diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 8422449..bcdf09a 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -543,12 +543,7 @@ 锁定OwnDroid 使用生物识别 验证 - 使用密码 - 使用密码进行验证 - 使用生物识别进行验证 处于后台时锁定 - 保护存储空间 - 存储空间受到保护,你不能清除OwnDroid的存储空间 清除存储空间 自动化API diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 76c392f..286d2fb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -557,12 +557,7 @@ Lock OwnDroid Auth with biometrics Authenticate - Use password - Authenticate OwnDroid with password - Authenticate OwnDroid with biometrics Lock when switch to background - Protect storage - Storage is protected, you can\'t clear storage of OwnDroid Clear storage Automation API diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 61fa424..05db652 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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"