diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 0b8b433..ecc2839 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -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)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 0aa7b3e..991bd5f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -9,6 +9,7 @@
+
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 = setOf(Uri.parse("https://example.com")),
onPackageRemove: (Uri) -> Unit = {},
onPackageChoose: (List) -> Unit = {},
- onFabPressed: () -> Unit = {},
+ onStartInstall: () -> Unit = {},
writtenPackages: Set = 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())
val writingPackage = MutableStateFlow(null)
- fun startInstallationProcess(activity: FragmentActivity) {
- val sp = SharedPrefs(getApplication())
- 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) {
diff --git a/app/src/main/java/com/bintianqi/owndroid/AppLock.kt b/app/src/main/java/com/bintianqi/owndroid/AppLock.kt
new file mode 100644
index 0000000..756fd4d
--- /dev/null
+++ b/app/src/main/java/com/bintianqi/owndroid/AppLock.kt
@@ -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)
+}
diff --git a/app/src/main/java/com/bintianqi/owndroid/Auth.kt b/app/src/main/java/com/bintianqi/owndroid/Auth.kt
deleted file mode 100644
index 6771a3c..0000000
--- a/app/src/main/java/com/bintianqi/owndroid/Auth.kt
+++ /dev/null
@@ -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())
-}
diff --git a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt
index a5b5d8e..0e8bda8 100644
--- a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt
+++ b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt
@@ -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 { AuthSettingsScreen(::navigateUp) }
+ composable { AppLockSettingsScreen(::navigateUp) }
composable { ApiSettings(::navigateUp) }
composable { NotificationsScreen(::navigateUp) }
composable { AboutScreen(::navigateUp) }
- composable(
- enterTransition = { fadeIn(animationSpec = tween(200)) },
- popExitTransition = { fadeOut(animationSpec = tween(400)) }
- ) { AuthenticateScreen(activity, ::navigateUp) }
+ dialog(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)
diff --git a/app/src/main/java/com/bintianqi/owndroid/ManageSpaceActivity.kt b/app/src/main/java/com/bintianqi/owndroid/ManageSpaceActivity.kt
index 1bf3f21..505b002 100644
--- a/app/src/main/java/com/bintianqi/owndroid/ManageSpaceActivity.kt
+++ b/app/src/main/java/com/bintianqi/owndroid/ManageSpaceActivity.kt
@@ -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()
- 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)
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/bintianqi/owndroid/Settings.kt b/app/src/main/java/com/bintianqi/owndroid/Settings.kt
index ed19e1f..6866ebb 100644
--- a/app/src/main/java/com/bintianqi/owndroid/Settings.kt
+++ b/app/src/main/java/com/bintianqi/owndroid/Settings.kt
@@ -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))
}
}
}
diff --git a/app/src/main/java/com/bintianqi/owndroid/SharedPrefs.kt b/app/src/main/java/com/bintianqi/owndroid/SharedPrefs.kt
index 6607bc0..8b84a0c 100644
--- a/app/src/main/java/com/bintianqi/owndroid/SharedPrefs.kt
+++ b/app/src/main/java/com/bintianqi/owndroid/SharedPrefs.kt
@@ -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 {
diff --git a/app/src/main/res/drawable/fingerprint_fill0.xml b/app/src/main/res/drawable/fingerprint_fill0.xml
new file mode 100644
index 0000000..2c68c10
--- /dev/null
+++ b/app/src/main/res/drawable/fingerprint_fill0.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index e183863..762c611 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -373,7 +373,6 @@
Показывать системные приложения
Приостановить
Скрыть
- Несуществующие приложения скрыты
Постоянный VPN
Включить блокировку
Очистить текущую конфигурацию
@@ -598,14 +597,9 @@
Домашняя страница проекта
Оформление
+
Безопасность
- Заблокировать OwnDroid
- Аутентификация по биометрии
- Аутентифицировать
- Блокировать при переключении в фоновый режим
Очистить хранилище
- Аутентификация пропущена, поскольку она недоступна.
- Ошибка аутентификации
API ключ
API ключ уже сущетвует, установка нового перезапишет текущий
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 27d3b72..1bf2a48 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -381,7 +381,6 @@
Sistem Uygulamalarını Göster
Askıya Al
Gizle
- Mevcut olmayan uygulamalar gizlidir
Her Zaman Açık VPN
Kilitlemeyi Etkinleştir
Mevcut Yapılandırmayı Temizle
@@ -602,14 +601,9 @@
Proje Ana Sayfası
Görünüm
+
Güvenlik
- OwnDroid\'i Kilitle
- Biyometrik ile Kimlik Doğrulama
- Kimlik Doğrula
- Arka Plana Geçtiğinde Kilitle
Depolamayı Temizle
- Kimlik doğrulama kullanılamadığı için atlandı.
- Kimlik doğrulama başarısız oldu
API Anahtarı
API anahtarı zaten mevcut, yeni bir anahtar ayarlamak mevcut anahtarı geçersiz kılacaktır.
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 5b51fb5..e30670d 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -364,7 +364,6 @@
显示系统应用
挂起
隐藏
- 如果隐藏,有可能是没安装
VPN保持打开
启用锁定
清除当前配置
@@ -582,14 +581,13 @@
项目主页
外观
+ 应用锁
+ 长度不得小于4
+ 留空以保持未更改
+ 允许生物识别
+ 解锁
安全性
- 锁定OwnDroid
- 使用生物识别
- 验证
- 处于后台时锁定
清除存储空间
- 验证已跳过,因为不可用
- 验证失败
API密钥
API密钥已存在,设置新的密钥将会覆盖当前密钥
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 571f1bb..7563be6 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -400,7 +400,6 @@
Show system apps
Suspend
Hide
- Non-existent apps is hidden
Always-on VPN
Enable lockdown
Clear current config
@@ -621,14 +620,13 @@
Project homepage
Appearance
+ App lock
+ The length must not be less than 4
+ Leave empty to remain unchanged
+ Allow biometrics
+ Unlock
Security
- Lock OwnDroid
- Auth with biometrics
- Authenticate
- Lock when switch to background
Clear storage
- Skipped authentication because it is unavailable.
- Failed to authenticate
API key
The API key already exists, setting a new key will overwrite the current key.
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 1b69022..a62d83c 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -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" }