From 8bd156dea6e87eb667f76f4738fde992323ce0cf Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Tue, 12 Jan 2021 11:19:28 +0530 Subject: [PATCH] Rework settings to use ModernAndroidPreferences (#1236) Co-authored-by: Fabian Henneke --- CHANGELOG.md | 1 + app/build.gradle.kts | 5 +- app/src/main/AndroidManifest.xml | 16 +- .../fragments/RepoLocationFragment.kt | 12 +- .../onboarding/fragments/WelcomeFragment.kt | 4 +- .../aps/ui/passwords/PasswordStore.kt | 7 +- .../aps/ui/settings/AutofillSettings.kt | 126 ++++ .../ui/settings/DirectorySelectionActivity.kt | 56 ++ .../aps/ui/settings/GeneralSettings.kt | 104 +++ .../msfjarvis/aps/ui/settings/MiscSettings.kt | 76 ++ .../aps/ui/settings/PasswordSettings.kt | 119 ++++ .../aps/ui/settings/RepositorySettings.kt | 198 +++++ .../aps/ui/settings/SettingsActivity.kt | 93 +++ .../aps/ui/settings/SettingsProvider.kt | 19 + .../aps/ui/settings/UserPreference.kt | 674 ------------------ .../aps/ui/sshkeygen/SshKeyGenActivity.kt | 1 + .../aps/ui/sshkeygen/SshKeyImportActivity.kt | 61 ++ .../aps/util/autofill/AutofillPreferences.kt | 7 +- .../aps/util/git/operation/GitOperation.kt | 11 +- .../res/drawable/app_settings_alt_24px.xml | 14 + .../main/res/drawable/ic_call_merge_24px.xml | 14 + .../ic_miscellaneous_services_24px.xml | 17 + app/src/main/res/drawable/ic_wysiwyg_24px.xml | 14 + .../activity_preference_recyclerview.xml | 18 + app/src/main/res/values-de/strings.xml | 64 +- app/src/main/res/values-fr/strings.xml | 60 +- app/src/main/res/values-gl/strings.xml | 66 +- app/src/main/res/values-it/strings.xml | 66 +- app/src/main/res/values-pt-rBR/strings.xml | 64 +- app/src/main/res/values-ru/strings.xml | 64 +- app/src/main/res/values-v29/arrays.xml | 6 +- app/src/main/res/values/arrays.xml | 6 +- app/src/main/res/values/strings.xml | 67 +- app/src/main/res/xml/preference.xml | 174 ----- buildSrc/src/main/java/Dependencies.kt | 1 + 35 files changed, 1197 insertions(+), 1108 deletions(-) create mode 100644 app/src/main/java/dev/msfjarvis/aps/ui/settings/AutofillSettings.kt create mode 100644 app/src/main/java/dev/msfjarvis/aps/ui/settings/DirectorySelectionActivity.kt create mode 100644 app/src/main/java/dev/msfjarvis/aps/ui/settings/GeneralSettings.kt create mode 100644 app/src/main/java/dev/msfjarvis/aps/ui/settings/MiscSettings.kt create mode 100644 app/src/main/java/dev/msfjarvis/aps/ui/settings/PasswordSettings.kt create mode 100644 app/src/main/java/dev/msfjarvis/aps/ui/settings/RepositorySettings.kt create mode 100644 app/src/main/java/dev/msfjarvis/aps/ui/settings/SettingsActivity.kt create mode 100644 app/src/main/java/dev/msfjarvis/aps/ui/settings/SettingsProvider.kt delete mode 100644 app/src/main/java/dev/msfjarvis/aps/ui/settings/UserPreference.kt create mode 100644 app/src/main/java/dev/msfjarvis/aps/ui/sshkeygen/SshKeyImportActivity.kt create mode 100644 app/src/main/res/drawable/app_settings_alt_24px.xml create mode 100644 app/src/main/res/drawable/ic_call_merge_24px.xml create mode 100644 app/src/main/res/drawable/ic_miscellaneous_services_24px.xml create mode 100644 app/src/main/res/drawable/ic_wysiwyg_24px.xml create mode 100644 app/src/main/res/layout/activity_preference_recyclerview.xml delete mode 100644 app/src/main/res/xml/preference.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 88686572..58e172d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ All notable changes to this project will be documented in this file. ### Changed - Accessibility autofill has been removed completely due to being buggy, insecure and lacking in features. Upgrade to Android 8 or preferably later to gain access to our advanced Autofill implementation. +- The settings UI has been completely re-done to dramatically improve discoverability and navigation for users ## [1.13.1] - 2020-10-23 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 90322274..a1b2583b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -73,6 +73,7 @@ dependencies { implementation(Dependencies.FirstParty.zxing_android_embedded) + implementation(Dependencies.ThirdParty.bouncycastle) implementation(Dependencies.ThirdParty.commons_codec) implementation(Dependencies.ThirdParty.eddsa) implementation(Dependencies.ThirdParty.fastscroll) @@ -80,10 +81,10 @@ dependencies { exclude(group = "org.apache.httpcomponents", module = "httpclient") } implementation(Dependencies.ThirdParty.kotlin_result) - implementation(Dependencies.ThirdParty.sshj) - implementation(Dependencies.ThirdParty.bouncycastle) + implementation(Dependencies.ThirdParty.modern_android_prefs) implementation(Dependencies.ThirdParty.plumber) implementation(Dependencies.ThirdParty.ssh_auth) + implementation(Dependencies.ThirdParty.sshj) implementation(Dependencies.ThirdParty.timber) implementation(Dependencies.ThirdParty.timberkt) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3e1d743e..69632359 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -33,7 +33,8 @@ android:name=".ui.onboarding.activity.OnboardingActivity" android:configChanges="orientation|screenSize" /> - + android:name=".ui.settings.SettingsActivity" + android:label="@string/action_settings" + android:parentActivityName=".ui.passwords.PasswordStore" /> + + + { runCatching { - startActivity(Intent(this, UserPreference::class.java)) + startActivity(Intent(this, SettingsActivity::class.java)) }.onFailure { e -> e.printStackTrace() } @@ -377,7 +378,7 @@ class PasswordStore : BaseGitActivity() { private fun checkLocalRepository() { val repo = PasswordRepository.initialize() if (repo == null) { - directorySelectAction.launch(UserPreference.createDirectorySelectionIntent(this)) + directorySelectAction.launch(Intent(this, DirectorySelectionActivity::class.java)) } else { checkLocalRepository(PasswordRepository.getRepositoryDirectory()) } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/settings/AutofillSettings.kt b/app/src/main/java/dev/msfjarvis/aps/ui/settings/AutofillSettings.kt new file mode 100644 index 00000000..74b407fd --- /dev/null +++ b/app/src/main/java/dev/msfjarvis/aps/ui/settings/AutofillSettings.kt @@ -0,0 +1,126 @@ +/* + * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package dev.msfjarvis.aps.ui.settings + +import de.Maxr1998.modernpreferences.PreferenceScreen +import de.Maxr1998.modernpreferences.helpers.editText +import de.Maxr1998.modernpreferences.helpers.onClick +import de.Maxr1998.modernpreferences.helpers.singleChoice +import de.Maxr1998.modernpreferences.helpers.switch +import de.Maxr1998.modernpreferences.preferences.SwitchPreference +import de.Maxr1998.modernpreferences.preferences.choice.SelectionItem +import dev.msfjarvis.aps.BuildConfig +import dev.msfjarvis.aps.R +import dev.msfjarvis.aps.util.autofill.DirectoryStructure +import dev.msfjarvis.aps.util.extensions.autofillManager +import dev.msfjarvis.aps.util.settings.PreferenceKeys +import android.annotation.SuppressLint +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.provider.Settings +import androidx.annotation.RequiresApi +import androidx.appcompat.widget.AppCompatTextView +import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import com.github.androidpasswordstore.autofillparser.BrowserAutofillSupportLevel +import com.github.androidpasswordstore.autofillparser.getInstalledBrowsersWithAutofillSupportLevel +import com.google.android.material.dialog.MaterialAlertDialogBuilder + +class AutofillSettings(private val activity: FragmentActivity) : SettingsProvider { + + private val isAutofillServiceEnabled: Boolean + get() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return false + return activity.autofillManager?.hasEnabledAutofillServices() == true + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun showAutofillDialog(pref: SwitchPreference) { + val observer = LifecycleEventObserver { _, event -> + when (event) { + Lifecycle.Event.ON_RESUME -> { + pref.checked = isAutofillServiceEnabled + } + else -> { + } + } + } + MaterialAlertDialogBuilder(activity).run { + setTitle(R.string.pref_autofill_enable_title) + @SuppressLint("InflateParams") + val layout = + activity.layoutInflater.inflate(R.layout.oreo_autofill_instructions, null) + val supportedBrowsersTextView = + layout.findViewById(R.id.supportedBrowsers) + supportedBrowsersTextView.text = + getInstalledBrowsersWithAutofillSupportLevel(context).joinToString( + separator = "\n" + ) { + val appLabel = it.first + val supportDescription = when (it.second) { + BrowserAutofillSupportLevel.None -> activity.getString(R.string.oreo_autofill_no_support) + BrowserAutofillSupportLevel.FlakyFill -> activity.getString(R.string.oreo_autofill_flaky_fill_support) + BrowserAutofillSupportLevel.PasswordFill -> activity.getString(R.string.oreo_autofill_password_fill_support) + BrowserAutofillSupportLevel.PasswordFillAndSaveIfNoAccessibility -> activity.getString(R.string.oreo_autofill_password_fill_and_conditional_save_support) + BrowserAutofillSupportLevel.GeneralFill -> activity.getString(R.string.oreo_autofill_general_fill_support) + BrowserAutofillSupportLevel.GeneralFillAndSave -> activity.getString(R.string.oreo_autofill_general_fill_and_save_support) + } + "$appLabel: $supportDescription" + } + setView(layout) + setPositiveButton(R.string.dialog_ok) { _, _ -> + val intent = Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE).apply { + data = Uri.parse("package:${BuildConfig.APPLICATION_ID}") + } + activity.startActivity(intent) + } + setNegativeButton(R.string.dialog_cancel, null) + setOnDismissListener { pref.checked = isAutofillServiceEnabled } + activity.lifecycle.addObserver(observer) + show() + } + } + + override fun provideSettings(builder: PreferenceScreen.Builder) { + builder.apply { + switch(PreferenceKeys.AUTOFILL_ENABLE) { + titleRes = R.string.pref_autofill_enable_title + visible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O + defaultValue = isAutofillServiceEnabled + onClick { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return@onClick true + if (isAutofillServiceEnabled) { + activity.autofillManager?.disableAutofillServices() + } else { + showAutofillDialog(this) + } + false + } + } + val values = activity.resources.getStringArray(R.array.oreo_autofill_directory_structure_values) + val titles = activity.resources.getStringArray(R.array.oreo_autofill_directory_structure_entries) + val items = values.zip(titles).map { SelectionItem(it.first, it.second, null) } + singleChoice(PreferenceKeys.OREO_AUTOFILL_DIRECTORY_STRUCTURE, items) { + initialSelection = DirectoryStructure.DEFAULT.value + dependency = PreferenceKeys.AUTOFILL_ENABLE + titleRes = R.string.oreo_autofill_preference_directory_structure + } + editText(PreferenceKeys.OREO_AUTOFILL_DEFAULT_USERNAME) { + dependency = PreferenceKeys.AUTOFILL_ENABLE + titleRes = R.string.preference_default_username_title + summaryProvider = { activity.getString(R.string.preference_default_username_summary) } + } + editText(PreferenceKeys.OREO_AUTOFILL_CUSTOM_PUBLIC_SUFFIXES) { + dependency = PreferenceKeys.AUTOFILL_ENABLE + titleRes = R.string.preference_custom_public_suffixes_title + summaryProvider = { activity.getString(R.string.preference_custom_public_suffixes_summary) } + textInputHintRes = R.string.preference_custom_public_suffixes_hint + } + } + } +} diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/settings/DirectorySelectionActivity.kt b/app/src/main/java/dev/msfjarvis/aps/ui/settings/DirectorySelectionActivity.kt new file mode 100644 index 00000000..41cd254b --- /dev/null +++ b/app/src/main/java/dev/msfjarvis/aps/ui/settings/DirectorySelectionActivity.kt @@ -0,0 +1,56 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package dev.msfjarvis.aps.ui.settings + +import dev.msfjarvis.aps.R +import dev.msfjarvis.aps.util.extensions.sharedPrefs +import dev.msfjarvis.aps.util.settings.PreferenceKeys +import android.net.Uri +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle +import android.os.Environment +import android.provider.DocumentsContract +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.content.edit +import com.github.ajalt.timberkt.d +import com.google.android.material.dialog.MaterialAlertDialogBuilder + +class DirectorySelectionActivity : AppCompatActivity() { + + @Suppress("DEPRECATION") + private val directorySelectAction = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri: Uri? -> + if (uri == null) return@registerForActivityResult + + d { "Selected repository URI is $uri" } + // TODO: This is fragile. Workaround until PasswordItem is backed by DocumentFile + val docId = DocumentsContract.getTreeDocumentId(uri) + val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + val path = if (split.size > 1) split[1] else split[0] + val repoPath = "${Environment.getExternalStorageDirectory()}/$path" + val prefs = sharedPrefs + + d { "Selected repository path is $repoPath" } + + if (Environment.getExternalStorageDirectory().path == repoPath) { + MaterialAlertDialogBuilder(this) + .setTitle(resources.getString(R.string.sdcard_root_warning_title)) + .setMessage(resources.getString(R.string.sdcard_root_warning_message)) + .setPositiveButton(resources.getString(R.string.sdcard_root_warning_remove_everything)) { _, _ -> + prefs.edit { putString(PreferenceKeys.GIT_EXTERNAL_REPO, uri.path) } + } + .setNegativeButton(R.string.dialog_cancel, null) + .show() + } + prefs.edit { putString(PreferenceKeys.GIT_EXTERNAL_REPO, repoPath) } + setResult(RESULT_OK) + finish() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + directorySelectAction.launch(null) + } +} diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/settings/GeneralSettings.kt b/app/src/main/java/dev/msfjarvis/aps/ui/settings/GeneralSettings.kt new file mode 100644 index 00000000..91df87d0 --- /dev/null +++ b/app/src/main/java/dev/msfjarvis/aps/ui/settings/GeneralSettings.kt @@ -0,0 +1,104 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package dev.msfjarvis.aps.ui.settings + +import de.Maxr1998.modernpreferences.PreferenceScreen +import de.Maxr1998.modernpreferences.helpers.checkBox +import de.Maxr1998.modernpreferences.helpers.onClick +import de.Maxr1998.modernpreferences.helpers.singleChoice +import de.Maxr1998.modernpreferences.preferences.choice.SelectionItem +import dev.msfjarvis.aps.R +import dev.msfjarvis.aps.util.auth.BiometricAuthenticator +import dev.msfjarvis.aps.util.extensions.sharedPrefs +import dev.msfjarvis.aps.util.settings.PreferenceKeys +import android.content.pm.ShortcutManager +import android.os.Build +import androidx.core.content.edit +import androidx.core.content.getSystemService +import androidx.fragment.app.FragmentActivity + +class GeneralSettings(private val activity: FragmentActivity) : SettingsProvider { + + override fun provideSettings(builder: PreferenceScreen.Builder) { + builder.apply { + val themeValues = activity.resources.getStringArray(R.array.app_theme_values) + val themeOptions = activity.resources.getStringArray(R.array.app_theme_options) + val themeItems = themeValues.zip(themeOptions).map { SelectionItem(it.first, it.second, null) } + singleChoice(PreferenceKeys.APP_THEME, themeItems) { + initialSelection = activity.resources.getString(R.string.app_theme_def) + titleRes = R.string.pref_app_theme_title + } + + val sortValues = activity.resources.getStringArray(R.array.sort_order_values) + val sortOptions = activity.resources.getStringArray(R.array.sort_order_entries) + val sortItems = sortValues.zip(sortOptions).map { SelectionItem(it.first, it.second, null) } + singleChoice(PreferenceKeys.SORT_ORDER, sortItems) { + initialSelection = sortValues[0] + titleRes = R.string.pref_sort_order_title + } + + checkBox(PreferenceKeys.FILTER_RECURSIVELY) { + titleRes = R.string.pref_recursive_filter_title + summaryRes = R.string.pref_recursive_filter_summary + defaultValue = true + } + + checkBox(PreferenceKeys.SEARCH_ON_START) { + titleRes = R.string.pref_search_on_start_title + summaryRes = R.string.pref_search_on_start_summary + defaultValue = false + } + + checkBox(PreferenceKeys.SHOW_HIDDEN_CONTENTS) { + titleRes = R.string.pref_show_hidden_title + summaryRes = R.string.pref_show_hidden_summary + defaultValue = false + } + + checkBox(PreferenceKeys.BIOMETRIC_AUTH) { + titleRes = R.string.pref_biometric_auth_title + defaultValue = false + }.apply { + val canAuthenticate = BiometricAuthenticator.canAuthenticate(activity) + if (!canAuthenticate) { + enabled = false + checked = false + summaryRes = R.string.pref_biometric_auth_summary_error + } else { + summaryRes = R.string.pref_biometric_auth_summary + onClick { + enabled = false + val isChecked = checked + activity.sharedPrefs.edit { + BiometricAuthenticator.authenticate(activity) { result -> + when (result) { + is BiometricAuthenticator.Result.Success -> { + // Apply the changes + putBoolean(PreferenceKeys.BIOMETRIC_AUTH, checked) + enabled = true + } + else -> { + // If any error occurs, revert back to the previous state. This + // catch-all clause includes the cancellation case. + putBoolean(PreferenceKeys.BIOMETRIC_AUTH, !checked) + checked = !isChecked + enabled = true + } + } + } + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + activity.getSystemService()?.apply { + removeDynamicShortcuts(dynamicShortcuts.map { it.id }.toMutableList()) + } + } + false + } + } + } + } + } +} diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/settings/MiscSettings.kt b/app/src/main/java/dev/msfjarvis/aps/ui/settings/MiscSettings.kt new file mode 100644 index 00000000..08da760c --- /dev/null +++ b/app/src/main/java/dev/msfjarvis/aps/ui/settings/MiscSettings.kt @@ -0,0 +1,76 @@ +/* + * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package dev.msfjarvis.aps.ui.settings + +import de.Maxr1998.modernpreferences.PreferenceScreen +import de.Maxr1998.modernpreferences.helpers.checkBox +import de.Maxr1998.modernpreferences.helpers.onClick +import de.Maxr1998.modernpreferences.helpers.pref +import dev.msfjarvis.aps.BuildConfig +import dev.msfjarvis.aps.R +import dev.msfjarvis.aps.util.services.PasswordExportService +import dev.msfjarvis.aps.util.settings.PreferenceKeys +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import androidx.activity.result.contract.ActivityResultContracts +import androidx.documentfile.provider.DocumentFile +import androidx.fragment.app.FragmentActivity + +class MiscSettings(activity: FragmentActivity) : SettingsProvider { + + private val storeExportAction = activity.registerForActivityResult(object : ActivityResultContracts.OpenDocumentTree() { + override fun createIntent(context: Context, input: Uri?): Intent { + return 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 + } + } + }) { uri: Uri? -> + if (uri == null) return@registerForActivityResult + val targetDirectory = DocumentFile.fromTreeUri(activity.applicationContext, uri) + + if (targetDirectory != null) { + val service = Intent(activity.applicationContext, PasswordExportService::class.java).apply { + action = PasswordExportService.ACTION_EXPORT_PASSWORD + putExtra("uri", uri) + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + activity.startForegroundService(service) + } else { + activity.startService(service) + } + } + } + + override fun provideSettings(builder: PreferenceScreen.Builder) { + builder.apply { + pref(PreferenceKeys.EXPORT_PASSWORDS) { + titleRes = R.string.prefs_export_passwords_title + summaryRes = R.string.prefs_export_passwords_summary + onClick { + storeExportAction.launch(null) + true + } + } + checkBox(PreferenceKeys.CLEAR_CLIPBOARD_20X) { + defaultValue = false + titleRes = R.string.pref_clear_clipboard_title + summaryRes = R.string.pref_clear_clipboard_summary + } + checkBox(PreferenceKeys.ENABLE_DEBUG_LOGGING) { + defaultValue = false + titleRes = R.string.pref_debug_logging_title + summaryRes = R.string.pref_debug_logging_summary + visible = !BuildConfig.DEBUG + } + } + } +} diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/settings/PasswordSettings.kt b/app/src/main/java/dev/msfjarvis/aps/ui/settings/PasswordSettings.kt new file mode 100644 index 00000000..9b7eb01c --- /dev/null +++ b/app/src/main/java/dev/msfjarvis/aps/ui/settings/PasswordSettings.kt @@ -0,0 +1,119 @@ +/* + * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package dev.msfjarvis.aps.ui.settings + +import de.Maxr1998.modernpreferences.Preference +import de.Maxr1998.modernpreferences.PreferenceScreen +import de.Maxr1998.modernpreferences.helpers.categoryHeader +import de.Maxr1998.modernpreferences.helpers.checkBox +import de.Maxr1998.modernpreferences.helpers.editText +import de.Maxr1998.modernpreferences.helpers.onCheckedChange +import de.Maxr1998.modernpreferences.helpers.onClick +import de.Maxr1998.modernpreferences.helpers.onSelectionChange +import de.Maxr1998.modernpreferences.helpers.singleChoice +import de.Maxr1998.modernpreferences.preferences.CheckBoxPreference +import de.Maxr1998.modernpreferences.preferences.choice.SelectionItem +import dev.msfjarvis.aps.R +import dev.msfjarvis.aps.util.extensions.getString +import dev.msfjarvis.aps.util.extensions.sharedPrefs +import dev.msfjarvis.aps.util.pwgenxkpwd.XkpwdDictionary +import dev.msfjarvis.aps.util.settings.PreferenceKeys +import android.text.InputType +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.content.edit +import androidx.fragment.app.FragmentActivity +import java.io.File + +class PasswordSettings(private val activity: FragmentActivity) : SettingsProvider { + + private val sharedPrefs by lazy(LazyThreadSafetyMode.NONE) { activity.sharedPrefs } + private val storeCustomXkpwdDictionaryAction = activity.registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> + if (uri == null) return@registerForActivityResult + + Toast.makeText( + activity, + activity.resources.getString(R.string.xkpwgen_custom_dict_imported, uri.path), + Toast.LENGTH_SHORT + ).show() + + sharedPrefs.edit { putString(PreferenceKeys.PREF_KEY_CUSTOM_DICT, uri.toString()) } + + val inputStream = activity.contentResolver.openInputStream(uri) + val customDictFile = File(activity.filesDir.toString(), XkpwdDictionary.XKPWD_CUSTOM_DICT_FILE).outputStream() + inputStream?.copyTo(customDictFile, 1024) + inputStream?.close() + customDictFile.close() + } + + override fun provideSettings(builder: PreferenceScreen.Builder) { + builder.apply { + val customDictPref = CheckBoxPreference(PreferenceKeys.PREF_KEY_IS_CUSTOM_DICT).apply { + titleRes = R.string.pref_xkpwgen_custom_wordlist_enabled_title + summaryRes = R.string.pref_xkpwgen_custom_dict_summary_off + summaryOnRes = R.string.pref_xkpwgen_custom_dict_summary_on + visible = sharedPrefs.getString(PreferenceKeys.PREF_KEY_PWGEN_TYPE) == "xkpasswd" + onCheckedChange { + requestRebind() + true + } + } + val customDictPathPref = Preference(PreferenceKeys.PREF_KEY_CUSTOM_DICT).apply { + dependency = PreferenceKeys.PREF_KEY_IS_CUSTOM_DICT + titleRes = R.string.pref_xkpwgen_custom_dict_picker_title + summary = sharedPrefs.getString(PreferenceKeys.PREF_KEY_CUSTOM_DICT) + ?: activity.resources.getString(R.string.pref_xkpwgen_custom_dict_picker_summary) + visible = sharedPrefs.getString(PreferenceKeys.PREF_KEY_PWGEN_TYPE) == "xkpasswd" + onClick { + storeCustomXkpwdDictionaryAction.launch(arrayOf("*/*")) + true + } + } + val values = activity.resources.getStringArray(R.array.pwgen_provider_values) + val labels = activity.resources.getStringArray(R.array.pwgen_provider_labels) + val items = values.zip(labels).map { SelectionItem(it.first, it.second, null) } + singleChoice( + PreferenceKeys.PREF_KEY_PWGEN_TYPE, + items, + ) { + initialSelection = "classic" + titleRes = R.string.pref_password_generator_type_title + onSelectionChange { selection -> + val xkpasswdEnabled = selection == "xkpasswd" + customDictPathPref.visible = xkpasswdEnabled + customDictPref.visible = xkpasswdEnabled + customDictPref.requestRebind() + customDictPathPref.requestRebind() + true + } + } + // We initialize them early and add them manually to be able to manually force a rebind + // when the password generator type is changed. + addPreferenceItem(customDictPref) + addPreferenceItem(customDictPathPref) + editText(PreferenceKeys.GENERAL_SHOW_TIME) { + titleRes = R.string.pref_clipboard_timeout_title + summaryProvider = { activity.getString(R.string.pref_clipboard_timeout_summary) } + textInputType = InputType.TYPE_CLASS_NUMBER + } + checkBox(PreferenceKeys.SHOW_PASSWORD) { + titleRes = R.string.show_password_pref_title + summaryRes = R.string.show_password_pref_summary + defaultValue = true + } + checkBox(PreferenceKeys.SHOW_EXTRA_CONTENT) { + titleRes = R.string.show_extra_content_pref_title + summaryRes = R.string.show_extra_content_pref_summary + defaultValue = true + } + checkBox(PreferenceKeys.COPY_ON_DECRYPT) { + titleRes = R.string.pref_copy_title + summaryRes = R.string.pref_copy_summary + defaultValue = false + } + } + } +} diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/settings/RepositorySettings.kt b/app/src/main/java/dev/msfjarvis/aps/ui/settings/RepositorySettings.kt new file mode 100644 index 00000000..32d04c9b --- /dev/null +++ b/app/src/main/java/dev/msfjarvis/aps/ui/settings/RepositorySettings.kt @@ -0,0 +1,198 @@ +/* + * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package dev.msfjarvis.aps.ui.settings + +import de.Maxr1998.modernpreferences.Preference +import de.Maxr1998.modernpreferences.PreferenceScreen +import de.Maxr1998.modernpreferences.helpers.checkBox +import de.Maxr1998.modernpreferences.helpers.onCheckedChange +import de.Maxr1998.modernpreferences.helpers.onClick +import de.Maxr1998.modernpreferences.helpers.pref +import dev.msfjarvis.aps.R +import dev.msfjarvis.aps.data.repo.PasswordRepository +import dev.msfjarvis.aps.ui.git.config.GitConfigActivity +import dev.msfjarvis.aps.ui.git.config.GitServerConfigActivity +import dev.msfjarvis.aps.ui.proxy.ProxySelectorActivity +import dev.msfjarvis.aps.ui.sshkeygen.ShowSshKeyFragment +import dev.msfjarvis.aps.ui.sshkeygen.SshKeyGenActivity +import dev.msfjarvis.aps.ui.sshkeygen.SshKeyImportActivity +import dev.msfjarvis.aps.util.extensions.getEncryptedGitPrefs +import dev.msfjarvis.aps.util.extensions.getString +import dev.msfjarvis.aps.util.extensions.sharedPrefs +import dev.msfjarvis.aps.util.extensions.snackbar +import dev.msfjarvis.aps.util.settings.GitSettings +import dev.msfjarvis.aps.util.settings.PreferenceKeys +import android.content.Intent +import android.content.pm.ShortcutManager +import android.os.Build +import androidx.core.content.edit +import androidx.core.content.getSystemService +import androidx.fragment.app.FragmentActivity +import com.github.michaelbull.result.onFailure +import com.github.michaelbull.result.runCatching +import com.google.android.material.dialog.MaterialAlertDialogBuilder + +class RepositorySettings(private val activity: FragmentActivity) : SettingsProvider { + + private val encryptedPreferences by lazy(LazyThreadSafetyMode.NONE) { activity.getEncryptedGitPrefs() } + + private fun launchActivity(clazz: Class) { + activity.startActivity(Intent(activity, clazz)) + } + + private fun selectExternalGitRepository() { + MaterialAlertDialogBuilder(activity) + .setTitle(activity.resources.getString(R.string.external_repository_dialog_title)) + .setMessage(activity.resources.getString(R.string.external_repository_dialog_text)) + .setPositiveButton(R.string.dialog_ok) { _, _ -> + launchActivity(DirectorySelectionActivity::class.java) + } + .setNegativeButton(R.string.dialog_cancel, null) + .show() + } + + override fun provideSettings(builder: PreferenceScreen.Builder) { + builder.apply { + pref(PreferenceKeys.GIT_SERVER_INFO) { + titleRes = R.string.pref_edit_git_server_settings + visible = PasswordRepository.isGitRepo() + onClick { + launchActivity(GitServerConfigActivity::class.java) + true + } + } + pref(PreferenceKeys.PROXY_SETTINGS) { + titleRes = R.string.pref_edit_proxy_settings + visible = GitSettings.url?.startsWith("https") == true && PasswordRepository.isGitRepo() + onClick { + launchActivity(ProxySelectorActivity::class.java) + true + } + } + pref(PreferenceKeys.GIT_CONFIG) { + titleRes = R.string.pref_edit_git_config + visible = PasswordRepository.isGitRepo() + onClick { + launchActivity(GitConfigActivity::class.java) + true + } + } + pref(PreferenceKeys.SSH_KEY) { + titleRes = R.string.pref_import_ssh_key_title + visible = PasswordRepository.isGitRepo() + onClick { + launchActivity(SshKeyImportActivity::class.java) + true + } + } + pref(PreferenceKeys.SSH_KEYGEN) { + titleRes = R.string.pref_ssh_keygen_title + onClick { + launchActivity(SshKeyGenActivity::class.java) + true + } + } + pref(PreferenceKeys.SSH_SEE_KEY) { + titleRes = R.string.pref_ssh_see_key_title + visible = PasswordRepository.isGitRepo() + onClick { + ShowSshKeyFragment().show(activity.supportFragmentManager, "public_key") + true + } + } + pref(PreferenceKeys.CLEAR_SAVED_PASS) { + fun Preference.updatePref() { + val sshPass = encryptedPreferences.getString(PreferenceKeys.SSH_KEY_LOCAL_PASSPHRASE) + val httpsPass = encryptedPreferences.getString(PreferenceKeys.HTTPS_PASSWORD) + if (sshPass == null && httpsPass == null) { + visible = false + return + } + when { + httpsPass != null -> titleRes = R.string.clear_saved_passphrase_https + sshPass != null -> titleRes = R.string.clear_saved_passphrase_ssh + } + visible = true + requestRebind() + } + onClick { + updatePref() + true + } + updatePref() + } + pref(PreferenceKeys.SSH_OPENKEYSTORE_CLEAR_KEY_ID) { + titleRes = R.string.pref_title_openkeystore_clear_keyid + visible = activity.sharedPrefs.getString(PreferenceKeys.SSH_OPENKEYSTORE_KEYID)?.isNotEmpty() + ?: false + onClick { + activity.sharedPrefs.edit { putString(PreferenceKeys.SSH_OPENKEYSTORE_KEYID, null) } + visible = false + true + } + } + val deleteRepoPref = pref(PreferenceKeys.GIT_DELETE_REPO) { + titleRes = R.string.pref_git_delete_repo_title + summaryRes = R.string.pref_git_delete_repo_summary + visible = !activity.sharedPrefs.getBoolean(PreferenceKeys.GIT_EXTERNAL, false) + onClick { + val repoDir = PasswordRepository.getRepositoryDirectory() + MaterialAlertDialogBuilder(activity) + .setTitle(R.string.pref_dialog_delete_title) + .setMessage(activity.getString(R.string.dialog_delete_msg, repoDir)) + .setCancelable(false) + .setPositiveButton(R.string.dialog_delete) { dialogInterface, _ -> + runCatching { + PasswordRepository.getRepositoryDirectory().deleteRecursively() + PasswordRepository.closeRepository() + }.onFailure { + it.message?.let { message -> + activity.snackbar(message = message) + } + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + activity.getSystemService()?.apply { + removeDynamicShortcuts(dynamicShortcuts.map { it.id }.toMutableList()) + } + } + activity.sharedPrefs.edit { putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, false) } + dialogInterface.cancel() + activity.finish() + } + .setNegativeButton(R.string.dialog_do_not_delete) { dialogInterface, _ -> run { dialogInterface.cancel() } } + .show() + true + } + } + checkBox(PreferenceKeys.GIT_EXTERNAL) { + titleRes = R.string.pref_external_repository_title + summaryRes = R.string.pref_external_repository_summary + onCheckedChange { checked -> + deleteRepoPref.visible = !checked + deleteRepoPref.requestRebind() + PasswordRepository.closeRepository() + activity.sharedPrefs.edit { putBoolean(PreferenceKeys.REPO_CHANGED, true) } + true + } + } + pref(PreferenceKeys.GIT_EXTERNAL_REPO) { + val externalRepo = activity.sharedPrefs.getString(PreferenceKeys.GIT_EXTERNAL_REPO) + if (externalRepo != null) { + summary = externalRepo + } else { + summaryRes = R.string.pref_select_external_repository_summary_no_repo_selected + } + titleRes = R.string.pref_select_external_repository_title + dependency = PreferenceKeys.GIT_EXTERNAL + onClick { + selectExternalGitRepository() + true + } + } + } + } +} diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/settings/SettingsActivity.kt b/app/src/main/java/dev/msfjarvis/aps/ui/settings/SettingsActivity.kt new file mode 100644 index 00000000..7db80023 --- /dev/null +++ b/app/src/main/java/dev/msfjarvis/aps/ui/settings/SettingsActivity.kt @@ -0,0 +1,93 @@ +/* + * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package dev.msfjarvis.aps.ui.settings + +import de.Maxr1998.modernpreferences.PreferencesAdapter +import de.Maxr1998.modernpreferences.helpers.screen +import de.Maxr1998.modernpreferences.helpers.subScreen +import dev.msfjarvis.aps.R +import dev.msfjarvis.aps.databinding.ActivityPreferenceRecyclerviewBinding +import dev.msfjarvis.aps.util.extensions.viewBinding +import android.os.Bundle +import android.view.MenuItem +import androidx.appcompat.app.AppCompatActivity + +class SettingsActivity : AppCompatActivity() { + + private val miscSettings = MiscSettings(this) + private val autofillSettings = AutofillSettings(this) + private val passwordSettings = PasswordSettings(this) + private val repositorySettings = RepositorySettings(this) + private val generalSettings = GeneralSettings(this) + + private val binding by viewBinding(ActivityPreferenceRecyclerviewBinding::inflate) + private val preferencesAdapter: PreferencesAdapter + get() = binding.preferenceRecyclerView.adapter as PreferencesAdapter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + val screen = screen(this) { + subScreen { + titleRes = R.string.pref_category_general_title + iconRes = R.drawable.app_settings_alt_24px + generalSettings.provideSettings(this) + } + subScreen { + titleRes = R.string.pref_category_autofill_title + iconRes = R.drawable.ic_wysiwyg_24px + autofillSettings.provideSettings(this) + } + subScreen { + titleRes = R.string.pref_category_passwords_title + iconRes = R.drawable.ic_lock_open_24px + passwordSettings.provideSettings(this) + } + subScreen { + titleRes = R.string.pref_category_repository_title + iconRes = R.drawable.ic_call_merge_24px + repositorySettings.provideSettings(this) + } + subScreen { + titleRes = R.string.pref_category_misc_title + iconRes = R.drawable.ic_miscellaneous_services_24px + miscSettings.provideSettings(this) + } + } + val adapter = PreferencesAdapter(screen) + adapter.onScreenChangeListener = PreferencesAdapter.OnScreenChangeListener { subScreen, entering -> + supportActionBar?.title = if (!entering) { + getString(R.string.action_settings) + } else { + getString(subScreen.titleRes) + } + } + savedInstanceState?.getParcelable("adapter") + ?.let(adapter::loadSavedState) + binding.preferenceRecyclerView.adapter = adapter + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putParcelable("adapter", preferencesAdapter.getSavedState()) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + android.R.id.home -> if (!preferencesAdapter.goBack()) { + super.onOptionsItemSelected(item) + } else { + true + } + else -> super.onOptionsItemSelected(item) + } + } + + override fun onBackPressed() { + if (!preferencesAdapter.goBack()) + super.onBackPressed() + } +} diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/settings/SettingsProvider.kt b/app/src/main/java/dev/msfjarvis/aps/ui/settings/SettingsProvider.kt new file mode 100644 index 00000000..b8398b94 --- /dev/null +++ b/app/src/main/java/dev/msfjarvis/aps/ui/settings/SettingsProvider.kt @@ -0,0 +1,19 @@ +/* + * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package dev.msfjarvis.aps.ui.settings + +import de.Maxr1998.modernpreferences.PreferenceScreen + +/** + * Used to generate a uniform API for all settings UI classes. + */ +interface SettingsProvider { + + /** + * Inserts the settings items for the class into the given [builder]. + */ + fun provideSettings(builder: PreferenceScreen.Builder) +} diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/settings/UserPreference.kt b/app/src/main/java/dev/msfjarvis/aps/ui/settings/UserPreference.kt deleted file mode 100644 index 315ebbda..00000000 --- a/app/src/main/java/dev/msfjarvis/aps/ui/settings/UserPreference.kt +++ /dev/null @@ -1,674 +0,0 @@ -/* - * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -package dev.msfjarvis.aps.ui.settings - -import android.annotation.SuppressLint -import android.content.Context -import android.content.Intent -import android.content.SharedPreferences -import android.content.pm.ShortcutManager -import android.net.Uri -import android.os.Build -import android.os.Bundle -import android.os.Environment -import android.provider.DocumentsContract -import android.provider.Settings -import android.text.TextUtils -import android.view.MenuItem -import android.widget.Toast -import androidx.activity.result.contract.ActivityResultContracts.OpenDocument -import androidx.activity.result.contract.ActivityResultContracts.OpenDocumentTree -import androidx.annotation.RequiresApi -import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.widget.AppCompatTextView -import androidx.core.content.edit -import androidx.core.content.getSystemService -import androidx.documentfile.provider.DocumentFile -import androidx.preference.CheckBoxPreference -import androidx.preference.EditTextPreference -import androidx.preference.ListPreference -import androidx.preference.Preference -import androidx.preference.PreferenceFragmentCompat -import androidx.preference.SwitchPreferenceCompat -import com.github.ajalt.timberkt.Timber.tag -import com.github.ajalt.timberkt.d -import com.github.ajalt.timberkt.w -import com.github.androidpasswordstore.autofillparser.BrowserAutofillSupportLevel -import com.github.androidpasswordstore.autofillparser.getInstalledBrowsersWithAutofillSupportLevel -import com.github.michaelbull.result.getOr -import com.github.michaelbull.result.onFailure -import com.github.michaelbull.result.runCatching -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import dev.msfjarvis.aps.BuildConfig -import dev.msfjarvis.aps.util.services.PasswordExportService -import dev.msfjarvis.aps.R -import dev.msfjarvis.aps.ui.crypto.BasePgpActivity -import dev.msfjarvis.aps.ui.git.config.GitConfigActivity -import dev.msfjarvis.aps.ui.git.config.GitServerConfigActivity -import dev.msfjarvis.aps.util.git.sshj.SshKey -import dev.msfjarvis.aps.util.pwgenxkpwd.XkpwdDictionary -import dev.msfjarvis.aps.ui.sshkeygen.ShowSshKeyFragment -import dev.msfjarvis.aps.ui.sshkeygen.SshKeyGenActivity -import dev.msfjarvis.aps.ui.proxy.ProxySelectorActivity -import dev.msfjarvis.aps.util.auth.BiometricAuthenticator -import dev.msfjarvis.aps.data.repo.PasswordRepository -import dev.msfjarvis.aps.util.settings.PreferenceKeys -import dev.msfjarvis.aps.util.extensions.autofillManager -import dev.msfjarvis.aps.util.extensions.getEncryptedGitPrefs -import dev.msfjarvis.aps.util.extensions.getString -import dev.msfjarvis.aps.util.extensions.sharedPrefs -import java.io.File - -typealias ClickListener = Preference.OnPreferenceClickListener -typealias ChangeListener = Preference.OnPreferenceChangeListener - -class UserPreference : AppCompatActivity() { - - private lateinit var prefsFragment: PrefsFragment - private var fromIntent = false - - @Suppress("DEPRECATION") - private val directorySelectAction = registerForActivityResult(OpenDocumentTree()) { uri: Uri? -> - if (uri == null) return@registerForActivityResult - - tag(TAG).d { "Selected repository URI is $uri" } - // TODO: This is fragile. Workaround until PasswordItem is backed by DocumentFile - val docId = DocumentsContract.getTreeDocumentId(uri) - val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() - val path = if (split.size > 1) split[1] else split[0] - val repoPath = "${Environment.getExternalStorageDirectory()}/$path" - val prefs = sharedPrefs - - tag(TAG).d { "Selected repository path is $repoPath" } - - if (Environment.getExternalStorageDirectory().path == repoPath) { - MaterialAlertDialogBuilder(this) - .setTitle(getString(R.string.sdcard_root_warning_title)) - .setMessage(getString(R.string.sdcard_root_warning_message)) - .setPositiveButton("Remove everything") { _, _ -> - prefs.edit { putString(PreferenceKeys.GIT_EXTERNAL_REPO, uri.path) } - } - .setNegativeButton(R.string.dialog_cancel, null) - .show() - } - prefs.edit { putString(PreferenceKeys.GIT_EXTERNAL_REPO, repoPath) } - if (fromIntent) { - setResult(RESULT_OK) - finish() - } - - } - - private val sshKeyImportAction = registerForActivityResult(OpenDocument()) { uri: Uri? -> - if (uri == null) return@registerForActivityResult - runCatching { - SshKey.import(uri) - - Toast.makeText(this, resources.getString(R.string.ssh_key_success_dialog_title), Toast.LENGTH_LONG).show() - setResult(RESULT_OK) - finish() - }.onFailure { e -> - MaterialAlertDialogBuilder(this) - .setTitle(resources.getString(R.string.ssh_key_error_dialog_title)) - .setMessage(e.message) - .setPositiveButton(resources.getString(R.string.dialog_ok), null) - .show() - } - } - - private val storeExportAction = registerForActivityResult(object : OpenDocumentTree() { - override fun createIntent(context: Context, input: Uri?): Intent { - return 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 - } - } - }) { uri: Uri? -> - if (uri == null) return@registerForActivityResult - val targetDirectory = DocumentFile.fromTreeUri(applicationContext, uri) - - if (targetDirectory != null) { - val service = Intent(applicationContext, PasswordExportService::class.java).apply { - action = PasswordExportService.ACTION_EXPORT_PASSWORD - putExtra("uri", uri) - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - startForegroundService(service) - } else { - startService(service) - } - } - } - - private val storeCustomXkpwdDictionaryAction = registerForActivityResult(OpenDocument()) { uri -> - if (uri == null) return@registerForActivityResult - - Toast.makeText( - this, - this.resources.getString(R.string.xkpwgen_custom_dict_imported, uri.path), - Toast.LENGTH_SHORT - ).show() - - sharedPrefs.edit { putString(PreferenceKeys.PREF_KEY_CUSTOM_DICT, uri.toString()) } - - val customDictPref = prefsFragment.findPreference(PreferenceKeys.PREF_KEY_CUSTOM_DICT) - setCustomDictSummary(customDictPref, uri) - // copy user selected file to internal storage - val inputStream = contentResolver.openInputStream(uri) - val customDictFile = File(filesDir.toString(), XkpwdDictionary.XKPWD_CUSTOM_DICT_FILE).outputStream() - inputStream?.copyTo(customDictFile, 1024) - inputStream?.close() - customDictFile.close() - - setResult(RESULT_OK) - } - - class PrefsFragment : PreferenceFragmentCompat() { - - private var autoFillEnablePreference: SwitchPreferenceCompat? = null - private var clearSavedPassPreference: Preference? = null - private var viewSshKeyPreference: Preference? = null - private lateinit var oreoAutofillDependencies: List - private lateinit var prefsActivity: UserPreference - private lateinit var sharedPreferences: SharedPreferences - private lateinit var encryptedPreferences: SharedPreferences - - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - prefsActivity = requireActivity() as UserPreference - val context = requireContext() - sharedPreferences = preferenceManager.sharedPreferences - encryptedPreferences = requireActivity().getEncryptedGitPrefs() - - addPreferencesFromResource(R.xml.preference) - - // Git preferences - val gitServerPreference = findPreference(PreferenceKeys.GIT_SERVER_INFO) - val openkeystoreIdPreference = findPreference(PreferenceKeys.SSH_OPENKEYSTORE_CLEAR_KEY_ID) - val gitConfigPreference = findPreference(PreferenceKeys.GIT_CONFIG) - val sshKeyPreference = findPreference(PreferenceKeys.SSH_KEY) - val sshKeygenPreference = findPreference(PreferenceKeys.SSH_KEYGEN) - viewSshKeyPreference = findPreference(PreferenceKeys.SSH_SEE_KEY) - clearSavedPassPreference = findPreference(PreferenceKeys.CLEAR_SAVED_PASS) - val deleteRepoPreference = findPreference(PreferenceKeys.GIT_DELETE_REPO) - val externalGitRepositoryPreference = findPreference(PreferenceKeys.GIT_EXTERNAL) - val selectExternalGitRepositoryPreference = findPreference(PreferenceKeys.PREF_SELECT_EXTERNAL) - - if (!PasswordRepository.isGitRepo()) { - listOfNotNull( - gitServerPreference, - gitConfigPreference, - sshKeyPreference, - viewSshKeyPreference, - clearSavedPassPreference, - ).forEach { - it.parent?.removePreference(it) - } - } - - // General preferences - val showTimePreference = findPreference(PreferenceKeys.GENERAL_SHOW_TIME) - val clearClipboard20xPreference = findPreference(PreferenceKeys.CLEAR_CLIPBOARD_20X) - - // Autofill preferences - autoFillEnablePreference = findPreference(PreferenceKeys.AUTOFILL_ENABLE) - val oreoAutofillDirectoryStructurePreference = findPreference(PreferenceKeys.OREO_AUTOFILL_DIRECTORY_STRUCTURE) - val oreoAutofillDefaultUsername = findPreference(PreferenceKeys.OREO_AUTOFILL_DEFAULT_USERNAME) - val oreoAutofillCustomPublixSuffixes = findPreference(PreferenceKeys.OREO_AUTOFILL_CUSTOM_PUBLIC_SUFFIXES) - oreoAutofillDependencies = listOfNotNull( - oreoAutofillDirectoryStructurePreference, - oreoAutofillDefaultUsername, - oreoAutofillCustomPublixSuffixes, - ) - oreoAutofillCustomPublixSuffixes?.apply { - setOnBindEditTextListener { - it.isSingleLine = false - it.setHint(R.string.preference_custom_public_suffixes_hint) - } - } - - // Misc preferences - val appVersionPreference = findPreference(PreferenceKeys.APP_VERSION) - - selectExternalGitRepositoryPreference?.summary = sharedPreferences.getString(PreferenceKeys.GIT_EXTERNAL_REPO) - ?: getString(R.string.no_repo_selected) - deleteRepoPreference?.isVisible = !sharedPreferences.getBoolean(PreferenceKeys.GIT_EXTERNAL, false) - clearClipboard20xPreference?.isVisible = sharedPreferences.getString(PreferenceKeys.GENERAL_SHOW_TIME)?.toInt() != 0 - openkeystoreIdPreference?.isVisible = sharedPreferences.getString(PreferenceKeys.SSH_OPENKEYSTORE_KEYID)?.isNotEmpty() - ?: false - - updateAutofillSettings() - updateClearSavedPassphrasePrefs() - - appVersionPreference?.summary = "Version: ${BuildConfig.VERSION_NAME}" - - sshKeyPreference?.onPreferenceClickListener = ClickListener { - prefsActivity.getSshKey() - true - } - - sshKeygenPreference?.onPreferenceClickListener = ClickListener { - prefsActivity.makeSshKey(true) - true - } - - viewSshKeyPreference?.onPreferenceClickListener = ClickListener { - val df = ShowSshKeyFragment() - df.show(parentFragmentManager, "public_key") - true - } - - clearSavedPassPreference?.onPreferenceClickListener = ClickListener { - encryptedPreferences.edit { - if (encryptedPreferences.getString(PreferenceKeys.HTTPS_PASSWORD) != null) - remove(PreferenceKeys.HTTPS_PASSWORD) - else if (encryptedPreferences.getString(PreferenceKeys.SSH_KEY_LOCAL_PASSPHRASE) != null) - remove(PreferenceKeys.SSH_KEY_LOCAL_PASSPHRASE) - } - updateClearSavedPassphrasePrefs() - true - } - - openkeystoreIdPreference?.onPreferenceClickListener = ClickListener { - sharedPreferences.edit { putString(PreferenceKeys.SSH_OPENKEYSTORE_KEYID, null) } - it.isVisible = false - true - } - - gitServerPreference?.onPreferenceClickListener = ClickListener { - startActivity(Intent(prefsActivity, GitServerConfigActivity::class.java)) - true - } - - gitConfigPreference?.onPreferenceClickListener = ClickListener { - startActivity(Intent(prefsActivity, GitConfigActivity::class.java)) - true - } - - deleteRepoPreference?.onPreferenceClickListener = ClickListener { - val repoDir = PasswordRepository.getRepositoryDirectory() - MaterialAlertDialogBuilder(prefsActivity) - .setTitle(R.string.pref_dialog_delete_title) - .setMessage(resources.getString(R.string.dialog_delete_msg, repoDir)) - .setCancelable(false) - .setPositiveButton(R.string.dialog_delete) { dialogInterface, _ -> - runCatching { - PasswordRepository.getRepositoryDirectory().deleteRecursively() - PasswordRepository.closeRepository() - }.onFailure { - // TODO Handle the different cases of exceptions - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { - requireContext().getSystemService()?.apply { - removeDynamicShortcuts(dynamicShortcuts.map { it.id }.toMutableList()) - } - } - sharedPreferences.edit { putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, false) } - dialogInterface.cancel() - prefsActivity.finish() - } - .setNegativeButton(R.string.dialog_do_not_delete) { dialogInterface, _ -> run { dialogInterface.cancel() } } - .show() - - true - } - - selectExternalGitRepositoryPreference?.summary = - sharedPreferences.getString(PreferenceKeys.GIT_EXTERNAL_REPO) - ?: context.getString(R.string.no_repo_selected) - selectExternalGitRepositoryPreference?.onPreferenceClickListener = ClickListener { - prefsActivity.selectExternalGitRepository() - true - } - - val resetRepo = Preference.OnPreferenceChangeListener { _, o -> - deleteRepoPreference?.isVisible = !(o as Boolean) - PasswordRepository.closeRepository() - sharedPreferences.edit { putBoolean(PreferenceKeys.REPO_CHANGED, true) } - true - } - - selectExternalGitRepositoryPreference?.onPreferenceChangeListener = resetRepo - externalGitRepositoryPreference?.onPreferenceChangeListener = resetRepo - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - autoFillEnablePreference?.onPreferenceClickListener = ClickListener { - onEnableAutofillClick() - true - } - } - - findPreference(PreferenceKeys.EXPORT_PASSWORDS)?.apply { - isVisible = sharedPreferences.getBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, false) - onPreferenceClickListener = Preference.OnPreferenceClickListener { - prefsActivity.exportPasswords() - true - } - } - - showTimePreference?.onPreferenceChangeListener = ChangeListener { _, newValue: Any? -> - runCatching { - val isEnabled = newValue.toString().toInt() != 0 - clearClipboard20xPreference?.isVisible = isEnabled - true - }.getOr(false) - } - - showTimePreference?.summaryProvider = Preference.SummaryProvider { - getString(R.string.pref_clipboard_timeout_summary, sharedPreferences.getString - (PreferenceKeys.GENERAL_SHOW_TIME, "45")) - } - - findPreference(PreferenceKeys.ENABLE_DEBUG_LOGGING)?.isVisible = !BuildConfig.ENABLE_DEBUG_FEATURES - - findPreference(PreferenceKeys.BIOMETRIC_AUTH)?.apply { - val canAuthenticate = BiometricAuthenticator.canAuthenticate(prefsActivity) - - if (!canAuthenticate) { - isEnabled = false - isChecked = false - summary = getString(R.string.biometric_auth_summary_error) - } else { - setOnPreferenceClickListener { - isEnabled = false - sharedPreferences.edit { - val checked = isChecked - BiometricAuthenticator.authenticate(requireActivity()) { result -> - when (result) { - is BiometricAuthenticator.Result.Success -> { - // Apply the changes - putBoolean(PreferenceKeys.BIOMETRIC_AUTH, checked) - isEnabled = true - } - else -> { - // If any error occurs, revert back to the previous state. This - // catch-all clause includes the cancellation case. - putBoolean(PreferenceKeys.BIOMETRIC_AUTH, !checked) - isChecked = !checked - isEnabled = true - } - } - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { - requireContext().getSystemService()?.apply { - removeDynamicShortcuts(dynamicShortcuts.map { it.id }.toMutableList()) - } - } - } - true - } - } - } - - findPreference(PreferenceKeys.PROXY_SETTINGS)?.onPreferenceClickListener = ClickListener { - startActivity(Intent(requireContext(), ProxySelectorActivity::class.java)) - true - } - - val prefCustomXkpwdDictionary = findPreference(PreferenceKeys.PREF_KEY_CUSTOM_DICT) - prefCustomXkpwdDictionary?.onPreferenceClickListener = ClickListener { - prefsActivity.storeCustomDictionaryPath() - true - } - val dictUri = sharedPreferences.getString(PreferenceKeys.PREF_KEY_CUSTOM_DICT) ?: "" - - if (!TextUtils.isEmpty(dictUri)) { - setCustomDictSummary(prefCustomXkpwdDictionary, Uri.parse(dictUri)) - } - - val prefIsCustomDict = findPreference(PreferenceKeys.PREF_KEY_IS_CUSTOM_DICT) - val prefCustomDictPicker = findPreference(PreferenceKeys.PREF_KEY_CUSTOM_DICT) - val prefPwgenType = findPreference(PreferenceKeys.PREF_KEY_PWGEN_TYPE) - updateXkPasswdPrefsVisibility(prefPwgenType?.value, prefIsCustomDict, prefCustomDictPicker) - - prefPwgenType?.onPreferenceChangeListener = ChangeListener { _, newValue -> - updateXkPasswdPrefsVisibility(newValue, prefIsCustomDict, prefCustomDictPicker) - true - } - - prefIsCustomDict?.onPreferenceChangeListener = ChangeListener { _, newValue -> - if (!(newValue as Boolean)) { - val customDictFile = File(context.filesDir, XkpwdDictionary.XKPWD_CUSTOM_DICT_FILE) - if (customDictFile.exists() && !customDictFile.delete()) { - w { "Failed to delete custom XkPassword dictionary: $customDictFile" } - } - prefCustomDictPicker?.setSummary(R.string.xkpwgen_pref_custom_dict_picker_summary) - } - true - } - } - - private fun updateXkPasswdPrefsVisibility(newValue: Any?, prefIsCustomDict: CheckBoxPreference?, prefCustomDictPicker: Preference?) { - when (newValue as String) { - BasePgpActivity.KEY_PWGEN_TYPE_CLASSIC -> { - prefIsCustomDict?.isVisible = false - prefCustomDictPicker?.isVisible = false - } - BasePgpActivity.KEY_PWGEN_TYPE_XKPASSWD -> { - prefIsCustomDict?.isVisible = true - prefCustomDictPicker?.isVisible = true - } - } - } - - private fun updateAutofillSettings() { - val isAutofillServiceEnabled = prefsActivity.isAutofillServiceEnabled - val isAutofillSupported = prefsActivity.isAutofillServiceSupported - if (!isAutofillSupported) { - autoFillEnablePreference?.isVisible = false - } else { - autoFillEnablePreference?.isChecked = isAutofillServiceEnabled - } - oreoAutofillDependencies.forEach { - it.isVisible = isAutofillServiceEnabled - } - } - - private fun updateClearSavedPassphrasePrefs() { - clearSavedPassPreference?.apply { - val sshPass = encryptedPreferences.getString(PreferenceKeys.SSH_KEY_LOCAL_PASSPHRASE) - val httpsPass = encryptedPreferences.getString(PreferenceKeys.HTTPS_PASSWORD) - if (sshPass == null && httpsPass == null) { - isVisible = false - return@apply - } - title = when { - httpsPass != null -> getString(R.string.clear_saved_passphrase_https) - sshPass != null -> getString(R.string.clear_saved_passphrase_ssh) - else -> null - } - isVisible = true - } - } - - private fun updateViewSshPubkeyPref() { - viewSshKeyPreference?.isVisible = SshKey.canShowSshPublicKey - } - - @RequiresApi(Build.VERSION_CODES.O) - private fun onEnableAutofillClick() { - if (prefsActivity.isAutofillServiceEnabled) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) - prefsActivity.autofillManager!!.disableAutofillServices() - else - throw IllegalStateException("isAutofillServiceEnabled == true, but Build.VERSION.SDK_INT < Build.VERSION_CODES.O") - } else { - MaterialAlertDialogBuilder(prefsActivity).run { - setTitle(R.string.pref_autofill_enable_title) - @SuppressLint("InflateParams") - val layout = - layoutInflater.inflate(R.layout.oreo_autofill_instructions, null) - val supportedBrowsersTextView = - layout.findViewById(R.id.supportedBrowsers) - supportedBrowsersTextView.text = - getInstalledBrowsersWithAutofillSupportLevel(context).joinToString( - separator = "\n" - ) { - val appLabel = it.first - val supportDescription = when (it.second) { - BrowserAutofillSupportLevel.None -> getString(R.string.oreo_autofill_no_support) - BrowserAutofillSupportLevel.FlakyFill -> getString(R.string.oreo_autofill_flaky_fill_support) - BrowserAutofillSupportLevel.PasswordFill -> getString(R.string.oreo_autofill_password_fill_support) - BrowserAutofillSupportLevel.PasswordFillAndSaveIfNoAccessibility -> getString(R.string.oreo_autofill_password_fill_and_conditional_save_support) - BrowserAutofillSupportLevel.GeneralFill -> getString(R.string.oreo_autofill_general_fill_support) - BrowserAutofillSupportLevel.GeneralFillAndSave -> getString(R.string.oreo_autofill_general_fill_and_save_support) - } - "$appLabel: $supportDescription" - } - setView(layout) - setPositiveButton(R.string.dialog_ok) { _, _ -> - val intent = Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE).apply { - data = Uri.parse("package:${BuildConfig.APPLICATION_ID}") - } - startActivity(intent) - } - setNegativeButton(R.string.dialog_cancel, null) - setOnDismissListener { updateAutofillSettings() } - show() - } - } - } - - override fun onResume() { - super.onResume() - updateAutofillSettings() - updateClearSavedPassphrasePrefs() - updateViewSshPubkeyPref() - } - } - - override fun onBackPressed() { - super.onBackPressed() - setResult(RESULT_OK) - finish() - } - - public override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - when (intent?.getStringExtra("operation")) { - "get_ssh_key" -> getSshKey() - "make_ssh_key" -> makeSshKey(false) - "git_external" -> { - fromIntent = true - selectExternalGitRepository() - } - } - prefsFragment = PrefsFragment() - - supportFragmentManager - .beginTransaction() - .replace(android.R.id.content, prefsFragment) - .commit() - - supportActionBar?.setDisplayHomeAsUpEnabled(true) - } - - @Suppress("Deprecation") // for Environment.getExternalStorageDirectory() - fun selectExternalGitRepository() { - MaterialAlertDialogBuilder(this) - .setTitle(this.resources.getString(R.string.external_repository_dialog_title)) - .setMessage(this.resources.getString(R.string.external_repository_dialog_text)) - .setPositiveButton(R.string.dialog_ok) { _, _ -> - directorySelectAction.launch(null) - } - .setNegativeButton(R.string.dialog_cancel, null) - .show() - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - android.R.id.home -> { - setResult(RESULT_OK) - onBackPressed() - true - } - else -> super.onOptionsItemSelected(item) - } - } - - private fun importSshKey() { - sshKeyImportAction.launch(arrayOf("*/*")) - } - - /** - * Opens a file explorer to import the private key - */ - private fun getSshKey() { - if (SshKey.exists) { - MaterialAlertDialogBuilder(this).run { - setTitle(R.string.ssh_keygen_existing_title) - setMessage(R.string.ssh_keygen_existing_message) - setPositiveButton(R.string.ssh_keygen_existing_replace) { _, _ -> - importSshKey() - } - setNegativeButton(R.string.ssh_keygen_existing_keep) { _, _ -> } - show() - } - } else { - importSshKey() - } - } - - /** - * Exports the passwords - */ - private fun exportPasswords() { - storeExportAction.launch(null) - } - - /** - * Opens a key generator to generate a public/private key pair - */ - fun makeSshKey(fromPreferences: Boolean) { - val intent = Intent(applicationContext, SshKeyGenActivity::class.java) - startActivity(intent) - if (!fromPreferences) { - setResult(RESULT_OK) - finish() - } - } - - /** - * Pick custom xkpwd dictionary from sdcard - */ - private fun storeCustomDictionaryPath() { - storeCustomXkpwdDictionaryAction.launch(arrayOf("*/*")) - } - - private val isAutofillServiceSupported: Boolean - get() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return false - return autofillManager?.isAutofillSupported != null - } - - private val isAutofillServiceEnabled: Boolean - get() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return false - return autofillManager?.hasEnabledAutofillServices() == true - } - - companion object { - - private const val TAG = "UserPreference" - - fun createDirectorySelectionIntent(context: Context): Intent { - return Intent(context, UserPreference::class.java).run { - putExtra("operation", "git_external") - } - } - - /** - * Set custom dictionary summary - */ - @JvmStatic - private fun setCustomDictSummary(customDictPref: Preference?, uri: Uri) { - val fileName = uri.path?.substring(uri.path?.lastIndexOf(":")!! + 1) - customDictPref?.summary = "Selected dictionary: $fileName" - } - } -} diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/sshkeygen/SshKeyGenActivity.kt b/app/src/main/java/dev/msfjarvis/aps/ui/sshkeygen/SshKeyGenActivity.kt index b260b77e..efcdd0f3 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/sshkeygen/SshKeyGenActivity.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/sshkeygen/SshKeyGenActivity.kt @@ -144,6 +144,7 @@ class SshKeyGenActivity : AppCompatActivity() { .setTitle(getString(R.string.error_generate_ssh_key)) .setMessage(getString(R.string.ssh_key_error_dialog_text) + e.message) .setPositiveButton(getString(R.string.dialog_ok)) { _, _ -> + setResult(RESULT_OK) finish() } .show() diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/sshkeygen/SshKeyImportActivity.kt b/app/src/main/java/dev/msfjarvis/aps/ui/sshkeygen/SshKeyImportActivity.kt new file mode 100644 index 00000000..1bb1056e --- /dev/null +++ b/app/src/main/java/dev/msfjarvis/aps/ui/sshkeygen/SshKeyImportActivity.kt @@ -0,0 +1,61 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package dev.msfjarvis.aps.ui.sshkeygen + +import dev.msfjarvis.aps.R +import dev.msfjarvis.aps.util.git.sshj.SshKey +import android.net.Uri +import android.os.Bundle +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import com.github.michaelbull.result.onFailure +import com.github.michaelbull.result.runCatching +import com.google.android.material.dialog.MaterialAlertDialogBuilder + +class SshKeyImportActivity : AppCompatActivity() { + + private val sshKeyImportAction = registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri: Uri? -> + if (uri == null) { + finish() + return@registerForActivityResult + } + runCatching { + SshKey.import(uri) + Toast.makeText(this, resources.getString(R.string.ssh_key_success_dialog_title), Toast.LENGTH_LONG).show() + setResult(RESULT_OK) + finish() + }.onFailure { e -> + MaterialAlertDialogBuilder(this) + .setTitle(resources.getString(R.string.ssh_key_error_dialog_title)) + .setMessage(e.message) + .setPositiveButton(resources.getString(R.string.dialog_ok)) { _, _ -> finish() } + .show() + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (SshKey.exists) { + MaterialAlertDialogBuilder(this).run { + setTitle(R.string.ssh_keygen_existing_title) + setMessage(R.string.ssh_keygen_existing_message) + setPositiveButton(R.string.ssh_keygen_existing_replace) { _, _ -> + importSshKey() + } + setNegativeButton(R.string.ssh_keygen_existing_keep) { _, _ -> finish() } + setOnCancelListener { finish() } + show() + } + } else { + importSshKey() + } + } + + private fun importSshKey() { + sshKeyImportAction.launch(arrayOf("*/*")) + } +} diff --git a/app/src/main/java/dev/msfjarvis/aps/util/autofill/AutofillPreferences.kt b/app/src/main/java/dev/msfjarvis/aps/util/autofill/AutofillPreferences.kt index aa70bacb..bcc5aac1 100644 --- a/app/src/main/java/dev/msfjarvis/aps/util/autofill/AutofillPreferences.kt +++ b/app/src/main/java/dev/msfjarvis/aps/util/autofill/AutofillPreferences.kt @@ -9,8 +9,10 @@ import android.os.Build import androidx.annotation.RequiresApi import com.github.androidpasswordstore.autofillparser.Credentials import dev.msfjarvis.aps.data.password.PasswordEntry +import dev.msfjarvis.aps.util.extensions.getString import dev.msfjarvis.aps.util.extensions.sharedPrefs import dev.msfjarvis.aps.util.services.getDefaultUsername +import dev.msfjarvis.aps.util.settings.PreferenceKeys import java.io.File import java.nio.file.Paths @@ -110,8 +112,7 @@ enum class DirectoryStructure(val value: String) { companion object { - const val PREFERENCE = "oreo_autofill_directory_structure" - private val DEFAULT = FileBased + val DEFAULT = FileBased private val reverseMap = values().associateBy { it.value } fun fromValue(value: String?) = if (value != null) reverseMap[value] ?: DEFAULT else DEFAULT @@ -121,7 +122,7 @@ enum class DirectoryStructure(val value: String) { object AutofillPreferences { fun directoryStructure(context: Context): DirectoryStructure { - val value = context.sharedPrefs.getString(DirectoryStructure.PREFERENCE, null) + val value = context.sharedPrefs.getString(PreferenceKeys.OREO_AUTOFILL_DIRECTORY_STRUCTURE) return DirectoryStructure.fromValue(value) } diff --git a/app/src/main/java/dev/msfjarvis/aps/util/git/operation/GitOperation.kt b/app/src/main/java/dev/msfjarvis/aps/util/git/operation/GitOperation.kt index 44292fc6..83c25a7e 100644 --- a/app/src/main/java/dev/msfjarvis/aps/util/git/operation/GitOperation.kt +++ b/app/src/main/java/dev/msfjarvis/aps/util/git/operation/GitOperation.kt @@ -15,7 +15,6 @@ import com.github.michaelbull.result.onFailure import com.github.michaelbull.result.runCatching import com.google.android.material.dialog.MaterialAlertDialogBuilder import dev.msfjarvis.aps.R -import dev.msfjarvis.aps.ui.settings.UserPreference import dev.msfjarvis.aps.util.git.GitCommandExecutor import dev.msfjarvis.aps.util.settings.AuthMode import dev.msfjarvis.aps.util.settings.GitSettings @@ -25,6 +24,8 @@ import dev.msfjarvis.aps.util.git.sshj.SshKey import dev.msfjarvis.aps.util.git.sshj.SshjSessionFactory import dev.msfjarvis.aps.util.auth.BiometricAuthenticator import dev.msfjarvis.aps.data.repo.PasswordRepository +import dev.msfjarvis.aps.ui.sshkeygen.SshKeyGenActivity +import dev.msfjarvis.aps.ui.sshkeygen.SshKeyImportActivity import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine import kotlinx.coroutines.Dispatchers @@ -92,9 +93,11 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) { private fun getSshKey(make: Boolean) { runCatching { - // Ask the UserPreference to provide us with the ssh-key - val intent = Intent(callingActivity.applicationContext, UserPreference::class.java) - intent.putExtra("operation", if (make) "make_ssh_key" else "get_ssh_key") + val intent = if (make) { + Intent(callingActivity.applicationContext, SshKeyGenActivity::class.java) + } else { + Intent(callingActivity.applicationContext, SshKeyImportActivity::class.java) + } callingActivity.startActivity(intent) }.onFailure { e -> e(e) diff --git a/app/src/main/res/drawable/app_settings_alt_24px.xml b/app/src/main/res/drawable/app_settings_alt_24px.xml new file mode 100644 index 00000000..a6dce94c --- /dev/null +++ b/app/src/main/res/drawable/app_settings_alt_24px.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_call_merge_24px.xml b/app/src/main/res/drawable/ic_call_merge_24px.xml new file mode 100644 index 00000000..1ecd5dac --- /dev/null +++ b/app/src/main/res/drawable/ic_call_merge_24px.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_miscellaneous_services_24px.xml b/app/src/main/res/drawable/ic_miscellaneous_services_24px.xml new file mode 100644 index 00000000..3fc185c5 --- /dev/null +++ b/app/src/main/res/drawable/ic_miscellaneous_services_24px.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_wysiwyg_24px.xml b/app/src/main/res/drawable/ic_wysiwyg_24px.xml new file mode 100644 index 00000000..7549868b --- /dev/null +++ b/app/src/main/res/drawable/ic_wysiwyg_24px.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/app/src/main/res/layout/activity_preference_recyclerview.xml b/app/src/main/res/layout/activity_preference_recyclerview.xml new file mode 100644 index 00000000..fe8cfa8e --- /dev/null +++ b/app/src/main/res/layout/activity_preference_recyclerview.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index df28d86c..50de1a02 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -89,43 +89,42 @@ Als Klartext teilen Zuletzt geändert %s - Repository - Git-Server Einstellungen + Repository + Git-Server Einstellungen Lokale Git Konfiguration & Dienstprogramme - Importiere SSH-Key + Importiere SSH-Key Erstelle SSH-Schlüsselpaar Zeige erstellten öffentlichen SSH-Key - Repository löschen + Repository löschen Repository löschen Allgemein - Passwörter + Passwörter Timeout für das Kopieren des Passwortes Legen Sie die Zeit (in Sekunden) fest, die das Passwort in der Zwischenablage liegen soll. 0 bedeutet für immer. Aktueller Wert: %1$s Kopiere Passwort automatisch - Kopiert das Passwort in die Zwischenablage, wenn der Eintrag entschlüsselt wurde. + Kopiert das Passwort in die Zwischenablage, wenn der Eintrag entschlüsselt wurde. Die ausgewählte Datei scheint kein privater SSH-Schlüssel zu sein. SSH-Key importiert Schlüssel-Importfehler Nachricht : \n - Suche in Unterordnern - Findet Passwörter auch in Unterordnern. + Suche in Unterordnern + Findet Passwörter auch in Unterordnern. Passwortsortierung Ordner zuerst Dateien zuerst Typ unabhängig Zuletzt verwendet - Automatisch ausfüllen + Automatisch ausfüllen Autofill aktivieren - Verschiedenes + Verschiedenes Lösche die Zwischenablage 20-mal - Speichert Nonsense 20-mal anstatt 1-mal in der Zwischenablage. Nützlich bspw. auf Samsung-Geräten, die den Verlauf der Zwischenablage speichern. + Speichert Nonsense 20-mal anstatt 1-mal in der Zwischenablage. Nützlich bspw. auf Samsung-Geräten, die den Verlauf der Zwischenablage speichern. Lösche das lokale (versteckte) Repository - Externes Repository + Externes Repository Nutze ein externes Repository - Wähle ein externes Repository + Wähle ein externes Repository Passwörter exportieren Exportiert die verschlüsselten Passwörter in ein externes Verzeichnis - Version Passwort generieren Generieren @@ -147,12 +146,12 @@ Eigene Wortliste: %1$s Das ausgewählte Wörterbuch enthält nicht genügend Wörter der angegebenen Länge %1$d..%2$d - Passwortgenerator - Eigene Wortliste - Eigene Wordlist-Datei verwenden - Integrierte Wortliste verwenden - Eigene Wortliste - Tippen Sie, um eine benutzerdefinierte Wordlist-Datei mit einem Wort pro Zeile auszuwählen + Passwortgenerator + Eigene Wortliste + Eigene Wordlist-Datei verwenden + Integrierte Wortliste verwenden + Eigene Wortliste + Tippen Sie, um eine benutzerdefinierte Wordlist-Datei mit einem Wort pro Zeile auszuwählen Passwort Generieren @@ -192,7 +191,7 @@ Soll weiterer Inhalt sichtbar sein? Generieren Aktualisieren - Kein externes Repository ausgewählt + Kein externes Repository ausgewählt Passwort unverschlüsselt senden an… App Icon @@ -238,10 +237,10 @@ Biometrische Abfrage Authentifizierungsfehler Authentifizierungsfehler: %s - Biometrische Authentifizierung aktivieren - Wenn aktiviert, werden Sie beim Starten der App nach Ihrem Fingerabdruck gefragt - Fingerabdrucksensor fehlt oder ist nicht ansprechbar - Lösche gespeicherte OpenKeystore SSH-Schlüssel-ID + Biometrische Authentifizierung aktivieren + Wenn aktiviert, werden Sie beim Starten der App nach Ihrem Fingerabdruck gefragt + Fingerabdrucksensor fehlt oder ist nicht ansprechbar + Lösche gespeicherte OpenKeystore SSH-Schlüssel-ID Der Speicherort befindet sich in Ihrer SD-Karte oder im internen Speicher, aber die App hat nicht die Berechtigung, darauf zuzugreifen. Ihr öffentlicher Schlüssel Fehler beim Generieren des SSH-Schlüssels @@ -254,16 +253,15 @@ Ziel muss innerhalb des repository sein Ziel für %1$s angeben Erstellen - Suchfeld beim Start öffnen - Suchleiste beim Start der App anzeigen - Passwort Generator + Suchfeld beim Start öffnen + Suchleiste beim Start der App anzeigen Hier tippen, um die Zwischenablage zu löschen Das Repository muss geklont werden, bevor Änderungen synchronisert werden können. - App Farbthema - Hell - Dunkel - Durch Energiesparmodus gesetzt - Systemstandard + App Farbthema + Hell + Dunkel + Durch Energiesparmodus gesetzt + Systemstandard SSH-Schlüssel Passwort Konfiguration erfolgreich gespeichert diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index d53cac7e..73008ecf 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -91,43 +91,42 @@ Partager en clair Dernière modification le %s - Dépôt - Modifier les paramètres du serveur Git + Dépôt + Modifier les paramètres du serveur Git Configuration locale de Git & utilitaires - Importer une clef SSH + Importer une clef SSH Générer une paire de clefs SSH Voir la clef publique SSH générée - Supprimer le dépôt + Supprimer le dépôt Effacer le dépôt Général - Mot de passe + Mot de passe Délai imparti pour la copie Définissez le temps (en secondes) durant lequel le mot de passe restera dans le presse-papiers. 0 pour une rétention illimitée. Valeur actuelle: %1$s Copie automatique du mot de passe - Copie automatiquement le mot de passe vers le presse-papier si le déchiffrement a réussi. + Copie automatiquement le mot de passe vers le presse-papier si le déchiffrement a réussi. Le fichier sélectionné ne semble pas être une clé privée SSH. Clef SSH importée Erreur d\'importation de la clé Message : \n - Filtre récursif - Cherche le mot de passe dans tous les sous-répertoires du répertoire actuel. + Filtre récursif + Cherche le mot de passe dans tous les sous-répertoires du répertoire actuel. Ordre de tri des mots de passe Dossiers en premier Fichiers en premier Indifférent au type d\'entrée Récemment utilisé - Saisie automatique + Saisie automatique Saisie automatique - Divers + Divers Effacer le presse-papier 20 fois - Enregistre des informations absurdes dans le presse-papier 20 fois à la place d\'une seule. Utile sur les téléphones Samsung qui disposent d\'un historique du presse-papier. + Enregistre des informations absurdes dans le presse-papier 20 fois à la place d\'une seule. Utile sur les téléphones Samsung qui disposent d\'un historique du presse-papier. Supprime le dépot local (caché) - Dépôt externe + Dépôt externe Utilise un dépôt externe pour les mots de passe - Choisissez un dépôt externe + Choisissez un dépôt externe Exporter les mots de passe Exporter les mots de passe (chiffrés) vers un répertoire externe - Version Générer un mot de passe Générer @@ -149,12 +148,12 @@ Liste de mots personnalisée : %1$s Le dictionnaire sélectionné ne contient pas assez de mots de la longueur %1$d..%2$d - Type de générateur de mot de passe - Liste de mots personnalisée - Utiliser une liste de mots personnalisée - Utilisation de la liste de mots intégrée - Fichier personnalisé de liste de mots - Touchez pour choisir un fichier de liste de mots personnalisés contenant un mot par ligne + Type de générateur de mot de passe + Liste de mots personnalisée + Utiliser une liste de mots personnalisée + Utilisation de la liste de mots intégrée + Fichier personnalisé de liste de mots + Touchez pour choisir un fichier de liste de mots personnalisés contenant un mot par ligne Mot de passe Générer @@ -191,7 +190,7 @@ Controller la visibilité du contenu supplémentaire une fois déchiffré Générer Rafraichir la liste - Pas de dépôt externe séléctionné + Pas de dépôt externe séléctionné Envoyer le mot de passe en clair via… Icône de l\'application @@ -224,9 +223,9 @@ Se rappeler de la phrase secrète dans la configuration de l\'application (peu sûr) Réinitialisation dure de la branche distante Identification biométrique - Authentification biométrique - Lorsque cette option est activée, Password Store vous demandera votre empreinte digitale au lancement - Lecteur d\'empreinte digitale non accessible ou manquant + Authentification biométrique + Lorsque cette option est activée, Password Store vous demandera votre empreinte digitale au lancement + Lecteur d\'empreinte digitale non accessible ou manquant Votre clé publique Une erreur est survenue pendant la génération de la clé ssh Afficher tous les fichiers et dossiers @@ -235,14 +234,13 @@ Renommer le dossier Entrez le chemin pour %1$s Créer - Ouvrir la recherche au démarrage - Ouvrir la barre de recherche au démarrage de l\'application - Générateur de mot de passe + Ouvrir la recherche au démarrage + Ouvrir la barre de recherche au démarrage de l\'application Appuyez ici pour effacer le presse-papiers - Thème de l\'application - Clair - Sombre - Thème système + Thème de l\'application + Clair + Sombre + Thème système Clé SSH Mot de passe L\'URL du dépôt fournie n\'est pas valide diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 89b4da27..5de9af33 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -93,43 +93,42 @@ Compartir como texto plano Último cambio %s - Repositorio - Editar axustes do servidor git + Repositorio + Editar axustes do servidor git Utilidades Git - Importar chave SSH + Importar chave SSH Crear par de chaves SSH Ver a chave pública SSH creada - Eliminar repositorio + Eliminar repositorio Baleirar repositorio Xeral - Contrasinais + Contrasinais Caducidade do copiado do contrasinal Establece os segundos que queres que o contrasinal permaneza copiado no portapapeis. 0 significa para sempre. Valor actual: %1$s Copiar contrasinal automáticamente - Copia automáticamente o contrasinal ao portapapeis se o descifra correctamente + Copia automáticamente o contrasinal ao portapapeis se o descifra correctamente O ficheiro escollido non semella ser unha chave privada SSH. Chave-SSH importada Houbo un fallo ao importar a chave ssh Mensaxe : \n - Filtro recursivo - Atopa as chaves de xeito recursivo no directorio actual. + Filtro recursivo + Atopa as chaves de xeito recursivo no directorio actual. Orde para mostrar contrasinais Primeiro cartafoles Primeiro ficheiros Independentemente do tipo Usadas recentemente - Completado automático + Completado automático Activar completado automático - Varios + Varios Baleirar portapapeis 20 veces - Gardar números consecutivos no portapapeis 20 veces. Resulta útil nos móbiles Samsung que gardan historial no portapapeis. + Gardar números consecutivos no portapapeis 20 veces. Resulta útil nos móbiles Samsung que gardan historial no portapapeis. Elimina repositorio local (oculto). - Repositorio externo + Repositorio externo Usar un repositorio externo de contrasinais - Selecciona repositorio externo + Selecciona repositorio externo Exportar contrasinais Exporta os contrasinais cifrados a un directorio externo - Versión Crear contrasinal Crear @@ -151,12 +150,12 @@ Lista persoal de palabras: %1$s O dicionario non contén palabras suficientes da lonxitude dada %1$d .. %2$d - Tipo de creador de contrasinais - Lista persoal de palabras - Usar ficheiro con palabras personalizadas - Usar lista de palabras incluída - Ficheiro persoal de palabras - Toca para escoller un ficheiro persoal con palabras que conteña unha palabra por liña + Tipo de creador de contrasinais + Lista persoal de palabras + Usar ficheiro con palabras personalizadas + Usar lista de palabras incluída + Ficheiro persoal de palabras + Toca para escoller un ficheiro persoal con palabras que conteña unha palabra por liña Frase de paso Crear @@ -196,7 +195,7 @@ Controla a visibilidade do contido extra unha vez descifrado Crear Actualizar lista - Non hai seleccionado ningún repositorio externo + Non hai seleccionado ningún repositorio externo Enviar contrasinal como texto plano usando... Icona da app @@ -249,10 +248,10 @@ a app desde unha fonte de confianza, como a Play Store, Amazon Appstore, F-Droid Petición biométrica Fallo de autenticación Fallou a autenticación: %s - Activar autenticación biométrica - Ao activala, Password Store vaiche pedir a túa pegada dactilar ao iniciar a app - O hardware de pegada dixital non é accesible ou existente - Eliminar o ID lembrado da chave SSH en OpenKeystore + Activar autenticación biométrica + Ao activala, Password Store vaiche pedir a túa pegada dactilar ao iniciar a app + O hardware de pegada dixital non é accesible ou existente + Eliminar o ID lembrado da chave SSH en OpenKeystore O almacenaxe está na tarxeta SD pero a app non ten permiso para acceder a el. Por favor concédelle permiso. A túa chave pública Algo fallou ao intentar crear a chave-ssh @@ -265,16 +264,15 @@ a app desde unha fonte de confianza, como a Play Store, Amazon Appstore, F-Droid O destino debe estar dentro do repositorio Escribe o destino para %1$s Crear - Abrir busca ao inicio - Abrir a barra de busca cando se inicia a app - Creador de contrasinais + Abrir busca ao inicio + Abrir a barra de busca cando se inicia a app Toca aquí para baleirar o portapapeis Clonar un repositorio git para sincronizar os cambios - Decorado da App - Claro - Escuro - Establecido polo Aforrador de enerxía - Por omisión do sistema + Decorado da App + Claro + Escuro + Establecido polo Aforrador de enerxía + Por omisión do sistema Chave SSH Contrasinal Configuración gardada @@ -354,7 +352,7 @@ a app desde unha fonte de confianza, como a Play Store, Amazon Appstore, F-Droid Servidor proxy Porto - Axustes do proxy HTTP(S) + Axustes do proxy HTTP(S) URL non válido Completa e garda contrasinais (gardar require que os servizos de accesibilidade non estén activados) Eliminar chave do host gardada diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 5fcd812d..103283ca 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -93,43 +93,42 @@ Condividi come testo semplice Ultima modifica %s - Repository - Modifica impostazioni del server di Git + Repository + Modifica impostazioni del server di Git Configurazione & utilità della configurazione di Git - Importa chiave SSH + Importa chiave SSH Genera coppia di chiavi SSH Visualizza la chiave SSH pubblica generata - Elimina repository + Elimina repository Cancella repository Generale - Password + Password Timeout copia della password Imposta per quanto tempo (in secondi) vuoi che la password rimanga negli appunti. 0 significa per sempre. Valore corrente: %1$s Copia automaticamente la password - Copia automaticamente la password negli appunti dopo il successo della decifratura. + Copia automaticamente la password negli appunti dopo il successo della decifratura. Il file selezionato non sembra essere una chiave privata SSH. Chiave SSH importata Errore di importazione della chiave Messaggio : \n - Filtro ricorsivo - Trova ricorsivamente le password della directory corrente. + Filtro ricorsivo + Trova ricorsivamente le password della directory corrente. Ordine password Prima le cartelle Prima i file Tipo indipendente Usato di recente - Auto-compilazione + Auto-compilazione Abilita Auto-Compilazione - Varie + Varie Cancella 20 volte gli appunti - Archivia i numeri consecutivi negli appunti per 20 volte. Utile sui telefoni Samsung che presentano la cronologia degli appunti. + Archivia i numeri consecutivi negli appunti per 20 volte. Utile sui telefoni Samsung che presentano la cronologia degli appunti. Elimina repository locale (nascosta) - Repository Esterna + Repository Esterna Usa una repository di password esterna - Seleziona repository esterna + Seleziona repository esterna Esporta password Esporta le password crittografate ad una directory esterna - Versione Genera Password Genera @@ -151,12 +150,12 @@ Lista di parole personalizzata: %1$s Il dizionario selezionato non contiene abbastanza parole della data lunghezza %1$d..%2$d - Tipo di generatore di password - Lista di parole personalizzata - Usando file di elenco di parole personalizzati - Usando liste di parole integrate - File di elenco di parole personalizzato - Tocca per selezionare un file di lista di parole personalizzato contenente una parola per riga + Tipo di generatore di password + Lista di parole personalizzata + Usando file di elenco di parole personalizzati + Usando liste di parole integrate + File di elenco di parole personalizzato + Tocca per selezionare un file di lista di parole personalizzato contenente una parola per riga Frase Segreta Genera @@ -196,7 +195,7 @@ Controlla la visibilità del contenuto extra una volta decrittografato. Genera Aggiorna elenco - Nessuna repository esterna selezionata + Nessuna repository esterna selezionata Invia password come testo semplice usando… Icona app @@ -248,10 +247,10 @@ Richiesta Biometrica Autenticazione non riuscita Autenticazione non riuscita: %s - Abilita autenticazione biometrica - Quando abilitata, il Password Store ti chiederà la tua impronta digitale al lancio dell\'app - L\'hardware delle impronte digitali non è accessibile o mancante - Elimina l\'ID della Chiave SSH di OpenKeystore memorizzato + Abilita autenticazione biometrica + Quando abilitata, il Password Store ti chiederà la tua impronta digitale al lancio dell\'app + L\'hardware delle impronte digitali non è accessibile o mancante + Elimina l\'ID della Chiave SSH di OpenKeystore memorizzato La posizione dell\'archiviazione nella tua Scheda SD o Archiviazione Interna, ma l\'app non ha i permessi per accedervi. La tua chiave pubblica Errore provando a generare la chiave-ssh @@ -264,16 +263,15 @@ La destinazione deve essere nella repository Inserire destinazione per %1$s Crea - Apri ricerca all\'avvio - Apri la barra di ricerca al lancio dell\'app - Generatore di Password + Apri ricerca all\'avvio + Apri la barra di ricerca al lancio dell\'app Tocca qui per cancellare gli appunti La repository deve essere clonata prima di sincronizzare le modifiche. - Tema dell\'App - Chiaro - Scuro - Impostato dal Risparmio Energetico - Predefinito del sistema + Tema dell\'App + Chiaro + Scuro + Impostato dal Risparmio Energetico + Predefinito del sistema Chiave SSH Password Configurazione correttamente salvata @@ -353,7 +351,7 @@ Nome host del proxy Porta - Impostazioni proxy HTTP(S) + Impostazioni proxy HTTP(S) URL non valido Compila e salva le password (il salvataggio necessita che nessun servizio di accessibilità sia abilitato) Cancella la chiave host salvata diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index a26ca996..ff6b5fc5 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -93,43 +93,42 @@ Compartilhar como texto Última alteração %s - Repositório - Editar configurações do servidor Git + Repositório + Editar configurações do servidor Git Configuração local do Git & utilitários - Importar chave SSH + Importar chave SSH Gerar um par de chave SSH Ver a chave SSH pública gerada - Excluir repositório + Excluir repositório Limpar repositório Geral - Senhas + Senhas Tempo limite de cópia da senha Definir o tempo (em segundos) que a senha está na área de transferência. 0 significa para sempre. Valor atual: %1$s Copiar senha automaticamente - Automaticamente copie a senha para a área de transferência após a descriptografia ser bem sucedida. + Automaticamente copie a senha para a área de transferência após a descriptografia ser bem sucedida. O arquivo selecionado não parece ser uma chave SSH privada. Chave SSH importada Erro ao importar chave Mensagem : \n - Filtragem recursiva - Encontrar senhas do diretório corrente recursivamente. + Filtragem recursiva + Encontrar senhas do diretório corrente recursivamente. Ordenação da Senha Pastas primeiro Arquivos primeiro Tipo independente Usado recentemente - Preenchimento Automático + Preenchimento Automático Ativar preenchimento automático - Outros + Outros Limpar área de transferência 20 vezes - Armazene números consecutivos na área de transferência 20 vezes. Útil em telefones Samsung que apresentam o histórico da área de transferência. + Armazene números consecutivos na área de transferência 20 vezes. Útil em telefones Samsung que apresentam o histórico da área de transferência. Exclui o repositório local (oculto) - Repositório externo + Repositório externo Use um repositório de senha externo - Selecionar repositório externo + Selecionar repositório externo Exportar senhas Exporta as senhas criptografadas para um diretório externo - Versão Gerar Senha Gerar @@ -151,12 +150,12 @@ Lista de palavras personalizada: %1$s O dicionário selecionado não contém palavras suficientes de tamanho %1$d..%2$d - Tipo de gerador de senha - Lista de palavras personalizadas - Usando um arquivo de Lista de Palavras - Usando o arquivo de palavras embutido - Lista de palavras personalizadas - Toque para escolher um arquivo personalizado de lista de palavras contendo uma palavra por linha + Tipo de gerador de senha + Lista de palavras personalizadas + Usando um arquivo de Lista de Palavras + Usando o arquivo de palavras embutido + Lista de palavras personalizadas + Toque para escolher um arquivo personalizado de lista de palavras contendo uma palavra por linha Frase Secreta Gerar @@ -196,7 +195,7 @@ Controlar a visibilidade do conteúdo extra uma vez descriptografado. Gerar Atualizar lista - Nenhum repositório externo selecionado + Nenhum repositório externo selecionado Enviar senha como texto simples usando… Ícone do aplicativo @@ -247,10 +246,10 @@ Confirmação Biométrica Falha de autenticação Falha de autenticação: %s - Ativar autenticação biométrica - Quando ativado, o Password Store irá pedir a sua impressão digital ao iniciar o aplicativo - Hardware de impressão digital não acessível ou ausente - Limpar ID de chave SSH lembrada do OpenKeystore + Ativar autenticação biométrica + Quando ativado, o Password Store irá pedir a sua impressão digital ao iniciar o aplicativo + Hardware de impressão digital não acessível ou ausente + Limpar ID de chave SSH lembrada do OpenKeystore O local do armazenamento está em seu cartão SD ou armazenamento interno, mas o aplicativo não tem permissão para acessá-lo. Sua chave pública Erro ao tentar gerar a chave SSH @@ -263,16 +262,15 @@ O destino deve estar dentro do repositório Insira o destino para %1$s Criar - Abrir pesquisa ao inicializar - Abrir barra de pesquisa quando o aplicativo for iniciado - Gerador de Senha + Abrir pesquisa ao inicializar + Abrir barra de pesquisa quando o aplicativo for iniciado Toque aqui para limpar a área de transferência O repositório deve ser clonado antes de sincronizar as alterações. - Tema do aplicativo - Claro - Escuro - Configurado pela Economia de bateria - Padrão do sistema + Tema do aplicativo + Claro + Escuro + Configurado pela Economia de bateria + Padrão do sistema Chave SSH Senha Configuração salva com sucesso diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index e8ca9488..f0e3c6d4 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -97,43 +97,42 @@ Поделиться в открытом виде Последние изменение %s - Репозиторий - Изменить настройки сервера Git + Репозиторий + Изменить настройки сервера Git Конфигурация локального Git - Импортировать SSH ключ + Импортировать SSH ключ Сгенерировать пару SSH ключей Просмотреть публичный SSH ключ - Удалить репозиторий + Удалить репозиторий Очистить репозиторий Общие - Пароли + Пароли Срок хранения пароля в буфере обмена Установите время (в секундах), которое вы хотите, чтобы пароль был в буфере обмена. 0 означает навсегда. Текущее значение: %1$s Автоматически копировать пароль - Автоматически копировать пароль в буфер обмена после успешного расшифрования + Автоматически копировать пароль в буфер обмена после успешного расшифрования Выбранный файл не похож на приватный SSH-ключ. SSH ключ импортирован Ошибка импорта ключа Сообщение: \n - Рекурсивная фильтрация - Рекурсивный поиск паролей в текущей директории + Рекурсивная фильтрация + Рекурсивный поиск паролей в текущей директории Порядок сортировки паролей Сначала папки Сначала файлы Типонезависимый Недавно использованные - Автозаполнение + Автозаполнение Включить автозаполнение - Другое + Другое Очистить буфер 20 раз - Вставлять чепуху в буфер обмена 20 раз вместо одного. Полезно на некоторых моделях телефонов с запоминанием истории буфера обмена + Вставлять чепуху в буфер обмена 20 раз вместо одного. Полезно на некоторых моделях телефонов с запоминанием истории буфера обмена Удалить локальный (скрытый) репозиторий - Внешний репозиторий + Внешний репозиторий Использовать внешний репозиторий - Выбрать внешний репозиторий + Выбрать внешний репозиторий Экспортировать пароли Экспортировать пароли в открытом виде во внешнее хранилище - Версия Сгенерировать пароль Сгенерировать @@ -155,12 +154,12 @@ Пользовательский список слов: %1$s Выбранный словарь не содержит достаточного количества слова заданной длинны %1$d..%2$d - Тип генератора паролей - Пользовательский список слов - Использовать файл списка слов созданный пользователем - Использовать встроенный список слов - Файл пользовательского списка слов - Нажмите чтобы выбрать файл пользовательского списка слов содержащий одно слово на строку + Тип генератора паролей + Пользовательский список слов + Использовать файл списка слов созданный пользователем + Использовать встроенный список слов + Файл пользовательского списка слов + Нажмите чтобы выбрать файл пользовательского списка слов содержащий одно слово на строку Пароль Сгенерировать @@ -200,7 +199,7 @@ Видимость поля дополнительной информации после расшифрования Сгенерировать Обновить список - Внешний репозиторий не выбран + Внешний репозиторий не выбран Поделиться паролем в открытом виде с помощью Иконка приложения @@ -251,10 +250,10 @@ Запрос биометрии Ошибка авторизации Ошибка аутентификации: %s - Включить биометрическую аутентификацию - Когда ключено, Password Store будет запрашивать ваш опечаток пальца при каждом запуске приложения - Сенсор отпечатка пальца не доступен или отсутствует - Очистить сохраненный SSH Key идентификатор OpenKystortore + Включить биометрическую аутентификацию + Когда ключено, Password Store будет запрашивать ваш опечаток пальца при каждом запуске приложения + Сенсор отпечатка пальца не доступен или отсутствует + Очистить сохраненный SSH Key идентификатор OpenKystortore Хранилище расположено на вашей SD-карте или во внутреннем хранилище, но у приложения нет разрешения на доступ к нему. Ваш публичный ключ Возникла ошибка при попытке генерации ssh ключа @@ -267,16 +266,15 @@ Путь должен указывать на область внутри репозитория Введите адрес назначения для %1$s Создать - Открыть поиск на старте - Открыть панель поиска при запуске приложения - Генератор паролей + Открыть поиск на старте + Открыть панель поиска при запуске приложения Нажмите здесь чтобы очистить буфер обмена Для синхронизации изменений клонируйте git репозиторий - Тема приложения - Светлая - Темная - Задается экономией батареи - Системная + Тема приложения + Светлая + Темная + Задается экономией батареи + Системная SSH ключ Пароль Конфигурация успешно сохранена diff --git a/app/src/main/res/values-v29/arrays.xml b/app/src/main/res/values-v29/arrays.xml index 97443b9f..13dfa87e 100644 --- a/app/src/main/res/values-v29/arrays.xml +++ b/app/src/main/res/values-v29/arrays.xml @@ -5,9 +5,9 @@ - @string/theme_light - @string/theme_dark - @string/theme_follow_system + @string/pref_app_theme_value_light + @string/pref_app_theme_value_dark + @string/pref_app_theme_value_follow_system light diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 2b320f31..a55e37b2 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -41,9 +41,9 @@ directory - @string/theme_light - @string/theme_dark - @string/theme_battery_saver + @string/pref_app_theme_value_light + @string/pref_app_theme_value_dark + @string/pref_app_theme_value_battery_saver light diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a370d230..46e85334 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -110,43 +110,43 @@ Last changed %s - Repository - Edit Git server settings + Repository + Edit Git server settings Local Git config & utilities - Import SSH key + Import SSH key Generate SSH key pair View generated public SSH key - Delete repository + Delete repository Clear repository General - Passwords + Passwords Password copy timeout Set the time (in seconds) you want the password to be in clipboard. 0 means forever. Current value: %1$s Automatically copy password - Automatically copy the password to the clipboard after decryption was successful. + Automatically copy the password to the clipboard after decryption was successful. Selected file does not appear to be an SSH private key. SSH-key imported Key import error Message : \n - Recursive filtering - Recursively find passwords of the current directory. + Recursive filtering + Recursively find passwords of the current directory. Password sort order Folders first Files first Type independent Recently used - Autofill + Autofill Enable Autofill - Misc + Misc Clear clipboard 20 times - Store consecutive numbers in the clipboard 20 times. Useful on Samsung phones that feature clipboard history. + Store consecutive numbers in the clipboard 20 times. Useful on Samsung phones that feature clipboard history. Deletes local (hidden) repository - External repository + External repository Use an external password repository - Select external repository + Select external repository + No external repository selected Export passwords Exports the encrypted passwords to an external directory - Version Generate Password @@ -171,12 +171,12 @@ Selected dictionary does not contain enough words of given length %1$d..%2$d - Password generator type - Custom wordlist - Using custom wordlist file - Using built-in wordlist - Custom worldlist file - Tap to pick a custom wordlist file containing one word per line + Password generator type + Custom wordlist + Using custom wordlist file + Using built-in wordlist + Custom worldlist file + Tap to pick a custom wordlist file containing one word per line Passphrase @@ -220,7 +220,6 @@ Control the visibility of the extra content once decrypted. Generate Refresh list - No external repository selected Send password as plaintext using… App icon @@ -271,14 +270,15 @@ Unable to locate HEAD SD-Card root selected You have selected the root of your sdcard for the store. This is extremely dangerous and you will lose your data as its content will, eventually, be deleted + Remove everything Abort and Push Biometric Prompt Authentication failure Authentication failure: %s - Enable biometric authentication - When enabled, Password Store will prompt you for your fingerprint when launching the app - Fingerprint hardware not accessible or missing - Clear remembered OpenKeystore SSH Key ID + Enable biometric authentication + When enabled, Password Store will prompt you for your fingerprint when launching the app + Fingerprint hardware not accessible or missing + Clear remembered OpenKeystore SSH Key ID The store location is in your SD Card or Internal storage, but the app does not have permission to access it. Your public key Error while trying to generate the ssh-key @@ -291,16 +291,15 @@ Destination must be within the repository Enter destination for %1$s Create - Open search on start - Open search bar when app is launched - Password Generator + Open search on start + Open search bar when app is launched Tap here to clear clipboard The repository must be cloned before syncing changes. - App theme - Light - Dark - Set by Battery Saver - System default + App theme + Light + Dark + Set by Battery Saver + System default SSH key Password OpenKeychain @@ -389,7 +388,7 @@ Proxy hostname Port - HTTP(S) proxy settings + HTTP(S) proxy settings Invalid URL Fill and save passwords (saving requires that no accessibility services are enabled) Clear saved host key diff --git a/app/src/main/res/xml/preference.xml b/app/src/main/res/xml/preference.xml deleted file mode 100644 index c0b3a105..00000000 --- a/app/src/main/res/xml/preference.xml +++ /dev/null @@ -1,174 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index 32abf6ab..8cffd7c2 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -53,6 +53,7 @@ object Dependencies { const val jgit = "org.eclipse.jgit:org.eclipse.jgit:3.7.1.201504261725-r" const val kotlin_result = "com.michael-bull.kotlin-result:kotlin-result:1.1.9" const val leakcanary = "com.squareup.leakcanary:leakcanary-android:2.5" + const val modern_android_prefs = "de.Maxr1998.android:modernpreferences:1.2.0-alpha1" const val plumber = "com.squareup.leakcanary:plumber-android:2.5" const val sshj = "com.hierynomus:sshj:0.30.0" const val ssh_auth = "org.sufficientlysecure:sshauthentication-api:1.0"