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" }