aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java')
-rw-r--r--app/src/main/java/app/passwordstore/ui/settings/SettingsDestination.kt297
-rw-r--r--app/src/main/java/app/passwordstore/ui/settings/SettingsScreen.kt348
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,
+ )
+ }
+ }
+ }
+}