mirror of
https://github.com/awfixers-stuff/OwnDroid.git
synced 2026-03-23 19:15:58 +00:00
Refactor App lock
This commit is contained in:
@@ -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) {
|
||||
|
||||
97
app/src/main/java/com/bintianqi/owndroid/AppLock.kt
Normal file
97
app/src/main/java/com/bintianqi/owndroid/AppLock.kt
Normal 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)
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user