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

@@ -92,7 +92,6 @@ dependencies {
implementation(libs.shizuku.provider)
implementation(libs.shizuku.api)
implementation(libs.dhizuku.api)
implementation(libs.androidx.biometric)
implementation(libs.androidx.fragment)
implementation(libs.hiddenApiBypass)
implementation(libs.serialization)

View File

@@ -9,6 +9,7 @@
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
<uses-sdk tools:overrideLibrary="rikka.shizuku.provider,rikka.shizuku.api,rikka.shizuku.shared,rikka.shizuku.aidl"/>
<application
android:dataExtractionRules="@xml/data_extraction_rules"
@@ -39,7 +40,6 @@
</activity>
<activity
android:name=".ManageSpaceActivity"
android:windowSoftInputMode="adjustResize|stateHidden"
android:theme="@style/Theme.Transparent">
</activity>
<activity

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,10 +22,38 @@ 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>()
setContent {
val theme by vm.theme.collectAsStateWithLifecycle()
OwnDroidTheme(theme) {
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()
@@ -38,53 +64,8 @@ class ManageSpaceActivity: FragmentActivity() {
val sharedPref = applicationContext.getSharedPreferences("data", MODE_PRIVATE)
sharedPref.edit().clear().apply()
}
this.showOperationResultToast(true)
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))
}
}
)
}
}
}
}

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> {

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M481,179q106,0 200,45.5T838,356q7,9 4.5,16t-8.5,12q-6,5 -14,4.5t-14,-8.5q-55,-78 -141.5,-119.5T481,219q-97,0 -182,41.5T158,380q-6,9 -14,10t-14,-4q-7,-5 -8.5,-12.5T126,358q62,-85 155.5,-132T481,179ZM481,273q135,0 232,90t97,223q0,50 -35.5,83.5T688,703q-51,0 -87.5,-33.5T564,586q0,-33 -24.5,-55.5T481,508q-34,0 -58.5,22.5T398,586q0,97 57.5,162T604,839q9,3 12,10t1,15q-2,7 -8,12t-15,3q-104,-26 -170,-103.5T358,586q0,-50 36,-84t87,-34q51,0 87,34t36,84q0,33 25,55.5t59,22.5q34,0 58,-22.5t24,-55.5q0,-116 -85,-195t-203,-79q-118,0 -203,79t-85,194q0,24 4.5,60t21.5,84q3,9 -0.5,16T208,755q-8,3 -15.5,-0.5T182,743q-15,-39 -21.5,-77.5T154,586q0,-133 96.5,-223T481,273ZM481,81q64,0 125,15.5T724,141q9,5 10.5,12t-1.5,14q-3,7 -10,11t-17,-1q-53,-27 -109.5,-41.5T481,121q-58,0 -114,13.5T260,177q-8,5 -16,2.5T232,169q-4,-8 -2,-14.5t10,-11.5q56,-30 117,-46t124,-16ZM481,370q93,0 160,62.5T708,586q0,9 -5.5,14.5T688,606q-8,0 -14,-5.5t-6,-14.5q0,-75 -55.5,-125.5T481,410q-76,0 -130.5,50.5T296,586q0,81 28,137.5T406,837q6,6 6,14t-6,14q-6,6 -14,6t-14,-6q-59,-62 -90.5,-126.5T256,586q0,-91 66,-153.5T481,370ZM480,566q9,0 14.5,6t5.5,14q0,75 54,123t126,48q6,0 17,-1t23,-3q9,-2 15.5,2.5T744,769q2,8 -3,14t-13,8q-18,5 -31.5,5.5t-16.5,0.5q-89,0 -154.5,-60T460,586q0,-8 5.5,-14t14.5,-6Z"
android:fillColor="#000000"/>
</vector>

View File

@@ -373,7 +373,6 @@
<string name="show_system_app">Показывать системные приложения</string>
<string name="suspend">Приостановить</string>
<string name="hide">Скрыть</string>
<string name="isapphidden_desc">Несуществующие приложения скрыты</string>
<string name="always_on_vpn">Постоянный VPN</string>
<string name="enable_lockdown">Включить блокировку</string>
<string name="clear_current_config">Очистить текущую конфигурацию</string>
@@ -598,14 +597,9 @@
<string name="project_homepage">Домашняя страница проекта</string>
<string name="appearance">Оформление</string>
<!--TODO: app lock-->
<string name="security">Безопасность</string>
<string name="lock_owndroid">Заблокировать OwnDroid</string>
<string name="enable_bio_auth">Аутентификация по биометрии</string>
<string name="authenticate">Аутентифицировать</string>
<string name="lock_in_background">Блокировать при переключении в фоновый режим</string>
<string name="clear_storage">Очистить хранилище</string>
<string name="skipped_authentication">Аутентификация пропущена, поскольку она недоступна.</string>
<string name="failed_to_authenticate">Ошибка аутентификации</string>
<string name="api_key">API ключ</string>
<string name="api_key_exist">API ключ уже сущетвует, установка нового перезапишет текущий</string>

View File

@@ -381,7 +381,6 @@
<string name="show_system_app">Sistem Uygulamalarını Göster</string>
<string name="suspend">Askıya Al</string>
<string name="hide">Gizle</string>
<string name="isapphidden_desc">Mevcut olmayan uygulamalar gizlidir</string>
<string name="always_on_vpn">Her Zaman Açık VPN</string>
<string name="enable_lockdown">Kilitlemeyi Etkinleştir</string>
<string name="clear_current_config">Mevcut Yapılandırmayı Temizle</string>
@@ -602,14 +601,9 @@
<string name="project_homepage">Proje Ana Sayfası</string>
<string name="appearance">Görünüm</string>
<!--TODO: App lock-->
<string name="security">Güvenlik</string>
<string name="lock_owndroid">OwnDroid\'i Kilitle</string>
<string name="enable_bio_auth">Biyometrik ile Kimlik Doğrulama</string>
<string name="authenticate">Kimlik Doğrula</string>
<string name="lock_in_background">Arka Plana Geçtiğinde Kilitle</string>
<string name="clear_storage">Depolamayı Temizle</string>
<string name="skipped_authentication">Kimlik doğrulama kullanılamadığı için atlandı.</string>
<string name="failed_to_authenticate">Kimlik doğrulama başarısız oldu</string>
<string name="api_key">API Anahtarı</string>
<string name="api_key_exist">API anahtarı zaten mevcut, yeni bir anahtar ayarlamak mevcut anahtarı geçersiz kılacaktır.</string>

View File

@@ -364,7 +364,6 @@
<string name="show_system_app">显示系统应用</string>
<string name="suspend">挂起</string>
<string name="hide">隐藏</string>
<string name="isapphidden_desc">如果隐藏,有可能是没安装</string>
<string name="always_on_vpn">VPN保持打开</string>
<string name="enable_lockdown">启用锁定</string>
<string name="clear_current_config">清除当前配置</string>
@@ -582,14 +581,13 @@
<string name="project_homepage">项目主页</string>
<string name="appearance">外观</string>
<string name="app_lock">应用锁</string>
<string name="minimum_length_4">长度不得小于4</string>
<string name="leave_empty_to_remain_unchanged">留空以保持未更改</string>
<string name="allow_biometrics">允许生物识别</string>
<string name="unlock">解锁</string>
<string name="security">安全性</string>
<string name="lock_owndroid">锁定OwnDroid</string>
<string name="enable_bio_auth">使用生物识别</string>
<string name="authenticate">验证</string>
<string name="lock_in_background">处于后台时锁定</string>
<string name="clear_storage">清除存储空间</string>
<string name="skipped_authentication">验证已跳过,因为不可用</string>
<string name="failed_to_authenticate">验证失败</string>
<string name="api_key">API密钥</string>
<string name="api_key_exist">API密钥已存在设置新的密钥将会覆盖当前密钥</string>

View File

@@ -400,7 +400,6 @@
<string name="show_system_app">Show system apps</string>
<string name="suspend">Suspend</string>
<string name="hide">Hide</string>
<string name="isapphidden_desc">Non-existent apps is hidden</string>
<string name="always_on_vpn">Always-on VPN</string>
<string name="enable_lockdown">Enable lockdown</string>
<string name="clear_current_config">Clear current config</string>
@@ -621,14 +620,13 @@
<string name="project_homepage">Project homepage</string>
<string name="appearance">Appearance</string>
<string name="app_lock">App lock</string>
<string name="minimum_length_4">The length must not be less than 4</string>
<string name="leave_empty_to_remain_unchanged">Leave empty to remain unchanged</string>
<string name="allow_biometrics">Allow biometrics</string>
<string name="unlock">Unlock</string>
<string name="security">Security</string>
<string name="lock_owndroid">Lock OwnDroid</string>
<string name="enable_bio_auth">Auth with biometrics</string>
<string name="authenticate">Authenticate</string>
<string name="lock_in_background">Lock when switch to background</string>
<string name="clear_storage">Clear storage</string>
<string name="skipped_authentication">Skipped authentication because it is unavailable.</string>
<string name="failed_to_authenticate">Failed to authenticate</string>
<string name="api_key">API key</string>
<string name="api_key_exist">The API key already exists, setting a new key will overwrite the current key.</string>

View File

@@ -7,7 +7,6 @@ composeBom = "2025.02.00"
accompanist-drawablepainter = "0.35.0-alpha"
accompanist-permissions = "0.37.0"
shizuku = "13.1.5"
biometric = "1.2.0-alpha05"
fragment = "1.8.6"
dhizuku = "2.5.3"
hiddenApiBypass = "4.3"
@@ -23,7 +22,6 @@ androidx-navigation-compose = { module = "androidx.navigation:navigation-compose
accompanist-drawablepainter = { module = "com.google.accompanist:accompanist-drawablepainter", version.ref = "accompanist-drawablepainter" }
accompanist-permissions = { group = "com.google.accompanist", name = "accompanist-permissions", version.ref = "accompanist-permissions" }
androidx-biometric = { group = "androidx.biometric", name = "biometric", version.ref = "biometric" }
shizuku-provider = { module = "dev.rikka.shizuku:provider", version.ref = "shizuku" }
shizuku-api = { module = "dev.rikka.shizuku:api", version.ref = "shizuku" }