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