aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/main/java
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-11-03 14:18:42 +0100
committerDanilo M. <danix@danix.xyz>2026-11-03 14:18:42 +0100
commit2911783653f699e41a07bb9536460d9d0a1c3b70 (patch)
treeb68692cc80bc0b463f671779ac3642c2ad8976cc /app/src/main/java
parentb4e1002735486bf56b67b8233c2c2abfa875eed4 (diff)
downloadHardenedPass-2911783653f699e41a07bb9536460d9d0a1c3b70.tar.gz
HardenedPass-2911783653f699e41a07bb9536460d9d0a1c3b70.zip
refactor(app): remove the legacy setup screens
The Compose welcome, git server, SSH key, key generation and PGP keys screens replace OnboardingActivity with its fragments, GitServerConfigActivity with BasicBottomSheet, SshKeyImportActivity, SshKeyGenActivity, ShowSshKeyFragment, PGPKeyImportActivity and PGPKeyListActivity with KeyList and PGPKeyListViewModel, their layouts and manifest entries, and ui:compose's APSAppBar. Resources only they used are removed too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'app/src/main/java')
-rw-r--r--app/src/main/java/app/passwordstore/ui/dialogs/BasicBottomSheet.kt178
-rw-r--r--app/src/main/java/app/passwordstore/ui/git/config/GitServerConfigActivity.kt304
-rw-r--r--app/src/main/java/app/passwordstore/ui/onboarding/activity/OnboardingActivity.kt27
-rw-r--r--app/src/main/java/app/passwordstore/ui/onboarding/fragments/CloneFragment.kt78
-rw-r--r--app/src/main/java/app/passwordstore/ui/onboarding/fragments/KeySelectionFragment.kt77
-rw-r--r--app/src/main/java/app/passwordstore/ui/onboarding/fragments/WelcomeFragment.kt33
-rw-r--r--app/src/main/java/app/passwordstore/ui/pgp/PGPKeyImportActivity.kt109
-rw-r--r--app/src/main/java/app/passwordstore/ui/pgp/PGPKeyList.kt169
-rw-r--r--app/src/main/java/app/passwordstore/ui/pgp/PGPKeyListActivity.kt94
-rw-r--r--app/src/main/java/app/passwordstore/ui/sshkeygen/ShowSshKeyFragment.kt39
-rw-r--r--app/src/main/java/app/passwordstore/ui/sshkeygen/SshKeyGenActivity.kt173
-rw-r--r--app/src/main/java/app/passwordstore/ui/sshkeygen/SshKeyImportActivity.kt66
-rw-r--r--app/src/main/java/app/passwordstore/util/viewmodel/PGPKeyListViewModel.kt48
13 files changed, 0 insertions, 1395 deletions
diff --git a/app/src/main/java/app/passwordstore/ui/dialogs/BasicBottomSheet.kt b/app/src/main/java/app/passwordstore/ui/dialogs/BasicBottomSheet.kt
deleted file mode 100644
index e689f3bd..00000000
--- a/app/src/main/java/app/passwordstore/ui/dialogs/BasicBottomSheet.kt
+++ /dev/null
@@ -1,178 +0,0 @@
-/*
- * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-package app.passwordstore.ui.dialogs
-
-import android.content.Context
-import android.content.DialogInterface.OnDismissListener
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.view.ViewTreeObserver
-import android.widget.FrameLayout
-import androidx.annotation.StringRes
-import androidx.core.view.isVisible
-import app.passwordstore.R
-import app.passwordstore.databinding.BasicBottomSheetBinding
-import app.passwordstore.util.extensions.viewBinding
-import com.google.android.material.bottomsheet.BottomSheetBehavior
-import com.google.android.material.bottomsheet.BottomSheetDialog
-import com.google.android.material.bottomsheet.BottomSheetDialogFragment
-
-/**
- * [BottomSheetDialogFragment] that exposes a simple [androidx.appcompat.app.AlertDialog] like API
- * through [BasicBottomSheet.Builder] to create a similar UI, just at the bottom of the screen.
- */
-class BasicBottomSheet
-private constructor(
- val title: String?,
- val message: String,
- val positiveButtonLabel: String?,
- val negativeButtonLabel: String?,
- val positiveButtonClickListener: View.OnClickListener?,
- val negativeButtonClickListener: View.OnClickListener?,
- val onDismissListener: OnDismissListener?,
-) : BottomSheetDialogFragment() {
-
- private val binding by viewBinding(BasicBottomSheetBinding::bind)
-
- private var behavior: BottomSheetBehavior<FrameLayout>? = null
- private val bottomSheetCallback =
- object : BottomSheetBehavior.BottomSheetCallback() {
- override fun onSlide(bottomSheet: View, slideOffset: Float) {}
-
- override fun onStateChanged(bottomSheet: View, newState: Int) {
- if (newState == BottomSheetBehavior.STATE_COLLAPSED) {
- dismiss()
- }
- }
- }
-
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?,
- ): View? {
- if (savedInstanceState != null) dismiss()
- return layoutInflater.inflate(R.layout.basic_bottom_sheet, container, false)
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- view.viewTreeObserver.addOnGlobalLayoutListener(
- object : ViewTreeObserver.OnGlobalLayoutListener {
- override fun onGlobalLayout() {
- view.viewTreeObserver.removeOnGlobalLayoutListener(this)
- val dialog = dialog as BottomSheetDialog? ?: return
- behavior = dialog.behavior
- behavior?.apply {
- state = BottomSheetBehavior.STATE_EXPANDED
- peekHeight = 0
- addBottomSheetCallback(bottomSheetCallback)
- }
- if (!title.isNullOrEmpty()) {
- binding.bottomSheetTitle.isVisible = true
- binding.bottomSheetTitle.text = title
- }
- binding.bottomSheetMessage.text = message
- if (positiveButtonClickListener != null) {
- positiveButtonLabel?.let { buttonLbl -> binding.bottomSheetOkButton.text = buttonLbl }
- binding.bottomSheetOkButton.isVisible = true
- binding.bottomSheetOkButton.setOnClickListener {
- positiveButtonClickListener.onClick(it)
- dismiss()
- }
- }
- if (negativeButtonClickListener != null) {
- binding.bottomSheetCancelButton.isVisible = true
- negativeButtonLabel?.let { buttonLbl ->
- binding.bottomSheetCancelButton.text = buttonLbl
- }
- binding.bottomSheetCancelButton.setOnClickListener {
- negativeButtonClickListener.onClick(it)
- dismiss()
- }
- }
- if (onDismissListener != null) {
- dialog.setOnDismissListener(onDismissListener)
- }
- }
- }
- )
- }
-
- override fun dismiss() {
- super.dismiss()
- behavior?.removeBottomSheetCallback(bottomSheetCallback)
- }
-
- class Builder(val context: Context) {
-
- private var title: String? = null
- private var message: String? = null
- private var positiveButtonLabel: String? = null
- private var negativeButtonLabel: String? = null
- private var positiveButtonClickListener: View.OnClickListener? = null
- private var negativeButtonClickListener: View.OnClickListener? = null
- private var onDismissListener: OnDismissListener? = null
-
- fun setTitleRes(@StringRes titleRes: Int): Builder {
- this.title = context.resources.getString(titleRes)
- return this
- }
-
- fun setTitle(title: String): Builder {
- this.title = title
- return this
- }
-
- fun setMessageRes(@StringRes messageRes: Int): Builder {
- this.message = context.resources.getString(messageRes)
- return this
- }
-
- fun setMessage(message: String): Builder {
- this.message = message
- return this
- }
-
- fun setPositiveButtonClickListener(
- buttonLabel: String? = null,
- listener: View.OnClickListener,
- ): Builder {
- this.positiveButtonClickListener = listener
- this.positiveButtonLabel = buttonLabel
- return this
- }
-
- fun setNegativeButtonClickListener(
- buttonLabel: String? = null,
- listener: View.OnClickListener,
- ): Builder {
- this.negativeButtonClickListener = listener
- this.negativeButtonLabel = buttonLabel
- return this
- }
-
- fun setOnDismissListener(onDismissListener: OnDismissListener): Builder {
- this.onDismissListener = onDismissListener
- return this
- }
-
- fun build(): BasicBottomSheet {
- require(message != null) { "Message needs to be set" }
- return BasicBottomSheet(
- title,
- message!!,
- positiveButtonLabel,
- negativeButtonLabel,
- positiveButtonClickListener,
- negativeButtonClickListener,
- onDismissListener,
- )
- }
- }
-}
diff --git a/app/src/main/java/app/passwordstore/ui/git/config/GitServerConfigActivity.kt b/app/src/main/java/app/passwordstore/ui/git/config/GitServerConfigActivity.kt
deleted file mode 100644
index 2a1d3ebd..00000000
--- a/app/src/main/java/app/passwordstore/ui/git/config/GitServerConfigActivity.kt
+++ /dev/null
@@ -1,304 +0,0 @@
-/*
- * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-package app.passwordstore.ui.git.config
-
-import android.content.Context
-import android.content.Intent
-import android.os.Bundle
-import android.os.Handler
-import android.os.Looper
-import android.view.MenuItem
-import android.view.View
-import androidx.core.os.postDelayed
-import androidx.core.view.isVisible
-import androidx.core.widget.doOnTextChanged
-import androidx.lifecycle.lifecycleScope
-import app.passwordstore.R
-import app.passwordstore.data.repo.PasswordRepository
-import app.passwordstore.databinding.ActivityGitCloneBinding
-import app.passwordstore.ui.dialogs.BasicBottomSheet
-import app.passwordstore.ui.git.base.BaseGitActivity
-import app.passwordstore.util.extensions.snackbar
-import app.passwordstore.util.extensions.viewBinding
-import app.passwordstore.util.settings.AuthMode
-import app.passwordstore.util.settings.GitSettings
-import app.passwordstore.util.settings.Protocol
-import com.github.michaelbull.result.fold
-import com.github.michaelbull.result.onFailure
-import com.github.michaelbull.result.runCatching
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import com.google.android.material.snackbar.Snackbar
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import logcat.LogPriority.ERROR
-import logcat.asLog
-import logcat.logcat
-
-/**
- * Activity that encompasses both the initial clone as well as editing the server config for future
- * changes.
- */
-class GitServerConfigActivity : BaseGitActivity() {
-
- private val binding by viewBinding(ActivityGitCloneBinding::inflate)
-
- private lateinit var newAuthMode: AuthMode
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- val isClone = intent?.extras?.getBoolean("cloning") ?: false
- if (isClone) {
- binding.saveButton.text = getString(R.string.clone_button)
- }
- setContentView(binding.root)
- supportActionBar?.setDisplayHomeAsUpEnabled(true)
-
- newAuthMode = gitSettings.authMode
-
- binding.authModeGroup.apply {
- when (newAuthMode) {
- AuthMode.SshKey -> check(binding.authModeSshKey.id)
- AuthMode.Password -> check(binding.authModePassword.id)
- AuthMode.None -> clearChecked()
- }
- addOnButtonCheckedListener { _, _, _ ->
- if (checkedButtonIds.isEmpty()) {
- newAuthMode = AuthMode.None
- } else {
- when (checkedButtonId) {
- binding.authModeSshKey.id -> newAuthMode = AuthMode.SshKey
- binding.authModePassword.id -> newAuthMode = AuthMode.Password
- View.NO_ID -> newAuthMode = AuthMode.None
- }
- }
- }
- }
-
- binding.serverUrl.setText(
- gitSettings.url.also {
- if (it.isNullOrEmpty()) return@also
- setAuthModes(it.startsWith("http://") || it.startsWith("https://"))
- }
- )
-
- binding.serverUrl.doOnTextChanged { text, _, _, _ ->
- if (text.isNullOrEmpty()) return@doOnTextChanged
- setAuthModes(text.startsWith("http://") || text.startsWith("https://"))
- }
-
- binding.clearHostKeyButton.isVisible = gitSettings.hasSavedHostKey()
- binding.clearHostKeyButton.setOnClickListener {
- gitSettings.clearSavedHostKey()
- Snackbar.make(
- binding.root,
- getString(R.string.clear_saved_host_key_success),
- Snackbar.LENGTH_LONG,
- )
- .show()
- it.isVisible = false
- }
- binding.saveButton.setOnClickListener {
- val newUrl = binding.serverUrl.text.toString().trim()
- // If url is of type john_doe@example.org:12435/path/to/repo, then not adding `ssh://`
- // in the beginning will cause the port to be seen as part of the path. Let users know
- // about it and offer a quickfix.
- if (newUrl.contains(PORT_REGEX)) {
- if (newUrl.startsWith("https://")) {
- BasicBottomSheet.Builder(this)
- .setTitleRes(R.string.https_scheme_with_port_title)
- .setMessageRes(R.string.https_scheme_with_port_message)
- .setPositiveButtonClickListener {
- binding.serverUrl.setText(newUrl.replace(PORT_REGEX, "/"))
- }
- .build()
- .show(supportFragmentManager, "SSH_SCHEME_WARNING")
- return@setOnClickListener
- } else if (!newUrl.startsWith("ssh://")) {
- BasicBottomSheet.Builder(this)
- .setTitleRes(R.string.ssh_scheme_needed_title)
- .setMessageRes(R.string.ssh_scheme_needed_message)
- .setPositiveButtonClickListener {
- @Suppress("SetTextI18n") binding.serverUrl.setText("ssh://$newUrl")
- }
- .build()
- .show(supportFragmentManager, "SSH_SCHEME_WARNING")
- return@setOnClickListener
- }
- }
- if (newUrl.startsWith("git://")) {
- BasicBottomSheet.Builder(this)
- .setTitleRes(R.string.git_scheme_disallowed_title)
- .setMessageRes(R.string.git_scheme_disallowed_message)
- .setPositiveButtonClickListener {}
- .build()
- .show(supportFragmentManager, "SSH_SCHEME_WARNING")
- return@setOnClickListener
- }
- when (
- val updateResult =
- gitSettings.updateConnectionSettingsIfValid(
- newAuthMode = newAuthMode,
- newUrl = binding.serverUrl.text.toString().trim(),
- )
- ) {
- GitSettings.UpdateConnectionSettingsResult.FailedToParseUrl -> {
- Snackbar.make(
- binding.root,
- getString(R.string.git_server_config_save_error),
- Snackbar.LENGTH_LONG,
- )
- .show()
- }
- is GitSettings.UpdateConnectionSettingsResult.MissingUsername -> {
- when (updateResult.newProtocol) {
- Protocol.Https ->
- BasicBottomSheet.Builder(this)
- .setTitleRes(R.string.ssh_scheme_needed_title)
- .setMessageRes(R.string.git_server_config_save_missing_username_https)
- .setPositiveButtonClickListener {}
- .build()
- .show(supportFragmentManager, "HTTPS_MISSING_USERNAME")
- Protocol.Ssh ->
- BasicBottomSheet.Builder(this)
- .setTitleRes(R.string.ssh_scheme_needed_title)
- .setMessageRes(R.string.git_server_config_save_missing_username_ssh)
- .setPositiveButtonClickListener {}
- .build()
- .show(supportFragmentManager, "SSH_MISSING_USERNAME")
- }
- }
- GitSettings.UpdateConnectionSettingsResult.Valid -> {
- if (isClone && PasswordRepository.repository == null) PasswordRepository.initialize()
- if (!isClone) {
- Snackbar.make(
- binding.root,
- getString(R.string.git_server_config_save_success),
- Snackbar.LENGTH_SHORT,
- )
- .show()
- Handler(Looper.getMainLooper()).postDelayed(500) { finish() }
- } else {
- cloneRepository()
- }
- }
- is GitSettings.UpdateConnectionSettingsResult.AuthModeMismatch -> {
- val message =
- getString(
- R.string.git_server_config_save_auth_mode_mismatch,
- updateResult.newProtocol,
- updateResult.validModes.joinToString(", "),
- )
- Snackbar.make(binding.root, message, Snackbar.LENGTH_LONG).show()
- }
- }
- }
- }
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- return when (item.itemId) {
- android.R.id.home -> {
- onBackPressedDispatcher.onBackPressed()
- true
- }
- else -> super.onOptionsItemSelected(item)
- }
- }
-
- private fun setAuthModes(isHttps: Boolean) =
- with(binding) {
- if (isHttps) {
- authModeSshKey.isVisible = false
- authModePassword.isVisible = true
- if (authModeGroup.checkedButtonId != authModePassword.id) authModeGroup.clearChecked()
- } else {
- authModeSshKey.isVisible = true
- authModePassword.isVisible = true
- if (authModeGroup.checkedButtonId == View.NO_ID) authModeGroup.check(authModeSshKey.id)
- }
- }
-
- /** Clones the repository, the directory exists, deletes it */
- private fun cloneRepository() {
- val localDir =
- requireNotNull(PasswordRepository.getRepositoryDirectory()) {
- "Repository directory must be set before cloning"
- }
- val localDirFiles = localDir.listFiles() ?: emptyArray()
- // Warn if non-empty folder unless it's a just-initialized store that has just a .git folder
- if (
- localDir.exists() &&
- localDirFiles.isNotEmpty() &&
- !(localDirFiles.size == 1 && localDirFiles[0].name == ".git")
- ) {
- MaterialAlertDialogBuilder(this)
- .setTitle(R.string.dialog_delete_title)
- .setMessage(resources.getString(R.string.dialog_delete_msg, localDir.toString()))
- .setCancelable(false)
- .setPositiveButton(R.string.dialog_delete) { dialog, _ ->
- runCatching {
- lifecycleScope.launch {
- val snackbar =
- snackbar(
- message = getString(R.string.delete_directory_progress_text),
- length = Snackbar.LENGTH_INDEFINITE,
- )
- withContext(dispatcherProvider.io()) {
- localDir.deleteRecursively()
- localDir.mkdirs()
- }
- snackbar.dismiss()
- launchGitOperation(GitOp.CLONE)
- .fold(
- success = {
- setResult(RESULT_OK)
- finish()
- },
- failure = { err -> promptOnErrorHandler(err) { finish() } },
- )
- }
- }
- .onFailure { e ->
- e.printStackTrace()
- MaterialAlertDialogBuilder(this).setMessage(e.message).show()
- }
- dialog.cancel()
- }
- .setNegativeButton(R.string.dialog_do_not_delete) { dialog, _ -> dialog.cancel() }
- .show()
- } else {
- runCatching {
- // Silently delete & replace the lone .git folder if it exists
- if (localDir.exists() && localDirFiles.size == 1 && localDirFiles[0].name == ".git") {
- localDir.deleteRecursively()
- }
- }
- .onFailure { e ->
- logcat(ERROR) { e.asLog() }
- MaterialAlertDialogBuilder(this).setMessage(e.message).show()
- }
- lifecycleScope.launch {
- launchGitOperation(GitOp.CLONE)
- .fold(
- success = {
- setResult(RESULT_OK)
- finish()
- },
- failure = { promptOnErrorHandler(it) },
- )
- }
- }
- }
-
- companion object {
-
- private val PORT_REGEX = ":[0-9]{1,5}/".toRegex()
-
- fun createCloneIntent(context: Context): Intent {
- return Intent(context, GitServerConfigActivity::class.java).apply {
- putExtra("cloning", true)
- }
- }
- }
-}
diff --git a/app/src/main/java/app/passwordstore/ui/onboarding/activity/OnboardingActivity.kt b/app/src/main/java/app/passwordstore/ui/onboarding/activity/OnboardingActivity.kt
deleted file mode 100644
index 709c2302..00000000
--- a/app/src/main/java/app/passwordstore/ui/onboarding/activity/OnboardingActivity.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-package app.passwordstore.ui.onboarding.activity
-
-import android.os.Bundle
-import androidx.activity.addCallback
-import androidx.appcompat.app.AppCompatActivity
-import app.passwordstore.R
-import dagger.hilt.android.AndroidEntryPoint
-
-@AndroidEntryPoint
-class OnboardingActivity : AppCompatActivity(R.layout.activity_onboarding) {
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- supportActionBar?.hide()
- val callback = onBackPressedDispatcher.addCallback(enabled = false) { finishAffinity() }
- supportFragmentManager.addOnBackStackChangedListener {
- if (supportFragmentManager.backStackEntryCount == 0) {
- callback.isEnabled = true
- }
- }
- }
-}
diff --git a/app/src/main/java/app/passwordstore/ui/onboarding/fragments/CloneFragment.kt b/app/src/main/java/app/passwordstore/ui/onboarding/fragments/CloneFragment.kt
deleted file mode 100644
index a9d933b6..00000000
--- a/app/src/main/java/app/passwordstore/ui/onboarding/fragments/CloneFragment.kt
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-package app.passwordstore.ui.onboarding.fragments
-
-import android.os.Bundle
-import android.view.View
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.appcompat.app.AppCompatActivity
-import androidx.core.content.edit
-import androidx.fragment.app.Fragment
-import app.passwordstore.R
-import app.passwordstore.data.repo.PasswordRepository
-import app.passwordstore.databinding.FragmentCloneBinding
-import app.passwordstore.ui.git.config.GitServerConfigActivity
-import app.passwordstore.util.extensions.finish
-import app.passwordstore.util.extensions.performTransactionWithBackStack
-import app.passwordstore.util.extensions.sharedPrefs
-import app.passwordstore.util.extensions.unsafeLazy
-import app.passwordstore.util.extensions.viewBinding
-import app.passwordstore.util.settings.PreferenceKeys
-import com.github.michaelbull.result.onFailure
-import com.github.michaelbull.result.runCatching
-import logcat.LogPriority.ERROR
-import logcat.asLog
-import logcat.logcat
-
-class CloneFragment : Fragment(R.layout.fragment_clone) {
-
- private val binding by viewBinding(FragmentCloneBinding::bind)
-
- private val settings by unsafeLazy { requireActivity().applicationContext.sharedPrefs }
-
- private val cloneAction =
- registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
- if (result.resultCode == AppCompatActivity.RESULT_OK) {
- settings.edit { putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, true) }
- finish()
- }
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- binding.cloneRemote.setOnClickListener { cloneToHiddenDir() }
- binding.createLocal.setOnClickListener { createRepository() }
- }
-
- /** Clones a remote Git repository to the app's private directory */
- private fun cloneToHiddenDir() {
- cloneAction.launch(GitServerConfigActivity.createCloneIntent(requireContext()))
- }
-
- private fun createRepository() {
- val localDir = PasswordRepository.getRepositoryDirectory()
- runCatching {
- check(localDir.exists() || localDir.mkdir()) { "Failed to create directory!" }
- PasswordRepository.createRepository(localDir)
- if (!PasswordRepository.isInitialized) {
- PasswordRepository.initialize()
- }
- parentFragmentManager.performTransactionWithBackStack(KeySelectionFragment.newInstance())
- }
- .onFailure { e ->
- logcat(ERROR) { e.asLog() }
- if (!localDir.delete()) {
- logcat { "Failed to delete local repository: $localDir" }
- }
- finish()
- }
- }
-
- companion object {
-
- fun newInstance(): CloneFragment = CloneFragment()
- }
-}
diff --git a/app/src/main/java/app/passwordstore/ui/onboarding/fragments/KeySelectionFragment.kt b/app/src/main/java/app/passwordstore/ui/onboarding/fragments/KeySelectionFragment.kt
deleted file mode 100644
index 64c2e124..00000000
--- a/app/src/main/java/app/passwordstore/ui/onboarding/fragments/KeySelectionFragment.kt
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-package app.passwordstore.ui.onboarding.fragments
-
-import android.content.SharedPreferences
-import android.os.Bundle
-import android.view.View
-import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
-import androidx.appcompat.app.AppCompatActivity
-import androidx.core.content.edit
-import androidx.fragment.app.Fragment
-import androidx.lifecycle.lifecycleScope
-import app.passwordstore.R
-import app.passwordstore.data.repo.PasswordRepository
-import app.passwordstore.databinding.FragmentKeySelectionBinding
-import app.passwordstore.injection.prefs.SettingsPreferences
-import app.passwordstore.ui.pgp.PGPKeyListActivity
-import app.passwordstore.util.coroutines.DispatcherProvider
-import app.passwordstore.util.extensions.commitChange
-import app.passwordstore.util.extensions.finish
-import app.passwordstore.util.extensions.snackbar
-import app.passwordstore.util.extensions.viewBinding
-import app.passwordstore.util.settings.PreferenceKeys
-import com.google.android.material.snackbar.Snackbar
-import dagger.hilt.android.AndroidEntryPoint
-import java.io.File
-import javax.inject.Inject
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-
-@AndroidEntryPoint
-class KeySelectionFragment : Fragment(R.layout.fragment_key_selection) {
-
- @Inject @SettingsPreferences lateinit var settings: SharedPreferences
- @Inject lateinit var dispatcherProvider: DispatcherProvider
- private val binding by viewBinding(FragmentKeySelectionBinding::bind)
- private val gpgKeySelectAction =
- registerForActivityResult(StartActivityForResult()) { result ->
- if (result.resultCode == AppCompatActivity.RESULT_OK) {
- val data = result.data ?: return@registerForActivityResult
- val selectedKey =
- data.getStringExtra(PGPKeyListActivity.EXTRA_SELECTED_KEY)
- ?: return@registerForActivityResult
- lifecycleScope.launch {
- withContext(dispatcherProvider.io()) {
- val gpgIdentifierFile = File(PasswordRepository.getRepositoryDirectory(), ".gpg-id")
- gpgIdentifierFile.writeText(selectedKey)
- }
- settings.edit { putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, true) }
- requireActivity()
- .commitChange(getString(R.string.git_commit_gpg_id, getString(R.string.app_name)))
- finish()
- }
- } else {
- requireActivity()
- .snackbar(
- message = getString(R.string.gpg_key_select_mandatory),
- length = Snackbar.LENGTH_LONG,
- )
- }
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- binding.selectKey.setOnClickListener {
- gpgKeySelectAction.launch(PGPKeyListActivity.newSelectionActivity(requireContext()))
- }
- }
-
- companion object {
-
- fun newInstance() = KeySelectionFragment()
- }
-}
diff --git a/app/src/main/java/app/passwordstore/ui/onboarding/fragments/WelcomeFragment.kt b/app/src/main/java/app/passwordstore/ui/onboarding/fragments/WelcomeFragment.kt
deleted file mode 100644
index 44881c64..00000000
--- a/app/src/main/java/app/passwordstore/ui/onboarding/fragments/WelcomeFragment.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-package app.passwordstore.ui.onboarding.fragments
-
-import android.os.Bundle
-import android.view.View
-import androidx.annotation.Keep
-import androidx.fragment.app.Fragment
-import app.passwordstore.R
-import app.passwordstore.databinding.FragmentWelcomeBinding
-import app.passwordstore.ui.settings.SettingsActivity
-import app.passwordstore.util.extensions.launchActivity
-import app.passwordstore.util.extensions.performTransactionWithBackStack
-import app.passwordstore.util.extensions.viewBinding
-
-@Keep
-class WelcomeFragment : Fragment(R.layout.fragment_welcome) {
-
- private val binding by viewBinding(FragmentWelcomeBinding::bind)
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- binding.letsGo.setOnClickListener {
- parentFragmentManager.performTransactionWithBackStack(CloneFragment.newInstance())
- }
- binding.settingsButton.setOnClickListener {
- requireActivity().launchActivity(SettingsActivity::class.java)
- }
- }
-}
diff --git a/app/src/main/java/app/passwordstore/ui/pgp/PGPKeyImportActivity.kt b/app/src/main/java/app/passwordstore/ui/pgp/PGPKeyImportActivity.kt
deleted file mode 100644
index d276c552..00000000
--- a/app/src/main/java/app/passwordstore/ui/pgp/PGPKeyImportActivity.kt
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-@file:Suppress("BlockingMethodInNonBlockingContext")
-
-package app.passwordstore.ui.pgp
-
-import android.os.Bundle
-import androidx.activity.result.contract.ActivityResultContracts.OpenDocument
-import androidx.appcompat.app.AppCompatActivity
-import app.passwordstore.R
-import app.passwordstore.crypto.KeyUtils.tryGetId
-import app.passwordstore.crypto.PGPKey
-import app.passwordstore.crypto.PGPKeyManager
-import app.passwordstore.crypto.errors.KeyAlreadyExistsException
-import com.github.michaelbull.result.Result
-import com.github.michaelbull.result.runCatching
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import dagger.hilt.android.AndroidEntryPoint
-import javax.inject.Inject
-import kotlinx.coroutines.runBlocking
-
-@AndroidEntryPoint
-class PGPKeyImportActivity : AppCompatActivity() {
-
- /**
- * A [ByteArray] containing the contents of the previously selected file. This is necessary for
- * the replacement case where we do not want users to have to pick the file again.
- */
- private var lastBytes: ByteArray? = null
- @Inject lateinit var keyManager: PGPKeyManager
-
- private val pgpKeyImportAction =
- registerForActivityResult(OpenDocument()) { uri ->
- runCatching {
- if (uri == null) {
- return@runCatching null
- }
- val keyInputStream =
- contentResolver.openInputStream(uri)
- ?: throw IllegalStateException("Failed to open selected file")
- val bytes = keyInputStream.use { `is` -> `is`.readBytes() }
- importKey(bytes, false)
- }
- .run(::handleImportResult)
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- pgpKeyImportAction.launch(arrayOf("*/*"))
- }
-
- override fun onDestroy() {
- lastBytes = null
- super.onDestroy()
- }
-
- private fun importKey(bytes: ByteArray, replace: Boolean): PGPKey? {
- lastBytes = bytes
- val (key, error) = runBlocking { keyManager.addKey(PGPKey(bytes), replace = replace) }
- if (replace) {
- lastBytes = null
- }
- if (error != null) throw error
- return key
- }
-
- private fun handleImportResult(result: Result<PGPKey?, Throwable>) {
- if (result.isOk) {
- val key = result.value
- if (key == null) {
- setResult(RESULT_CANCELED)
- finish()
- // This return convinces Kotlin that the control flow for `key == null` definitely
- // terminates here and allows for a smart cast below.
- return
- }
- MaterialAlertDialogBuilder(this)
- .setTitle(getString(R.string.pgp_key_import_succeeded))
- .setMessage(getString(R.string.pgp_key_import_succeeded_message, tryGetId(key)))
- .setPositiveButton(android.R.string.ok) { _, _ ->
- setResult(RESULT_OK)
- finish()
- }
- .setCancelable(false)
- .show()
- } else {
- if (result.error is KeyAlreadyExistsException && lastBytes != null) {
- MaterialAlertDialogBuilder(this)
- .setTitle(getString(R.string.pgp_key_import_failed))
- .setMessage(getString(R.string.pgp_key_import_failed_replace_message))
- .setPositiveButton(R.string.dialog_yes) { _, _ ->
- handleImportResult(runCatching { importKey(lastBytes!!, replace = true) })
- }
- .setNegativeButton(R.string.dialog_no) { _, _ -> finish() }
- .setCancelable(false)
- .show()
- } else {
- MaterialAlertDialogBuilder(this)
- .setTitle(getString(R.string.pgp_key_import_failed))
- .setMessage(result.error.message)
- .setPositiveButton(android.R.string.ok) { _, _ -> finish() }
- .setCancelable(false)
- .show()
- }
- }
- }
-}
diff --git a/app/src/main/java/app/passwordstore/ui/pgp/PGPKeyList.kt b/app/src/main/java/app/passwordstore/ui/pgp/PGPKeyList.kt
deleted file mode 100644
index 312f5fb0..00000000
--- a/app/src/main/java/app/passwordstore/ui/pgp/PGPKeyList.kt
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-package app.passwordstore.ui.pgp
-
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
-import androidx.compose.material3.AlertDialog
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.tooling.preview.Preview
-import app.passwordstore.R
-import app.passwordstore.crypto.PGPIdentifier
-import app.passwordstore.ui.compose.theme.APSTheme
-import app.passwordstore.ui.compose.theme.SpacingLarge
-import app.passwordstore.util.extensions.conditional
-import kotlinx.collections.immutable.ImmutableList
-import kotlinx.collections.immutable.persistentListOf
-import kotlinx.collections.immutable.toPersistentList
-
-@Composable
-fun KeyList(
- identifiers: ImmutableList<PGPIdentifier>,
- onItemClick: (identifier: PGPIdentifier) -> Unit,
- modifier: Modifier = Modifier,
- onKeySelected: ((identifier: PGPIdentifier) -> Unit)? = null,
-) {
- if (identifiers.isEmpty()) {
- Column(
- modifier = modifier.fillMaxSize(),
- verticalArrangement = Arrangement.Center,
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- Image(
- painter = painterResource(id = R.drawable.ic_launcher_foreground),
- contentDescription = "Password Store logo",
- )
- Text(stringResource(R.string.pgp_key_manager_no_keys_guidance))
- }
- } else {
- LazyColumn(modifier = modifier) {
- items(identifiers) { identifier ->
- KeyItem(identifier = identifier, onItemClick = onItemClick, onKeySelected = onKeySelected)
- }
- }
- }
-}
-
-@Composable
-private fun KeyItem(
- identifier: PGPIdentifier,
- onItemClick: (identifier: PGPIdentifier) -> Unit,
- modifier: Modifier = Modifier,
- onKeySelected: ((identifier: PGPIdentifier) -> Unit)? = null,
-) {
- var isDeleting by remember { mutableStateOf(false) }
- DeleteConfirmationDialog(
- isDeleting = isDeleting,
- onDismiss = { isDeleting = false },
- onConfirm = {
- onItemClick(identifier)
- isDeleting = false
- },
- )
- val label =
- when (identifier) {
- is PGPIdentifier.KeyId -> identifier.id.toString()
- is PGPIdentifier.UserId -> identifier.email
- }
- Row(
- modifier =
- modifier.padding(SpacingLarge).fillMaxWidth().conditional(onKeySelected != null) {
- clickable { onKeySelected?.invoke(identifier) }
- },
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Text(
- text = label,
- modifier = Modifier.weight(1f),
- overflow = TextOverflow.Ellipsis,
- maxLines = 1,
- )
- if (onKeySelected == null) {
- IconButton(onClick = { isDeleting = true }) {
- Icon(
- painter = painterResource(R.drawable.ic_delete_24dp),
- stringResource(id = R.string.delete),
- )
- }
- }
- }
-}
-
-@Suppress("NOTHING_TO_INLINE")
-@Composable
-private inline fun DeleteConfirmationDialog(
- isDeleting: Boolean,
- noinline onDismiss: () -> Unit,
- noinline onConfirm: () -> Unit,
-) {
- if (isDeleting) {
- AlertDialog(
- onDismissRequest = onDismiss,
- title = {
- Text(text = stringResource(R.string.pgp_key_manager_delete_confirmation_dialog_title))
- },
- confirmButton = {
- TextButton(onClick = onConfirm) { Text(text = stringResource(R.string.dialog_yes)) }
- },
- dismissButton = {
- TextButton(onClick = onDismiss) { Text(text = stringResource(R.string.dialog_no)) }
- },
- )
- }
-}
-
-@Preview
-@Composable
-private fun KeyListPreview() {
- APSTheme {
- Box(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
- KeyList(
- identifiers =
- listOfNotNull(
- PGPIdentifier.fromString("ultramicroscopicsilicovolcanoconiosis@example.com"),
- PGPIdentifier.fromString("0xB950AE2813841585"),
- )
- .toPersistentList(),
- onItemClick = {},
- )
- }
- }
-}
-
-@Preview
-@Composable
-private fun EmptyKeyListPreview() {
- APSTheme {
- Box(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
- KeyList(identifiers = persistentListOf(), onItemClick = {})
- }
- }
-}
diff --git a/app/src/main/java/app/passwordstore/ui/pgp/PGPKeyListActivity.kt b/app/src/main/java/app/passwordstore/ui/pgp/PGPKeyListActivity.kt
deleted file mode 100644
index 77b9aaf1..00000000
--- a/app/src/main/java/app/passwordstore/ui/pgp/PGPKeyListActivity.kt
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-package app.passwordstore.ui.pgp
-
-import android.content.Context
-import android.content.Intent
-import android.os.Bundle
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
-import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
-import androidx.activity.viewModels
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.FloatingActionButton
-import androidx.compose.material3.Icon
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Scaffold
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.res.stringResource
-import app.passwordstore.R
-import app.passwordstore.ui.APSAppBar
-import app.passwordstore.ui.compose.theme.APSTheme
-import app.passwordstore.util.viewmodel.PGPKeyListViewModel
-import dagger.hilt.android.AndroidEntryPoint
-
-@AndroidEntryPoint
-class PGPKeyListActivity : ComponentActivity() {
-
- private val viewModel: PGPKeyListViewModel by viewModels()
- private val keyImportAction =
- registerForActivityResult(StartActivityForResult()) {
- if (it.resultCode == RESULT_OK) {
- viewModel.updateKeySet()
- }
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- val isSelecting = intent.extras?.getBoolean(EXTRA_KEY_SELECTION) ?: false
- setContent {
- APSTheme {
- Scaffold(
- topBar = {
- APSAppBar(
- title = stringResource(R.string.activity_label_pgp_key_manager),
- navigationIcon = painterResource(R.drawable.ic_arrow_back_black_24dp),
- onNavigationIconClick = { finish() },
- backgroundColor = MaterialTheme.colorScheme.surface,
- )
- },
- floatingActionButton = {
- FloatingActionButton(
- onClick = { keyImportAction.launch(Intent(this, PGPKeyImportActivity::class.java)) }
- ) {
- Icon(
- painter = painterResource(R.drawable.ic_add_48dp),
- stringResource(R.string.pref_import_pgp_key_title),
- )
- }
- },
- ) { paddingValues ->
- KeyList(
- identifiers = viewModel.keys,
- onItemClick = viewModel::deleteKey,
- modifier = Modifier.padding(paddingValues),
- onKeySelected =
- if (isSelecting) {
- { identifier ->
- val result = Intent()
- result.putExtra(EXTRA_SELECTED_KEY, identifier.toString())
- setResult(RESULT_OK, result)
- finish()
- }
- } else null,
- )
- }
- }
- }
- }
-
- companion object {
- const val EXTRA_SELECTED_KEY = "SELECTED_KEY"
- private const val EXTRA_KEY_SELECTION = "KEY_SELECTION_MODE"
-
- fun newSelectionActivity(context: Context): Intent {
- val intent = Intent(context, PGPKeyListActivity::class.java)
- intent.putExtra(EXTRA_KEY_SELECTION, true)
- return intent
- }
- }
-}
diff --git a/app/src/main/java/app/passwordstore/ui/sshkeygen/ShowSshKeyFragment.kt b/app/src/main/java/app/passwordstore/ui/sshkeygen/ShowSshKeyFragment.kt
deleted file mode 100644
index 3f04e35e..00000000
--- a/app/src/main/java/app/passwordstore/ui/sshkeygen/ShowSshKeyFragment.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-package app.passwordstore.ui.sshkeygen
-
-import android.app.Dialog
-import android.content.Intent
-import android.os.Bundle
-import androidx.fragment.app.DialogFragment
-import app.passwordstore.R
-import app.passwordstore.util.git.sshj.SshKey
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-
-class ShowSshKeyFragment : DialogFragment() {
-
- override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
- val activity = requireActivity()
- val publicKey = SshKey.sshPublicKey
- return MaterialAlertDialogBuilder(requireActivity()).run {
- setMessage(getString(R.string.ssh_keygen_message, publicKey))
- setTitle(R.string.your_public_key)
- setNegativeButton(R.string.ssh_keygen_later) { _, _ ->
- (activity as? SshKeyGenActivity)?.finish()
- }
- setPositiveButton(R.string.ssh_keygen_share) { _, _ ->
- val sendIntent =
- Intent().apply {
- action = Intent.ACTION_SEND
- type = "text/plain"
- putExtra(Intent.EXTRA_TEXT, publicKey)
- }
- startActivity(Intent.createChooser(sendIntent, null))
- (activity as? SshKeyGenActivity)?.finish()
- }
- create()
- }
- }
-}
diff --git a/app/src/main/java/app/passwordstore/ui/sshkeygen/SshKeyGenActivity.kt b/app/src/main/java/app/passwordstore/ui/sshkeygen/SshKeyGenActivity.kt
deleted file mode 100644
index 618a4be0..00000000
--- a/app/src/main/java/app/passwordstore/ui/sshkeygen/SshKeyGenActivity.kt
+++ /dev/null
@@ -1,173 +0,0 @@
-/*
- * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-package app.passwordstore.ui.sshkeygen
-
-import android.content.SharedPreferences
-import android.os.Bundle
-import android.security.keystore.UserNotAuthenticatedException
-import android.view.MenuItem
-import android.view.View
-import android.view.inputmethod.InputMethodManager
-import androidx.appcompat.app.AppCompatActivity
-import androidx.core.content.edit
-import androidx.core.content.getSystemService
-import androidx.lifecycle.lifecycleScope
-import app.passwordstore.R
-import app.passwordstore.databinding.ActivitySshKeygenBinding
-import app.passwordstore.injection.prefs.GitPreferences
-import app.passwordstore.util.auth.BiometricAuthenticator
-import app.passwordstore.util.auth.BiometricAuthenticator.Result
-import app.passwordstore.util.coroutines.DispatcherProvider
-import app.passwordstore.util.extensions.keyguardManager
-import app.passwordstore.util.extensions.viewBinding
-import app.passwordstore.util.git.sshj.SshKey
-import com.github.michaelbull.result.fold
-import com.github.michaelbull.result.runCatching
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import dagger.hilt.android.AndroidEntryPoint
-import javax.inject.Inject
-import kotlin.coroutines.resume
-import kotlin.coroutines.suspendCoroutine
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-
-private enum class KeyGenType(val generateKey: suspend (requireAuthentication: Boolean) -> Unit) {
- Rsa({ requireAuthentication ->
- SshKey.generateKeystoreNativeKey(SshKey.Algorithm.Rsa, requireAuthentication)
- }),
- Ecdsa({ requireAuthentication ->
- SshKey.generateKeystoreNativeKey(SshKey.Algorithm.Ecdsa, requireAuthentication)
- }),
- Ed25519({ requireAuthentication ->
- SshKey.generateKeystoreWrappedEd25519Key(requireAuthentication)
- }),
-}
-
-@AndroidEntryPoint
-class SshKeyGenActivity : AppCompatActivity() {
-
- private var keyGenType = KeyGenType.Ecdsa
- private val binding by viewBinding(ActivitySshKeygenBinding::inflate)
- @GitPreferences @Inject lateinit var gitPrefs: SharedPreferences
- @Inject lateinit var dispatcherProvider: DispatcherProvider
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(binding.root)
- supportActionBar?.setDisplayHomeAsUpEnabled(true)
- with(binding) {
- generate.setOnClickListener {
- if (SshKey.exists) {
- MaterialAlertDialogBuilder(this@SshKeyGenActivity).run {
- setTitle(R.string.ssh_keygen_existing_title)
- setMessage(R.string.ssh_keygen_existing_message)
- setPositiveButton(R.string.ssh_keygen_existing_replace) { _, _ ->
- lifecycleScope.launch { generate() }
- }
- setNegativeButton(R.string.ssh_keygen_existing_keep) { _, _ ->
- setResult(RESULT_CANCELED)
- finish()
- }
- show()
- }
- } else {
- lifecycleScope.launch { generate() }
- }
- }
- keyTypeGroup.check(R.id.key_type_ecdsa)
- keyTypeExplanation.setText(R.string.ssh_keygen_explanation_ecdsa)
- keyTypeGroup.addOnButtonCheckedListener { _, checkedId, isChecked ->
- if (isChecked) {
- keyGenType =
- when (checkedId) {
- R.id.key_type_ed25519 -> KeyGenType.Ed25519
- R.id.key_type_ecdsa -> KeyGenType.Ecdsa
- R.id.key_type_rsa -> KeyGenType.Rsa
- else -> throw IllegalStateException("Impossible key type selection")
- }
- keyTypeExplanation.setText(
- when (keyGenType) {
- KeyGenType.Ed25519 -> R.string.ssh_keygen_explanation_ed25519
- KeyGenType.Ecdsa -> R.string.ssh_keygen_explanation_ecdsa
- KeyGenType.Rsa -> R.string.ssh_keygen_explanation_rsa
- }
- )
- }
- }
- keyRequireAuthentication.isEnabled = keyguardManager.isDeviceSecure
- keyRequireAuthentication.isChecked = keyRequireAuthentication.isEnabled
- }
- }
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- return when (item.itemId) {
- android.R.id.home -> {
- onBackPressedDispatcher.onBackPressed()
- true
- }
- else -> super.onOptionsItemSelected(item)
- }
- }
-
- private suspend fun generate() {
- binding.generate.apply {
- text = getString(R.string.ssh_key_gen_generating_progress)
- isEnabled = false
- }
- binding.generate.text = getString(R.string.ssh_key_gen_generating_progress)
- val result = runCatching {
- withContext(dispatcherProvider.io()) {
- val requireAuthentication = binding.keyRequireAuthentication.isChecked
- if (requireAuthentication) {
- val result =
- withContext(dispatcherProvider.main()) {
- suspendCoroutine { cont ->
- BiometricAuthenticator.authenticate(
- this@SshKeyGenActivity,
- R.string.biometric_prompt_title_ssh_keygen,
- ) { result ->
- // Do not cancel on failed attempts as these are handled by the
- // authenticator UI.
- if (result !is Result.Retry) cont.resume(result)
- }
- }
- }
- if (result !is Result.Success)
- throw UserNotAuthenticatedException(getString(R.string.biometric_auth_generic_failure))
- }
- keyGenType.generateKey(requireAuthentication)
- }
- }
- gitPrefs.edit { remove("ssh_key_local_passphrase") }
- binding.generate.apply {
- text = getString(R.string.ssh_keygen_generate)
- isEnabled = true
- }
- result.fold(
- success = { ShowSshKeyFragment().show(supportFragmentManager, "public_key") },
- failure = { e ->
- e.printStackTrace()
- MaterialAlertDialogBuilder(this)
- .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()
- },
- )
- hideKeyboard()
- }
-
- private fun hideKeyboard() {
- val imm = getSystemService<InputMethodManager>() ?: return
- var view = currentFocus
- if (view == null) {
- view = View(this)
- }
- imm.hideSoftInputFromWindow(view.windowToken, 0)
- }
-}
diff --git a/app/src/main/java/app/passwordstore/ui/sshkeygen/SshKeyImportActivity.kt b/app/src/main/java/app/passwordstore/ui/sshkeygen/SshKeyImportActivity.kt
deleted file mode 100644
index e4740741..00000000
--- a/app/src/main/java/app/passwordstore/ui/sshkeygen/SshKeyImportActivity.kt
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-package app.passwordstore.ui.sshkeygen
-
-import android.net.Uri
-import android.os.Bundle
-import android.widget.Toast
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.appcompat.app.AppCompatActivity
-import app.passwordstore.R
-import app.passwordstore.util.git.sshj.SshKey
-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/app/passwordstore/util/viewmodel/PGPKeyListViewModel.kt b/app/src/main/java/app/passwordstore/util/viewmodel/PGPKeyListViewModel.kt
deleted file mode 100644
index 5c7429cd..00000000
--- a/app/src/main/java/app/passwordstore/util/viewmodel/PGPKeyListViewModel.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-package app.passwordstore.util.viewmodel
-
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import app.passwordstore.crypto.KeyUtils
-import app.passwordstore.crypto.PGPIdentifier
-import app.passwordstore.crypto.PGPKeyManager
-import com.github.michaelbull.result.map
-import com.github.michaelbull.result.onSuccess
-import dagger.hilt.android.lifecycle.HiltViewModel
-import javax.inject.Inject
-import kotlinx.collections.immutable.ImmutableList
-import kotlinx.collections.immutable.persistentListOf
-import kotlinx.collections.immutable.toPersistentList
-import kotlinx.coroutines.launch
-
-@HiltViewModel
-class PGPKeyListViewModel @Inject constructor(private val keyManager: PGPKeyManager) : ViewModel() {
- var keys: ImmutableList<PGPIdentifier> by mutableStateOf(persistentListOf())
-
- init {
- updateKeySet()
- }
-
- fun updateKeySet() {
- viewModelScope.launch {
- keyManager
- .getAllKeys()
- .map { keys -> keys.mapNotNull { key -> KeyUtils.tryGetEmail(key) } }
- .onSuccess { keys = it.toPersistentList() }
- }
- }
-
- fun deleteKey(identifier: PGPIdentifier) {
- viewModelScope.launch {
- keyManager.removeKey(identifier)
- updateKeySet()
- }
- }
-}