mirror of
https://github.com/awfixers-stuff/OwnDroid.git
synced 2026-03-23 11:05:59 +00:00
Refactor App lock
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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> {
|
||||
|
||||
9
app/src/main/res/drawable/fingerprint_fill0.xml
Normal file
9
app/src/main/res/drawable/fingerprint_fill0.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" }
|
||||
|
||||
Reference in New Issue
Block a user