aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java
diff options
context:
space:
mode:
authorHarsh Shandilya <me@msfjarvis.dev>2021-01-12 11:19:28 +0530
committerGitHub <noreply@github.com>2021-01-12 11:19:28 +0530
commit8bd156dea6e87eb667f76f4738fde992323ce0cf (patch)
tree505b93720ff21cdb2d7a7144253a7d9b352d041d /app/src/main/java
parent91e00d897f190ec6ab5330f7b3f43d6d7f3e57fb (diff)
Rework settings to use ModernAndroidPreferences (#1236)
Co-authored-by: Fabian Henneke <fabian@hen.ne.ke>
Diffstat (limited to 'app/src/main/java')
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/ui/onboarding/fragments/RepoLocationFragment.kt12
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/ui/onboarding/fragments/WelcomeFragment.kt4
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/ui/passwords/PasswordStore.kt7
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/ui/settings/AutofillSettings.kt126
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/ui/settings/DirectorySelectionActivity.kt56
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/ui/settings/GeneralSettings.kt104
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/ui/settings/MiscSettings.kt76
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/ui/settings/PasswordSettings.kt119
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/ui/settings/RepositorySettings.kt198
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/ui/settings/SettingsActivity.kt93
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/ui/settings/SettingsProvider.kt19
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/ui/settings/UserPreference.kt674
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/ui/sshkeygen/SshKeyGenActivity.kt1
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/ui/sshkeygen/SshKeyImportActivity.kt61
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/util/autofill/AutofillPreferences.kt7
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/util/git/operation/GitOperation.kt11
16 files changed, 877 insertions, 691 deletions
diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/onboarding/fragments/RepoLocationFragment.kt b/app/src/main/java/dev/msfjarvis/aps/ui/onboarding/fragments/RepoLocationFragment.kt
index 08090d1b..37c23cea 100644
--- a/app/src/main/java/dev/msfjarvis/aps/ui/onboarding/fragments/RepoLocationFragment.kt
+++ b/app/src/main/java/dev/msfjarvis/aps/ui/onboarding/fragments/RepoLocationFragment.kt
@@ -19,9 +19,9 @@ 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.databinding.FragmentRepoLocationBinding
import dev.msfjarvis.aps.data.repo.PasswordRepository
+import dev.msfjarvis.aps.ui.settings.DirectorySelectionActivity
import dev.msfjarvis.aps.util.settings.PasswordSortOrder
import dev.msfjarvis.aps.util.settings.PreferenceKeys
import dev.msfjarvis.aps.util.extensions.finish
@@ -31,11 +31,13 @@ import dev.msfjarvis.aps.util.extensions.listFilesRecursively
import dev.msfjarvis.aps.util.extensions.performTransactionWithBackStack
import dev.msfjarvis.aps.util.extensions.sharedPrefs
import dev.msfjarvis.aps.util.extensions.viewBinding
+import android.content.Intent
import java.io.File
class RepoLocationFragment : Fragment(R.layout.fragment_repo_location) {
private val settings by lazy(LazyThreadSafetyMode.NONE) { requireActivity().applicationContext.sharedPrefs }
+ private val directorySelectIntent by lazy(LazyThreadSafetyMode.NONE) { Intent(requireContext(), DirectorySelectionActivity::class.java) }
private val binding by viewBinding(FragmentRepoLocationBinding::bind)
private val sortOrder: PasswordSortOrder
get() = PasswordSortOrder.getSortOrder(settings)
@@ -57,7 +59,7 @@ class RepoLocationFragment : Fragment(R.layout.fragment_repo_location) {
}
private val externalDirPermGrantedAction = createPermGrantedAction {
- externalDirectorySelectAction.launch(UserPreference.createDirectorySelectionIntent(requireContext()))
+ externalDirectorySelectAction.launch(directorySelectIntent)
}
private val repositoryUsePermGrantedAction = createPermGrantedAction {
@@ -65,7 +67,7 @@ class RepoLocationFragment : Fragment(R.layout.fragment_repo_location) {
}
private val repositoryChangePermGrantedAction = createPermGrantedAction {
- repositoryInitAction.launch(UserPreference.createDirectorySelectionIntent(requireContext()))
+ repositoryInitAction.launch(directorySelectIntent)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -102,7 +104,7 @@ class RepoLocationFragment : Fragment(R.layout.fragment_repo_location) {
} else {
// Unlikely we have storage permissions without user ever selecting a directory,
// but let's not assume.
- externalDirectorySelectAction.launch(UserPreference.createDirectorySelectionIntent(requireContext()))
+ externalDirectorySelectAction.launch(directorySelectIntent)
}
} else {
MaterialAlertDialogBuilder(requireActivity())
@@ -119,7 +121,7 @@ class RepoLocationFragment : Fragment(R.layout.fragment_repo_location) {
if (!isPermissionGranted(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
repositoryChangePermGrantedAction.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
} else {
- repositoryInitAction.launch(UserPreference.createDirectorySelectionIntent(requireContext()))
+ repositoryInitAction.launch(directorySelectIntent)
}
}
.show()
diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/onboarding/fragments/WelcomeFragment.kt b/app/src/main/java/dev/msfjarvis/aps/ui/onboarding/fragments/WelcomeFragment.kt
index 696aba17..34459208 100644
--- a/app/src/main/java/dev/msfjarvis/aps/ui/onboarding/fragments/WelcomeFragment.kt
+++ b/app/src/main/java/dev/msfjarvis/aps/ui/onboarding/fragments/WelcomeFragment.kt
@@ -11,8 +11,8 @@ import android.view.View
import androidx.annotation.Keep
import androidx.fragment.app.Fragment
import dev.msfjarvis.aps.R
-import dev.msfjarvis.aps.ui.settings.UserPreference
import dev.msfjarvis.aps.databinding.FragmentWelcomeBinding
+import dev.msfjarvis.aps.ui.settings.SettingsActivity
import dev.msfjarvis.aps.util.extensions.performTransactionWithBackStack
import dev.msfjarvis.aps.util.extensions.viewBinding
@@ -25,6 +25,6 @@ class WelcomeFragment : Fragment(R.layout.fragment_welcome) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.letsGo.setOnClickListener { parentFragmentManager.performTransactionWithBackStack(CloneFragment.newInstance()) }
- binding.settingsButton.setOnClickListener { startActivity(Intent(requireContext(), UserPreference::class.java)) }
+ binding.settingsButton.setOnClickListener { startActivity(Intent(requireContext(), SettingsActivity::class.java)) }
}
}
diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/passwords/PasswordStore.kt b/app/src/main/java/dev/msfjarvis/aps/ui/passwords/PasswordStore.kt
index 49620ac8..bdd8c9bb 100644
--- a/app/src/main/java/dev/msfjarvis/aps/ui/passwords/PasswordStore.kt
+++ b/app/src/main/java/dev/msfjarvis/aps/ui/passwords/PasswordStore.kt
@@ -42,7 +42,6 @@ import dev.msfjarvis.aps.ui.main.LaunchActivity
import dev.msfjarvis.aps.R
import dev.msfjarvis.aps.util.viewmodel.SearchableRepositoryViewModel
import dev.msfjarvis.aps.ui.folderselect.SelectFolderActivity
-import dev.msfjarvis.aps.ui.settings.UserPreference
import dev.msfjarvis.aps.util.autofill.AutofillMatcher
import dev.msfjarvis.aps.ui.crypto.BasePgpActivity.Companion.getLongName
import dev.msfjarvis.aps.ui.crypto.DecryptActivity
@@ -55,6 +54,8 @@ import dev.msfjarvis.aps.ui.dialogs.FolderCreationDialogFragment
import dev.msfjarvis.aps.ui.onboarding.activity.OnboardingActivity
import dev.msfjarvis.aps.data.password.PasswordItem
import dev.msfjarvis.aps.data.repo.PasswordRepository
+import dev.msfjarvis.aps.ui.settings.DirectorySelectionActivity
+import dev.msfjarvis.aps.ui.settings.SettingsActivity
import dev.msfjarvis.aps.util.settings.PreferenceKeys
import dev.msfjarvis.aps.util.extensions.base64
import dev.msfjarvis.aps.util.extensions.commitChange
@@ -300,7 +301,7 @@ class PasswordStore : BaseGitActivity() {
when (id) {
R.id.user_pref -> {
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<AppCompatTextView>(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<ShortcutManager>()?.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 <T : FragmentActivity> launchActivity(clazz: Class<T>) {
+ 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<ShortcutManager>()?.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<PreferencesAdapter.SavedState>("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<Preference>(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<Preference>
- 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<Preference>(PreferenceKeys.GIT_SERVER_INFO)
- val openkeystoreIdPreference = findPreference<Preference>(PreferenceKeys.SSH_OPENKEYSTORE_CLEAR_KEY_ID)
- val gitConfigPreference = findPreference<Preference>(PreferenceKeys.GIT_CONFIG)
- val sshKeyPreference = findPreference<Preference>(PreferenceKeys.SSH_KEY)
- val sshKeygenPreference = findPreference<Preference>(PreferenceKeys.SSH_KEYGEN)
- viewSshKeyPreference = findPreference(PreferenceKeys.SSH_SEE_KEY)
- clearSavedPassPreference = findPreference(PreferenceKeys.CLEAR_SAVED_PASS)
- val deleteRepoPreference = findPreference<Preference>(PreferenceKeys.GIT_DELETE_REPO)
- val externalGitRepositoryPreference = findPreference<Preference>(PreferenceKeys.GIT_EXTERNAL)
- val selectExternalGitRepositoryPreference = findPreference<Preference>(PreferenceKeys.PREF_SELECT_EXTERNAL)
-
- if (!PasswordRepository.isGitRepo()) {
- listOfNotNull(
- gitServerPreference,
- gitConfigPreference,
- sshKeyPreference,
- viewSshKeyPreference,
- clearSavedPassPreference,
- ).forEach {
- it.parent?.removePreference(it)
- }
- }
-
- // General preferences
- val showTimePreference = findPreference<Preference>(PreferenceKeys.GENERAL_SHOW_TIME)
- val clearClipboard20xPreference = findPreference<CheckBoxPreference>(PreferenceKeys.CLEAR_CLIPBOARD_20X)
-
- // Autofill preferences
- autoFillEnablePreference = findPreference(PreferenceKeys.AUTOFILL_ENABLE)
- val oreoAutofillDirectoryStructurePreference = findPreference<ListPreference>(PreferenceKeys.OREO_AUTOFILL_DIRECTORY_STRUCTURE)
- val oreoAutofillDefaultUsername = findPreference<EditTextPreference>(PreferenceKeys.OREO_AUTOFILL_DEFAULT_USERNAME)
- val oreoAutofillCustomPublixSuffixes = findPreference<EditTextPreference>(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<Preference>(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<ShortcutManager>()?.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<Preference>(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<Preference> {
- getString(R.string.pref_clipboard_timeout_summary, sharedPreferences.getString
- (PreferenceKeys.GENERAL_SHOW_TIME, "45"))
- }
-
- findPreference<CheckBoxPreference>(PreferenceKeys.ENABLE_DEBUG_LOGGING)?.isVisible = !BuildConfig.ENABLE_DEBUG_FEATURES
-
- findPreference<CheckBoxPreference>(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<ShortcutManager>()?.apply {
- removeDynamicShortcuts(dynamicShortcuts.map { it.id }.toMutableList())
- }
- }
- }
- true
- }
- }
- }
-
- findPreference<Preference>(PreferenceKeys.PROXY_SETTINGS)?.onPreferenceClickListener = ClickListener {
- startActivity(Intent(requireContext(), ProxySelectorActivity::class.java))
- true
- }
-
- val prefCustomXkpwdDictionary = findPreference<Preference>(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<CheckBoxPreference>(PreferenceKeys.PREF_KEY_IS_CUSTOM_DICT)
- val prefCustomDictPicker = findPreference<Preference>(PreferenceKeys.PREF_KEY_CUSTOM_DICT)
- val prefPwgenType = findPreference<ListPreference>(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<AppCompatTextView>(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)