Refactor App lock

This commit is contained in:
BinTianqi
2025-03-09 20:11:24 +08:00
parent 10e34a4e96
commit 6ffe79fd8b
15 changed files with 252 additions and 255 deletions

View File

@@ -11,13 +11,11 @@ import android.content.pm.PackageInstaller
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.biometric.BiometricPrompt
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@@ -54,8 +52,10 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -63,6 +63,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.AndroidViewModel
@@ -100,7 +101,7 @@ class AppInstallerActivity:FragmentActivity() {
installing, options, { if(!installing) vm.options.value = it },
packages, { uri -> vm.packages.update { it.minus(uri) } },
{ uris -> vm.packages.update { it.plus(uris) } },
{ vm.startInstallationProcess(this) }, writtenPackages, writingPackage,
vm::startInstall, writtenPackages, writingPackage,
result, { vm.result.value = null }
)
}
@@ -118,12 +119,14 @@ private fun AppInstaller(
packages: Set<Uri> = setOf(Uri.parse("https://example.com")),
onPackageRemove: (Uri) -> Unit = {},
onPackageChoose: (List<Uri>) -> Unit = {},
onFabPressed: () -> Unit = {},
onStartInstall: () -> Unit = {},
writtenPackages: Set<Uri> = setOf(Uri.parse("https://example.com")),
writingPackage: Uri? = null,
result: Intent? = null,
onResultDialogClose: () -> Unit = {}
) {
val context = LocalContext.current
var appLockDialog by rememberSaveable { mutableStateOf(false) }
val coroutine = rememberCoroutineScope()
Scaffold(
topBar = {
@@ -138,7 +141,9 @@ private fun AppInstaller(
if(installing) CircularProgressIndicator(modifier = Modifier.size(24.dp))
else Icon(Icons.Default.PlayArrow, null)
},
onClick = onFabPressed,
onClick = {
if(SharedPrefs(context).lockPassword.isNullOrEmpty()) onStartInstall() else appLockDialog = true
},
expanded = !installing
)
}
@@ -175,6 +180,12 @@ private fun AppInstaller(
ResultDialog(result, onResultDialogClose)
}
}
if(appLockDialog) Dialog({ appLockDialog = false }) {
AppLockDialog({
appLockDialog = false
onStartInstall()
}) { appLockDialog = false }
}
}
@@ -311,20 +322,6 @@ class AppInstallerViewModel(application: Application): AndroidViewModel(applicat
val writtenPackages = MutableStateFlow(setOf<Uri>())
val writingPackage = MutableStateFlow<Uri?>(null)
fun startInstallationProcess(activity: FragmentActivity) {
val sp = SharedPrefs(getApplication<Application>())
if(sp.auth) startAuth(activity, object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
startInstall()
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
Toast.makeText(activity, R.string.failed_to_authenticate, Toast.LENGTH_SHORT).show()
}
})
else startInstall()
}
private fun getSessionParams(): PackageInstaller.SessionParams {
return PackageInstaller.SessionParams(options.value.mode).apply {
if(Build.VERSION.SDK_INT >= 34) {
@@ -334,7 +331,7 @@ class AppInstallerViewModel(application: Application): AndroidViewModel(applicat
setInstallLocation(options.value.location)
}
}
private fun startInstall() {
fun startInstall() {
if(installing.value) return
installing.value = true
viewModelScope.launch(Dispatchers.IO) {

View File

@@ -0,0 +1,97 @@
package com.bintianqi.owndroid
import android.content.Context
import android.hardware.biometrics.BiometricPrompt
import android.hardware.biometrics.BiometricPrompt.AuthenticationCallback
import android.os.Build
import android.os.CancellationSignal
import androidx.activity.compose.BackHandler
import androidx.annotation.RequiresApi
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import kotlinx.serialization.Serializable
@Serializable object AppLock
@Composable
fun AppLockDialog(onSucceed: () -> Unit, onDismiss: () -> Unit) {
val context = LocalContext.current
val fm = LocalFocusManager.current
val sp = SharedPrefs(context)
var input by remember { mutableStateOf("") }
var isError by remember { mutableStateOf(false) }
fun unlock() {
if(input == sp.lockPassword) {
fm.clearFocus()
onSucceed()
} else {
isError = true
}
}
BackHandler(onBack = onDismiss)
Card(Modifier.pointerInput(Unit) { detectTapGestures(onTap = { fm.clearFocus() }) }, shape = RoundedCornerShape(16.dp)) {
Column(Modifier.padding(12.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
OutlinedTextField(
input, { input = it; isError = false }, Modifier.width(200.dp),
label = { Text(stringResource(R.string.password)) }, isError = isError,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password, imeAction = if(input.length >= 4) ImeAction.Go else ImeAction.Done
),
keyboardActions = KeyboardActions({ fm.clearFocus() }, { unlock() })
)
if(Build.VERSION.SDK_INT >= 28 && sp.biometricsUnlock) {
FilledTonalIconButton({ startBiometricsUnlock(context, onSucceed) }, Modifier.padding(start = 4.dp)) {
Icon(painterResource(R.drawable.fingerprint_fill0), null)
}
}
}
Button(::unlock, Modifier.align(Alignment.End).padding(top = 8.dp), input.length >= 4) {
Text(stringResource(R.string.unlock))
}
}
}
}
@RequiresApi(28)
fun startBiometricsUnlock(context: Context, onSucceed: () -> Unit) {
val callback = object : AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult?) {
super.onAuthenticationSucceeded(result)
onSucceed()
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) {
super.onAuthenticationError(errorCode, errString)
if(errorCode != BiometricPrompt.BIOMETRIC_ERROR_CANCELED) context.showOperationResultToast(false)
}
}
val cancel = CancellationSignal()
BiometricPrompt.Builder(context)
.setTitle(context.getText(R.string.unlock))
.setNegativeButton(context.getString(R.string.cancel), context.mainExecutor) { _, _ -> cancel.cancel() }
.build()
.authenticate(cancel, context.mainExecutor, callback)
}

View File

@@ -1,92 +0,0 @@
package com.bintianqi.owndroid
import android.widget.Toast
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.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.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.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import kotlinx.coroutines.delay
import kotlinx.serialization.Serializable
@Serializable object Authenticate
@Composable
fun AuthenticateScreen(activity: FragmentActivity, onAuthSucceed: () -> Unit) {
val context = LocalContext.current
BackHandler { activity.moveTaskToBack(true) }
var status by rememberSaveable { mutableIntStateOf(0) } // 0:Prompt automatically, 1:Authenticating, 2:Prompt manually
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, BiometricPrompt.ERROR_NO_SPACE, BiometricPrompt.ERROR_HW_NOT_PRESENT,
BiometricPrompt.ERROR_VENDOR, BiometricPrompt.ERROR_NO_BIOMETRICS -> {
Toast.makeText(context, R.string.skipped_authentication, Toast.LENGTH_SHORT).show()
onAuthSucceed()
}
else -> status = 2
}
}
}
LaunchedEffect(Unit) {
if(status == 0) {
delay(300)
startAuth(activity, callback)
status = 1
}
}
Scaffold { paddingValues ->
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxSize().padding(paddingValues)
) {
Text(
text = stringResource(R.string.authenticate),
style = MaterialTheme.typography.headlineLarge,
)
Button(
onClick = {
startAuth(activity, callback)
status = 1
},
enabled = status != 1
) {
Text(text = stringResource(R.string.start))
}
}
}
}
fun startAuth(activity: FragmentActivity, callback: AuthenticationCallback) {
val context = activity.applicationContext
val promptInfo = Builder().setTitle(context.getText(R.string.authenticate))
if(SharedPrefs(context).biometricsAuth != 0) {
promptInfo.setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL or BiometricManager.Authenticators.BIOMETRIC_WEAK)
} else {
promptInfo.setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL)
}
val executor = ContextCompat.getMainExecutor(context)
BiometricPrompt(activity, executor, callback).authenticate(promptInfo.build())
}

View File

@@ -1,6 +1,7 @@
package com.bintianqi.owndroid
import android.annotation.SuppressLint
import android.app.Activity
import android.app.admin.DevicePolicyManager
import android.os.Build.VERSION
import android.os.Bundle
@@ -8,9 +9,6 @@ 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
@@ -49,6 +47,7 @@ import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import androidx.core.view.WindowCompat
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Lifecycle
@@ -58,6 +57,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.dialog
import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
import com.bintianqi.owndroid.dpm.Accounts
@@ -236,7 +236,7 @@ class MainActivity : FragmentActivity() {
setContent {
val theme by vm.theme.collectAsStateWithLifecycle()
OwnDroidTheme(theme) {
Home(this, vm)
Home(vm)
}
}
}
@@ -258,7 +258,7 @@ class MainActivity : FragmentActivity() {
@ExperimentalMaterial3Api
@Composable
fun Home(activity: FragmentActivity, vm: MyViewModel) {
fun Home(vm: MyViewModel) {
val navController = rememberNavController()
val context = LocalContext.current
val receiver = context.getReceiver()
@@ -395,24 +395,19 @@ fun Home(activity: FragmentActivity, vm: MyViewModel) {
val theme by vm.theme.collectAsStateWithLifecycle()
AppearanceScreen(::navigateUp, theme) { vm.theme.value = it }
}
composable<AuthSettings> { AuthSettingsScreen(::navigateUp) }
composable<AppLockSettings> { AppLockSettingsScreen(::navigateUp) }
composable<ApiSettings> { ApiSettings(::navigateUp) }
composable<Notifications> { NotificationsScreen(::navigateUp) }
composable<About> { AboutScreen(::navigateUp) }
composable<Authenticate>(
enterTransition = { fadeIn(animationSpec = tween(200)) },
popExitTransition = { fadeOut(animationSpec = tween(400)) }
) { AuthenticateScreen(activity, ::navigateUp) }
dialog<AppLock>(dialogProperties = DialogProperties(false, false)) {
AppLockDialog(::navigateUp) { (context as? Activity)?.moveTaskToBack(true) }
}
}
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
val sp = SharedPrefs(context)
if(
(event == Lifecycle.Event.ON_RESUME && sp.auth && sp.lockInBackground) ||
(event == Lifecycle.Event.ON_CREATE && sp.auth)
) {
navController.navigate(Authenticate) { launchSingleTop = true }
if(event == Lifecycle.Event.ON_CREATE && !SharedPrefs(context).lockPassword.isNullOrEmpty()) {
navController.navigate(AppLock)
}
}
lifecycleOwner.lifecycle.addObserver(observer)

View File

@@ -5,8 +5,6 @@ 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
@@ -15,7 +13,7 @@ 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.compose.ui.window.Dialog
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bintianqi.owndroid.ui.theme.OwnDroidTheme
@@ -24,67 +22,50 @@ import kotlin.system.exitProcess
class ManageSpaceActivity: FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
WindowCompat.setDecorFitsSystemWindows(window, false)
super.onCreate(savedInstanceState)
val authenticate = SharedPrefs(applicationContext).auth
val vm by viewModels<MyViewModel>()
fun clearStorage() {
filesDir.deleteRecursively()
cacheDir.deleteRecursively()
codeCacheDir.deleteRecursively()
if(Build.VERSION.SDK_INT >= 24) {
dataDir.resolve("shared_prefs").deleteRecursively()
} else {
val sharedPref = applicationContext.getSharedPreferences("data", MODE_PRIVATE)
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
}
}
}
val theme by vm.theme.collectAsStateWithLifecycle()
OwnDroidTheme(theme) {
AlertDialog(
text = {
Text(stringResource(R.string.clear_storage))
},
onDismissRequest = { finish() },
dismissButton = {
TextButton(onClick = { finish() }) {
Text(stringResource(R.string.cancel))
}
},
confirmButton = {
TextButton(
onClick = {
if(authenticate) {
authenticating = true
startAuth(this, callback)
} else {
clearStorage()
}
},
enabled = !authenticating
) {
Text(stringResource(R.string.confirm))
}
var appLockDialog by remember { mutableStateOf(!SharedPrefs(this).lockPassword.isNullOrEmpty()) }
if(appLockDialog) {
Dialog(::finish) {
AppLockDialog({ appLockDialog = false }, ::finish)
}
)
} else {
AlertDialog(
text = {
Text(stringResource(R.string.clear_storage))
},
onDismissRequest = ::finish,
dismissButton = {
TextButton(::finish) {
Text(stringResource(R.string.cancel))
}
},
confirmButton = {
TextButton(::clearStorage) {
Text(stringResource(R.string.confirm))
}
}
)
}
}
}
}
fun clearStorage() {
filesDir.deleteRecursively()
cacheDir.deleteRecursively()
codeCacheDir.deleteRecursively()
if(Build.VERSION.SDK_INT >= 24) {
dataDir.resolve("shared_prefs").deleteRecursively()
} else {
val sharedPref = applicationContext.getSharedPreferences("data", MODE_PRIVATE)
sharedPref.edit().clear().apply()
}
this.showOperationResultToast(true)
finish()
exitProcess(0)
}
}

View File

@@ -6,37 +6,48 @@ import android.net.Uri
import android.os.Build.VERSION
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.biometric.BiometricManager
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Switch
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
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import androidx.core.content.edit
import com.bintianqi.owndroid.ui.FunctionItem
import com.bintianqi.owndroid.ui.Notes
import com.bintianqi.owndroid.ui.MyScaffold
import com.bintianqi.owndroid.ui.Notes
import com.bintianqi.owndroid.ui.SwitchItem
import kotlinx.serialization.Serializable
import java.security.SecureRandom
@@ -55,7 +66,7 @@ fun SettingsScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) {
MyScaffold(R.string.settings, 0.dp, onNavigateUp) {
FunctionItem(title = R.string.options, icon = R.drawable.tune_fill0) { onNavigate(SettingsOptions) }
FunctionItem(title = R.string.appearance, icon = R.drawable.format_paint_fill0) { onNavigate(Appearance) }
FunctionItem(title = R.string.security, icon = R.drawable.lock_fill0) { onNavigate(AuthSettings) }
FunctionItem(R.string.app_lock, icon = R.drawable.lock_fill0) { onNavigate(AppLockSettings) }
FunctionItem(title = R.string.api, icon = R.drawable.apps_fill0) { onNavigate(ApiSettings) }
FunctionItem(R.string.notifications, icon = R.drawable.notifications_fill0) { onNavigate(Notifications) }
FunctionItem(title = R.string.export_logs, icon = R.drawable.description_fill0) {
@@ -139,39 +150,58 @@ fun AppearanceScreen(onNavigateUp: () -> Unit, currentTheme: ThemeSettings, onTh
}
}
@Serializable object AuthSettings
@Serializable object AppLockSettings
@Composable
fun AuthSettingsScreen(onNavigateUp: () -> Unit) {
val context = LocalContext.current
val sp = SharedPrefs(context)
var auth by remember{ mutableStateOf(sp.auth) }
MyScaffold(R.string.security, 0.dp, onNavigateUp) {
SwitchItem(
R.string.lock_owndroid, state = auth,
onCheckedChange = {
sp.auth = it
auth = it
}
fun AppLockSettingsScreen(onNavigateUp: () -> Unit) = MyScaffold(R.string.app_lock, 0.dp, onNavigateUp) {
val fm = LocalFocusManager.current
val sp = SharedPrefs(LocalContext.current)
var password by remember { mutableStateOf("") }
var confirmPassword by remember { mutableStateOf("") }
var allowBiometrics by remember { mutableStateOf(sp.biometricsUnlock) }
val fr = FocusRequester()
val alreadySet = !sp.lockPassword.isNullOrEmpty()
val isInputLegal = password.length !in 1..3 && (alreadySet || (password.isNotEmpty() && password.isNotBlank()))
Column(Modifier.widthIn(max = 300.dp).align(Alignment.CenterHorizontally)) {
OutlinedTextField(
password, { password = it }, Modifier.fillMaxWidth().padding(vertical = 4.dp),
label = { Text(stringResource(R.string.password)) },
supportingText = { Text(stringResource(if(alreadySet) R.string.leave_empty_to_remain_unchanged else R.string.minimum_length_4)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Next),
keyboardActions = KeyboardActions { fr.requestFocus() }
)
if(auth) {
var bioAuth by remember { mutableIntStateOf(sp.biometricsAuth) } // 0:Disabled, 1:Enabled 2:Force enabled
LaunchedEffect(Unit) {
val bioManager = BiometricManager.from(context)
if(bioManager.canAuthenticate(BiometricManager.Authenticators.DEVICE_CREDENTIAL) != BiometricManager.BIOMETRIC_SUCCESS) {
sp.biometricsAuth = 2
bioAuth = 2
}
}
SwitchItem(
R.string.enable_bio_auth, state = bioAuth != 0,
onCheckedChange = { bioAuth = if(it) 1 else 0; sp.biometricsAuth = bioAuth }, enabled = bioAuth != 2
)
SwitchItem(
R.string.lock_in_background,
getState = { sp.lockInBackground },
onCheckedChange = { sp.lockInBackground = it }
)
OutlinedTextField(
confirmPassword, { confirmPassword = it }, Modifier.fillMaxWidth().focusRequester(fr),
label = { Text(stringResource(R.string.confirm_password)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions { fm.clearFocus() }
)
if(VERSION.SDK_INT >= 28) Row(Modifier.fillMaxWidth().padding(vertical = 6.dp), Arrangement.SpaceBetween, Alignment.CenterVertically) {
Text(stringResource(R.string.allow_biometrics))
Switch(allowBiometrics, { allowBiometrics = it })
}
Button(
onClick = {
fm.clearFocus()
if(password.isNotEmpty()) sp.lockPassword = password
sp.biometricsUnlock = allowBiometrics
onNavigateUp()
},
modifier = Modifier.fillMaxWidth(),
enabled = isInputLegal && confirmPassword == password
) {
Text(stringResource(if(alreadySet) R.string.update else R.string.set))
}
if(alreadySet) FilledTonalButton(
onClick = {
fm.clearFocus()
sp.lockPassword = ""
sp.biometricsUnlock = false
onNavigateUp()
},
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.disable))
}
}
}

View File

@@ -14,13 +14,12 @@ class SharedPrefs(context: Context) {
var displayDangerousFeatures by BooleanSharedPref("display_dangerous_features")
var isApiEnabled by BooleanSharedPref("api.enabled")
var apiKey by StringSharedPref("api.key")
var auth by BooleanSharedPref("auth")
var biometricsAuth by IntSharedPref("auth.biometrics")
var lockInBackground by BooleanSharedPref("auth.lock_in_background")
var materialYou by BooleanSharedPref("theme.material_you", Build.VERSION.SDK_INT >= 31)
/** -1: follow system, 0: off, 1: on */
var darkTheme by IntSharedPref("theme.dark", -1)
var blackTheme by BooleanSharedPref("theme.black")
var lockPassword by StringSharedPref("lock.password")
var biometricsUnlock by BooleanSharedPref("lock.biometrics")
}
private class BooleanSharedPref(val key: String, val defValue: Boolean = false): ReadWriteProperty<SharedPrefs, Boolean> {