diff options
| author | Danilo M. <danix@danix.xyz> | 2026-11-03 18:39:12 +0100 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-11-03 18:39:12 +0100 |
| commit | f58576017be0a280523a75a3580094ffac4ee7bd (patch) | |
| tree | 8620c70eb54020f5be5eaa62c8332843aee13350 /app/src/main/java | |
| parent | b814da3b2328a05b2fe94a5ae653cc3288f68fff (diff) | |
| download | HardenedPass-f58576017be0a280523a75a3580094ffac4ee7bd.tar.gz HardenedPass-f58576017be0a280523a75a3580094ffac4ee7bd.zip | |
feat(app): add the Compose settings screens
The categories and one screen per category, with the old rows over the
same keys. Screen lock gated switches store their value only after a
successful prompt. Clearing a saved HTTPS password or SSH passphrase,
which never removed anything, now asks and removes it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'app/src/main/java')
| -rw-r--r-- | app/src/main/java/app/passwordstore/ui/settings/SettingsDestination.kt | 297 | ||||
| -rw-r--r-- | app/src/main/java/app/passwordstore/ui/settings/SettingsScreen.kt | 348 |
2 files changed, 645 insertions, 0 deletions
diff --git a/app/src/main/java/app/passwordstore/ui/settings/SettingsDestination.kt b/app/src/main/java/app/passwordstore/ui/settings/SettingsDestination.kt new file mode 100644 index 00000000..1885c1ee --- /dev/null +++ b/app/src/main/java/app/passwordstore/ui/settings/SettingsDestination.kt @@ -0,0 +1,297 @@ +/* + * Copyright (C) 2026 Danilo M. <danix@danix.xyz> + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.ui.settings + +import android.content.Context +import android.content.Intent +import android.content.pm.ShortcutManager +import android.net.Uri +import android.provider.Settings +import androidx.activity.compose.BackHandler +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.core.content.getSystemService +import androidx.core.net.toUri +import androidx.documentfile.provider.DocumentFile +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.LifecycleResumeEffect +import app.passwordstore.BuildConfig +import app.passwordstore.R +import app.passwordstore.ui.compose.theme.SpacingLarge +import app.passwordstore.ui.main.MainActivity +import app.passwordstore.ui.setup.SetupDialog +import app.passwordstore.util.auth.BiometricAuthenticator +import app.passwordstore.util.auth.BiometricAuthenticator.Result +import app.passwordstore.util.extensions.autofillManager +import app.passwordstore.util.extensions.snackbar +import app.passwordstore.util.features.Feature +import app.passwordstore.util.services.PasswordExportService +import app.passwordstore.util.settings.PreferenceKeys +import com.github.androidpasswordstore.autofillparser.BrowserAutofillSupportLevel +import com.github.androidpasswordstore.autofillparser.getInstalledBrowsersWithAutofillSupportLevel + +private enum class SettingsDialog { + Autofill, + ClearSecret, + DeleteRepository, +} + +/** The export's folder picker, with the persistable write grant `PasswordExportService` needs. */ +private class ExportFolder : ActivityResultContracts.OpenDocumentTree() { + override fun createIntent(context: Context, input: Uri?): Intent = + super.createIntent(context, input).apply { + flags = + Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION or + Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or + Intent.FLAG_GRANT_PREFIX_URI_PERMISSION + } +} + +private fun MainActivity.autofillEnabled() = autofillManager?.hasEnabledAutofillServices() == true + +/** + * One settings category: wires [SettingsSectionScreen] to the [viewModel] and to what needs the + * activity: screen lock prompts, the autofill service, the export picker, confirmations. [onOpen] + * gets the actions that open another screen; [onDeleted] runs once the store was deleted. + */ +@Composable +fun SettingsSectionDestination( + activity: MainActivity, + section: SettingsSection, + onBack: () -> Unit, + onOpen: (SettingsAction) -> Unit, + onDeleted: () -> Unit, + modifier: Modifier = Modifier, + viewModel: SettingsViewModel = hiltViewModel(), +) { + var dialog by rememberSaveable { mutableStateOf<SettingsDialog?>(null) } + var canAuthenticate by remember { + mutableStateOf(BiometricAuthenticator.canAuthenticate(activity)) + } + var autofillEnabled by remember { mutableStateOf(activity.autofillEnabled()) } + + fun refresh() { + viewModel.refresh() + canAuthenticate = BiometricAuthenticator.canAuthenticate(activity) + autofillEnabled = activity.autofillEnabled() + } + + // Back from another screen (git server, SSH key, key generation, the system's autofill + // settings): rows that depend on it follow, e.g. "View generated public SSH key". + LifecycleResumeEffect(Unit) { + refresh() + onPauseOrDispose {} + } + // A finished deletion moves on, also when the screen was recreated in between. + val deleted = viewModel.deleted + LaunchedEffect(deleted) { + if (deleted == null) return@LaunchedEffect + if (!deleted) activity.snackbar(message = activity.getString(R.string.store_delete_failed)) + onDeleted() + } + BackHandler(enabled = viewModel.deleting) {} + + val export = + rememberLauncherForActivityResult(ExportFolder()) { uri -> + if (uri != null && DocumentFile.fromTreeUri(activity, uri) != null) + activity.startForegroundService( + Intent(activity, PasswordExportService::class.java) + .setAction(PasswordExportService.ACTION_EXPORT_PASSWORD) + .putExtra("uri", uri) + ) + } + + // One screen lock prompt at a time: a double tap must not open two. A recreation resets it + // (remember), and the old activity's prompt is gone with it. + var prompting by remember { mutableStateOf(false) } + + fun authenticate(@StringRes title: Int, onResult: (Result) -> Unit) { + if (prompting) return + prompting = true + BiometricAuthenticator.authenticate(activity, title) { result -> + // Retry: a finger was not recognized, the prompt stays open. + if (result !is Result.Retry) prompting = false + onResult(result) + } + } + + // Values are written only once allowed: a failed or cancelled prompt leaves the stored value, + // which the switch shows. + fun setBoolean(key: String, value: Boolean) { + when (key) { + PreferenceKeys.AUTOFILL_ENABLE -> + if (value) dialog = SettingsDialog.Autofill + else { + activity.autofillManager?.disableAutofillServices() + refresh() + } + PreferenceKeys.BIOMETRIC_AUTH_2 -> + authenticate(R.string.biometric_prompt_title) { result -> + if (result is Result.Success) { + viewModel.putBoolean(key, value) + // Shortcuts were built for the old lock setting, as the old screen knew. + activity.getSystemService<ShortcutManager>()?.removeAllDynamicShortcuts() + } + } + Feature.EnablePGPPassphraseCache.configKey -> + if (!BiometricAuthenticator.canAuthenticate(activity)) viewModel.setPassphraseCache(false) + else + authenticate( + if (value) R.string.pref_passphrase_cache_authenticate_enable + else R.string.pref_passphrase_cache_authenticate_disable + ) { result -> + if (result is Result.Success) viewModel.setPassphraseCache(value) + } + PreferenceKeys.CLEAR_PASSPHRASE_CACHE -> + when { + value -> viewModel.putBoolean(key, true) + !BiometricAuthenticator.canAuthenticate(activity) -> viewModel.resetAutoClear() + else -> + authenticate(R.string.pref_passphrase_cache_auto_clear_authenticate_disable) { result -> + when (result) { + is Result.Success -> viewModel.disableAutoClear() + is Result.Retry -> Unit + else -> viewModel.resetAutoClear() + } + } + } + else -> viewModel.putBoolean(key, value) + } + } + + Box(modifier) { + SettingsSectionScreen( + section = section, + state = viewModel.state, + canAuthenticate = canAuthenticate, + autofillEnabled = autofillEnabled, + deleting = viewModel.deleting, + // Nothing else starts while the store is being deleted. + onBoolean = { key, value -> if (!viewModel.deleting) setBoolean(key, value) }, + onString = { key, value -> if (!viewModel.deleting) viewModel.putString(key, value) }, + onAction = { action -> + if (!viewModel.deleting) + when (action) { + SettingsAction.ClearSecret -> dialog = SettingsDialog.ClearSecret + SettingsAction.DeleteRepository -> dialog = SettingsDialog.DeleteRepository + SettingsAction.Export -> export.launch(null) + else -> onOpen(action) + } + }, + onBack = { if (!viewModel.deleting) onBack() }, + ) + when (dialog) { + null -> Unit + SettingsDialog.Autofill -> + AutofillDialog( + onConfirm = { + dialog = null + activity.startActivity( + Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE) + .setData("package:${BuildConfig.APPLICATION_ID}".toUri()) + ) + }, + onDismiss = { dialog = null }, + ) + SettingsDialog.ClearSecret -> + SetupDialog( + title = viewModel.state.savedSecret?.let { stringResource(it) }, + text = stringResource(R.string.clear_saved_secret_message), + confirm = stringResource(R.string.dialog_ok), + onConfirm = { + dialog = null + viewModel.clearSavedSecret() + }, + onDismiss = { dialog = null }, + dismiss = stringResource(R.string.dialog_cancel), + ) + SettingsDialog.DeleteRepository -> + SetupDialog( + title = stringResource(R.string.pref_dialog_delete_title), + text = stringResource(R.string.store_delete_confirm), + confirm = stringResource(R.string.dialog_delete), + onConfirm = { + dialog = null + viewModel.deleteRepository() + }, + onDismiss = { dialog = null }, + dismiss = stringResource(R.string.dialog_do_not_delete), + ) + } + } +} + +/** The old autofill instructions: what it does, how to enable it, and the installed browsers. */ +@Composable +private fun AutofillDialog( + onConfirm: () -> Unit, + onDismiss: () -> Unit, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + val browsers = remember { getInstalledBrowsersWithAutofillSupportLevel(context) } + AlertDialog( + onDismissRequest = onDismiss, + modifier = modifier, + title = { Text(stringResource(R.string.pref_autofill_enable_title)) }, + text = { + Column( + Modifier.verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(SpacingLarge), + ) { + Text(stringResource(R.string.oreo_autofill_enable_dialog_description)) + Text(stringResource(R.string.oreo_autofill_enable_dialog_instructions)) + Column { + Text( + stringResource(R.string.oreo_autofill_enable_dialog_installed_browsers), + fontWeight = FontWeight.Bold, + ) + browsers.forEach { (label, level) -> Text("$label: ${stringResource(level.label())}") } + } + } + }, + confirmButton = { + TextButton(onClick = onConfirm) { Text(stringResource(R.string.dialog_ok)) } + }, + dismissButton = { + TextButton(onClick = onDismiss) { Text(stringResource(R.string.dialog_cancel)) } + }, + ) +} + +@StringRes +private fun BrowserAutofillSupportLevel.label(): Int = + when (this) { + BrowserAutofillSupportLevel.None -> R.string.oreo_autofill_no_support + BrowserAutofillSupportLevel.FlakyFill -> R.string.oreo_autofill_flaky_fill_support + BrowserAutofillSupportLevel.PasswordFill -> R.string.oreo_autofill_password_fill_support + BrowserAutofillSupportLevel.PasswordFillAndSaveIfNoAccessibility -> + R.string.oreo_autofill_password_fill_and_conditional_save_support + BrowserAutofillSupportLevel.GeneralFill -> R.string.oreo_autofill_general_fill_support + BrowserAutofillSupportLevel.GeneralFillAndSave -> + R.string.oreo_autofill_general_fill_and_save_support + } diff --git a/app/src/main/java/app/passwordstore/ui/settings/SettingsScreen.kt b/app/src/main/java/app/passwordstore/ui/settings/SettingsScreen.kt new file mode 100644 index 00000000..2327551c --- /dev/null +++ b/app/src/main/java/app/passwordstore/ui/settings/SettingsScreen.kt @@ -0,0 +1,348 @@ +/* + * Copyright (C) 2026 Danilo M. <danix@danix.xyz> + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.ui.settings + +import androidx.annotation.DrawableRes +import androidx.annotation.Keep +import androidx.annotation.StringRes +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringArrayResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import app.passwordstore.BuildConfig +import app.passwordstore.R +import app.passwordstore.ui.compose.GroupedSurface +import app.passwordstore.util.features.Feature +import app.passwordstore.util.settings.PreferenceKeys +import kotlinx.collections.immutable.persistentListOf +import kotlinx.serialization.Serializable + +/** The settings categories, one screen each. A route argument: kept from minification. */ +@Keep +@Serializable +enum class SettingsSection(@StringRes val title: Int, @DrawableRes val icon: Int) { + General(R.string.pref_category_general_title, R.drawable.app_settings_alt_24px), + Autofill(R.string.pref_category_autofill_title, R.drawable.ic_wysiwyg_24px), + Passwords(R.string.pref_category_passwords_title, R.drawable.ic_password_24px), + Repository(R.string.pref_category_repository_title, R.drawable.ic_call_merge_24px), + Misc(R.string.pref_category_misc_title, R.drawable.ic_miscellaneous_services_24px), + Pgp(R.string.pref_category_pgp_title, R.drawable.ic_lock_open_24px), +} + +/** What a row asks for beyond storing a value: another screen, a dialog or a picker. */ +enum class SettingsAction { + GitServer, + Proxy, + GitConfig, + SshKey, + SshKeyGen, + KeyManager, + ClearSecret, + DeleteRepository, + Export, +} + +/** Top bar with [title] and back, then [content] in one grouped surface, scrolling. */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun SettingsScaffold( + title: String, + onBack: () -> Unit, + modifier: Modifier = Modifier, + content: @Composable ColumnScope.() -> Unit, +) { + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + title = { Text(title) }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.entry_back)) + } + }, + ) + }, + ) { padding -> + Column(Modifier.padding(padding).fillMaxSize().verticalScroll(rememberScrollState())) { + GroupedSurface(content = content) + } + } +} + +/** The settings categories. */ +@Composable +fun SettingsScreen( + onSection: (SettingsSection) -> Unit, + onBack: () -> Unit, + modifier: Modifier = Modifier, +) { + SettingsScaffold(stringResource(R.string.action_settings), onBack, modifier) { + SettingsSection.entries.forEach { section -> + ListItem( + headlineContent = { Text(stringResource(section.title)) }, + modifier = Modifier.clickable { onSection(section) }, + leadingContent = { Icon(painterResource(section.icon), contentDescription = null) }, + colors = rowColors(), + ) + } + } +} + +/** + * One category's rows. Switches and values go out through [onBoolean] and [onString] with their + * preference key (the destination decides which need a prompt first); everything else through + * [onAction]. [canAuthenticate] and [autofillEnabled] come from the activity. + */ +@Composable +fun SettingsSectionScreen( + section: SettingsSection, + state: SettingsState, + canAuthenticate: Boolean, + autofillEnabled: Boolean, + deleting: Boolean, + onBoolean: (key: String, value: Boolean) -> Unit, + onString: (key: String, value: String) -> Unit, + onAction: (SettingsAction) -> Unit, + onBack: () -> Unit, + modifier: Modifier = Modifier, +) { + SettingsScaffold(stringResource(section.title), onBack, modifier) { + when (section) { + SettingsSection.General -> { + PreferenceChoice( + title = stringResource(R.string.pref_app_theme_title), + values = persistentListOf(*stringArrayResource(R.array.app_theme_values)), + labels = persistentListOf(*stringArrayResource(R.array.app_theme_options)), + selected = state.theme, + onSelect = { onString(PreferenceKeys.APP_THEME, it) }, + ) + PreferenceChoice( + title = stringResource(R.string.pref_directory_structure), + values = persistentListOf(*stringArrayResource(R.array.directory_structure_values)), + labels = persistentListOf(*stringArrayResource(R.array.directory_structure_entries)), + selected = state.directoryStructure, + onSelect = { onString(PreferenceKeys.DIRECTORY_STRUCTURE, it) }, + ) + PreferenceChoice( + title = stringResource(R.string.pref_sort_order_title), + values = persistentListOf(*stringArrayResource(R.array.sort_order_values)), + labels = persistentListOf(*stringArrayResource(R.array.sort_order_entries)), + selected = state.sortOrder, + onSelect = { onString(PreferenceKeys.SORT_ORDER, it) }, + ) + PreferenceSwitch( + title = stringResource(R.string.pref_disable_sync_on_pull_title), + checked = state.disableSync, + onCheckedChange = { onBoolean(PreferenceKeys.DISABLE_SYNC_ACTION, it) }, + summary = stringResource(R.string.pref_disable_sync_on_pull_summary), + ) + PreferenceSwitch( + title = stringResource(R.string.pref_recursive_filter_title), + checked = state.filterRecursively, + onCheckedChange = { onBoolean(PreferenceKeys.FILTER_RECURSIVELY, it) }, + summary = stringResource(R.string.pref_recursive_filter_summary), + ) + PreferenceSwitch( + title = stringResource(R.string.pref_search_on_start_title), + checked = state.searchOnStart, + onCheckedChange = { onBoolean(PreferenceKeys.SEARCH_ON_START, it) }, + summary = stringResource(R.string.pref_search_on_start_summary), + ) + PreferenceSwitch( + title = stringResource(R.string.pref_show_hidden_title), + checked = state.showHidden, + onCheckedChange = { onBoolean(PreferenceKeys.SHOW_HIDDEN_CONTENTS, it) }, + summary = stringResource(R.string.pref_show_hidden_summary), + ) + // Enabled on Android 14 too (maintainer decision 4): upstream blocked it there for issue + // 2802; if that platform bug shows on the maintainer's Android 14 tablet, the block + // returns. + PreferenceSwitch( + title = stringResource(R.string.pref_biometric_auth_title), + checked = state.biometric, + onCheckedChange = { onBoolean(PreferenceKeys.BIOMETRIC_AUTH_2, it) }, + summary = + stringResource( + if (canAuthenticate) R.string.pref_biometric_auth_summary + else R.string.pref_biometric_auth_summary_error + ), + ) + } + SettingsSection.Autofill -> { + PreferenceSwitch( + title = stringResource(R.string.pref_autofill_enable_title), + checked = autofillEnabled, + onCheckedChange = { onBoolean(PreferenceKeys.AUTOFILL_ENABLE, it) }, + ) + PreferenceText( + title = stringResource(R.string.preference_default_username_title), + value = state.defaultUsername, + onValueChange = { onString(PreferenceKeys.OREO_AUTOFILL_DEFAULT_USERNAME, it) }, + summary = stringResource(R.string.preference_default_username_summary), + enabled = autofillEnabled, + ) + PreferenceText( + title = stringResource(R.string.preference_custom_public_suffixes_title), + value = state.customSuffixes, + onValueChange = { onString(PreferenceKeys.OREO_AUTOFILL_CUSTOM_PUBLIC_SUFFIXES, it) }, + summary = stringResource(R.string.preference_custom_public_suffixes_summary), + hint = stringResource(R.string.preference_custom_public_suffixes_hint), + singleLine = false, + enabled = autofillEnabled, + ) + } + SettingsSection.Passwords -> { + PreferenceChoice( + title = stringResource(R.string.pref_password_generator_type_title), + values = persistentListOf(*stringArrayResource(R.array.pwgen_provider_values)), + labels = persistentListOf(*stringArrayResource(R.array.pwgen_provider_labels)), + selected = state.pwgenType, + onSelect = { onString(PreferenceKeys.PREF_KEY_PWGEN_TYPE, it) }, + ) + PreferenceText( + title = stringResource(R.string.pref_clipboard_timeout_title), + value = state.clipboardTimeout, + onValueChange = { onString(PreferenceKeys.GENERAL_SHOW_TIME, it) }, + summary = + stringResource( + R.string.pref_clipboard_timeout_summary, + state.clipboardTimeout.ifEmpty { "45" }, + ), + keyboardType = KeyboardType.Number, + ) + PreferenceSwitch( + title = stringResource(R.string.show_password_pref_title), + checked = state.showPassword, + onCheckedChange = { onBoolean(PreferenceKeys.SHOW_PASSWORD, it) }, + summary = stringResource(R.string.show_password_pref_summary), + ) + PreferenceSwitch( + title = stringResource(R.string.pref_copy_title), + checked = state.copyOnDecrypt, + onCheckedChange = { onBoolean(PreferenceKeys.COPY_ON_DECRYPT, it) }, + summary = stringResource(R.string.pref_copy_summary), + ) + } + SettingsSection.Repository -> { + PreferenceSwitch( + title = stringResource(R.string.pref_rebase_on_pull_title), + checked = state.rebaseOnPull, + onCheckedChange = { onBoolean(PreferenceKeys.REBASE_ON_PULL, it) }, + summary = + stringResource( + if (state.rebaseOnPull) R.string.pref_rebase_on_pull_summary_on + else R.string.pref_rebase_on_pull_summary + ), + ) + if (state.isGitRepo) { + PreferenceAction( + title = stringResource(R.string.pref_edit_git_server_settings), + onClick = { onAction(SettingsAction.GitServer) }, + ) + if (state.httpsRemote) + PreferenceAction( + title = stringResource(R.string.pref_edit_proxy_settings), + onClick = { onAction(SettingsAction.Proxy) }, + ) + PreferenceAction( + title = stringResource(R.string.pref_edit_git_config), + onClick = { onAction(SettingsAction.GitConfig) }, + ) + PreferenceAction( + title = stringResource(R.string.pref_import_ssh_key_title), + onClick = { onAction(SettingsAction.SshKey) }, + ) + } + PreferenceAction( + title = stringResource(R.string.pref_ssh_keygen_title), + onClick = { onAction(SettingsAction.SshKeyGen) }, + ) + if (state.isGitRepo && state.canShowSshKey) + PreferenceAction( + title = stringResource(R.string.pref_ssh_see_key_title), + onClick = { onAction(SettingsAction.SshKey) }, + ) + state.savedSecret?.let { title -> + PreferenceAction( + title = stringResource(title), + onClick = { onAction(SettingsAction.ClearSecret) }, + ) + } + PreferenceAction( + title = stringResource(R.string.pref_git_delete_repo_title), + onClick = { onAction(SettingsAction.DeleteRepository) }, + summary = stringResource(R.string.pref_git_delete_repo_summary), + enabled = !deleting, + ) + } + SettingsSection.Misc -> { + PreferenceAction( + title = stringResource(R.string.prefs_export_passwords_title), + onClick = { onAction(SettingsAction.Export) }, + summary = stringResource(R.string.prefs_export_passwords_summary), + ) + PreferenceSwitch( + title = stringResource(R.string.pref_clear_clipboard_title), + checked = state.clearClipboardHistory, + onCheckedChange = { onBoolean(PreferenceKeys.CLEAR_CLIPBOARD_HISTORY, it) }, + summary = stringResource(R.string.pref_clear_clipboard_summary), + ) + if (!BuildConfig.DEBUG) + PreferenceSwitch( + title = stringResource(R.string.pref_debug_logging_title), + checked = state.debugLogging, + onCheckedChange = { onBoolean(PreferenceKeys.ENABLE_DEBUG_LOGGING, it) }, + summary = stringResource(R.string.pref_debug_logging_summary), + ) + } + SettingsSection.Pgp -> { + PreferenceAction( + title = stringResource(R.string.pref_pgp_key_manager_title), + onClick = { onAction(SettingsAction.KeyManager) }, + ) + PreferenceSwitch( + title = stringResource(R.string.pref_pgp_ascii_armor_title), + checked = state.asciiArmor, + onCheckedChange = { onBoolean(PreferenceKeys.ASCII_ARMOR, it) }, + ) + PreferenceSwitch( + title = stringResource(R.string.pref_passphrase_cache_title), + checked = state.passphraseCache, + onCheckedChange = { onBoolean(Feature.EnablePGPPassphraseCache.configKey, it) }, + summary = stringResource(R.string.pref_passphrase_cache_summary), + // Without a screen lock it can still be turned off. + enabled = canAuthenticate || state.passphraseCache, + ) + PreferenceSwitch( + title = stringResource(R.string.pref_passphrase_cache_auto_clear_title), + checked = state.autoClearCache, + onCheckedChange = { onBoolean(PreferenceKeys.CLEAR_PASSPHRASE_CACHE, it) }, + summary = stringResource(R.string.pref_passphrase_cache_auto_clear_summary), + enabled = state.passphraseCache, + ) + } + } + } +} |
