aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/main/java
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-11-03 08:59:12 +0100
committerDanilo M. <danix@danix.xyz>2026-11-03 08:59:12 +0100
commit7bdcb041de95c8d8d8d14f1dbd34a3380b326871 (patch)
tree42c1c3799f821ecc796fd88708b494237573583a /app/src/main/java
parent32511cf9a3970727df982ac2509696e534a027e7 (diff)
downloadHardenedPass-7bdcb041de95c8d8d8d14f1dbd34a3380b326871.tar.gz
HardenedPass-7bdcb041de95c8d8d8d14f1dbd34a3380b326871.zip
refactor(app): remove the legacy editor, generators and folder picker
The Compose editor, generator sheet, OTP dialog and folder picker replace PasswordCreationActivity, the generator and manual OTP dialog fragments, SelectFolderActivity with its fragment and adapter, their layouts and menus, and upstream's unused EditPasswordScreen with the PasswordField it used. flowbinding and swiperefreshlayout had no other users. BasePGPActivity keeps what autofill decryption still needs. Also drops two stale SshKey baseline entries, the snackbar FAB anchor whose view is gone, and resources only the deleted files used. 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/injection/pwgen/DicewareModule.kt3
-rw-r--r--app/src/main/java/app/passwordstore/ui/adapters/PasswordItemRecyclerAdapter.kt99
-rw-r--r--app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt8
-rw-r--r--app/src/main/java/app/passwordstore/ui/crypto/EditPasswordScreen.kt105
-rw-r--r--app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt510
-rw-r--r--app/src/main/java/app/passwordstore/ui/dialogs/DicewarePasswordGeneratorDialogFragment.kt90
-rw-r--r--app/src/main/java/app/passwordstore/ui/dialogs/OtpImportDialogFragment.kt47
-rw-r--r--app/src/main/java/app/passwordstore/ui/dialogs/PasswordGeneratorDialogFragment.kt148
-rw-r--r--app/src/main/java/app/passwordstore/ui/folderselect/SelectFolderActivity.kt51
-rw-r--r--app/src/main/java/app/passwordstore/ui/folderselect/SelectFolderFragment.kt98
-rw-r--r--app/src/main/java/app/passwordstore/util/extensions/AndroidExtensions.kt7
11 files changed, 2 insertions, 1164 deletions
diff --git a/app/src/main/java/app/passwordstore/injection/pwgen/DicewareModule.kt b/app/src/main/java/app/passwordstore/injection/pwgen/DicewareModule.kt
index 8572e3b8..c9c4e928 100644
--- a/app/src/main/java/app/passwordstore/injection/pwgen/DicewareModule.kt
+++ b/app/src/main/java/app/passwordstore/injection/pwgen/DicewareModule.kt
@@ -13,7 +13,6 @@ import app.passwordstore.passgen.diceware.RandomIntGenerator
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
-import dagger.hilt.android.components.FragmentComponent
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.InputStream
@@ -21,7 +20,7 @@ import java.security.SecureRandom
import javax.inject.Qualifier
@Module
-@InstallIn(FragmentComponent::class, ViewModelComponent::class)
+@InstallIn(ViewModelComponent::class)
object DicewareModule {
@Provides
diff --git a/app/src/main/java/app/passwordstore/ui/adapters/PasswordItemRecyclerAdapter.kt b/app/src/main/java/app/passwordstore/ui/adapters/PasswordItemRecyclerAdapter.kt
deleted file mode 100644
index 54be64d6..00000000
--- a/app/src/main/java/app/passwordstore/ui/adapters/PasswordItemRecyclerAdapter.kt
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-package app.passwordstore.ui.adapters
-
-import android.text.SpannableString
-import android.text.style.RelativeSizeSpan
-import android.view.MotionEvent
-import android.view.View
-import androidx.appcompat.widget.AppCompatImageView
-import androidx.appcompat.widget.AppCompatTextView
-import androidx.recyclerview.selection.ItemDetailsLookup
-import androidx.recyclerview.selection.Selection
-import androidx.recyclerview.widget.RecyclerView
-import app.passwordstore.R
-import app.passwordstore.data.password.PasswordItem
-import app.passwordstore.util.coroutines.DispatcherProvider
-import app.passwordstore.util.viewmodel.SearchableRepositoryAdapter
-import app.passwordstore.util.viewmodel.stableId
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.withContext
-
-open class PasswordItemRecyclerAdapter(
- coroutineScope: CoroutineScope,
- dispatcherProvider: DispatcherProvider,
-) :
- SearchableRepositoryAdapter<PasswordItemRecyclerAdapter.PasswordItemViewHolder>(
- R.layout.password_row_layout,
- ::PasswordItemViewHolder,
- coroutineScope,
- dispatcherProvider,
- PasswordItemViewHolder::bind,
- ) {
-
- fun makeSelectable(recyclerView: RecyclerView) {
- makeSelectable(recyclerView, ::PasswordItemDetailsLookup)
- }
-
- override fun onItemClicked(
- listener: (holder: PasswordItemViewHolder, item: PasswordItem) -> Unit
- ): PasswordItemRecyclerAdapter {
- return super.onItemClicked(listener) as PasswordItemRecyclerAdapter
- }
-
- override fun onSelectionChanged(
- listener: (selection: Selection<String>) -> Unit
- ): PasswordItemRecyclerAdapter {
- return super.onSelectionChanged(listener) as PasswordItemRecyclerAdapter
- }
-
- class PasswordItemViewHolder(view: View) : RecyclerView.ViewHolder(view) {
-
- private val name: AppCompatTextView = itemView.findViewById(R.id.label)
- private val childCount: AppCompatTextView = itemView.findViewById(R.id.child_count)
- private val folderIndicator: AppCompatImageView = itemView.findViewById(R.id.folder_indicator)
- var itemDetails: ItemDetailsLookup.ItemDetails<String>? = null
-
- suspend fun bind(item: PasswordItem, dispatcherProvider: DispatcherProvider) {
- val parentPath = item.fullPathToParent.replace("(^/)|(/$)".toRegex(), "")
- val source =
- if (parentPath.isNotEmpty()) {
- "$parentPath\n$item"
- } else {
- "$item"
- }
- val spannable = SpannableString(source)
- spannable.setSpan(RelativeSizeSpan(0.7f), 0, parentPath.length, 0)
- name.text = spannable
- if (item.type == PasswordItem.TYPE_CATEGORY) {
- folderIndicator.visibility = View.VISIBLE
- val count =
- withContext(dispatcherProvider.io()) {
- item.file.listFiles { path -> path.isDirectory || path.extension == "gpg" }?.size ?: 0
- }
- childCount.visibility = if (count > 0) View.VISIBLE else View.GONE
- childCount.text = "$count"
- } else {
- childCount.visibility = View.GONE
- folderIndicator.visibility = View.GONE
- }
- itemDetails =
- object : ItemDetailsLookup.ItemDetails<String>() {
- override fun getPosition() = absoluteAdapterPosition
-
- override fun getSelectionKey() = item.stableId
- }
- }
- }
-
- class PasswordItemDetailsLookup(private val recyclerView: RecyclerView) :
- ItemDetailsLookup<String>() {
-
- override fun getItemDetails(event: MotionEvent): ItemDetails<String>? {
- val view = recyclerView.findChildViewUnder(event.x, event.y) ?: return null
- return (recyclerView.getChildViewHolder(view) as PasswordItemViewHolder).itemDetails
- }
- }
-}
diff --git a/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt b/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt
index ba49a62c..ccd3bf08 100644
--- a/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt
+++ b/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt
@@ -29,7 +29,6 @@ import app.passwordstore.util.extensions.clipboard
import app.passwordstore.util.extensions.getString
import app.passwordstore.util.extensions.sharedPrefs
import app.passwordstore.util.extensions.snackbar
-import app.passwordstore.util.extensions.unsafeLazy
import app.passwordstore.util.services.ClipboardService
import app.passwordstore.util.settings.Constants
import app.passwordstore.util.settings.PreferenceKeys
@@ -44,12 +43,6 @@ import kotlinx.coroutines.withContext
@AndroidEntryPoint
open class BasePGPActivity : AppCompatActivity() {
- /** Full path to the repository */
- val repoPath by unsafeLazy { intent.getStringExtra("REPO_PATH")!! }
-
- /** Full path to the password file being worked on */
- val fullPath by unsafeLazy { intent.getStringExtra("FILE_PATH")!! }
-
/** Action to invoke if [keyImportAction] succeeds. */
private var onKeyImport: (() -> Unit)? = null
private val keyImportAction =
@@ -103,7 +96,6 @@ open class BasePGPActivity : AppCompatActivity() {
companion object {
const val EXTRA_FILE_PATH = "FILE_PATH"
- const val EXTRA_REPO_PATH = "REPO_PATH"
/** Gets the relative path to the repository */
fun getRelativePath(fullPath: String, repositoryPath: String): String =
diff --git a/app/src/main/java/app/passwordstore/ui/crypto/EditPasswordScreen.kt b/app/src/main/java/app/passwordstore/ui/crypto/EditPasswordScreen.kt
deleted file mode 100644
index d8610307..00000000
--- a/app/src/main/java/app/passwordstore/ui/crypto/EditPasswordScreen.kt
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-package app.passwordstore.ui.crypto
-
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextField
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.tooling.preview.Preview
-import app.passwordstore.R
-import app.passwordstore.data.passfile.PasswordEntry
-import app.passwordstore.ui.APSAppBar
-import app.passwordstore.ui.compose.PasswordField
-import app.passwordstore.ui.compose.theme.APSTheme
-import app.passwordstore.ui.compose.theme.SpacingLarge
-import app.passwordstore.ui.compose.theme.SpacingMedium
-import app.passwordstore.util.time.UserClock
-import app.passwordstore.util.totp.UriTotpFinder
-
-/** Composable to show allow editing an existing [PasswordEntry]. */
-@Composable
-fun EditPasswordScreen(
- entryName: String,
- entry: PasswordEntry,
- onNavigateUp: () -> Unit,
- @Suppress("UNUSED_PARAMETER") onSave: (PasswordEntry) -> Unit,
- modifier: Modifier = Modifier,
-) {
- Scaffold(
- topBar = {
- APSAppBar(
- title = entryName,
- navigationIcon = painterResource(R.drawable.ic_arrow_back_black_24dp),
- onNavigationIconClick = onNavigateUp,
- backgroundColor = MaterialTheme.colorScheme.surface,
- )
- }
- ) { paddingValues ->
- Box(modifier = modifier.padding(paddingValues)) {
- Column(
- modifier =
- Modifier.padding(vertical = SpacingMedium, horizontal = SpacingLarge).fillMaxSize()
- ) {
- if (entry.password != null) {
- PasswordField(
- value = entry.password!!,
- label = stringResource(R.string.password),
- initialVisibility = false,
- modifier = Modifier.padding(bottom = SpacingMedium).fillMaxWidth(),
- )
- }
- ExtraContent(entry = entry)
- }
- }
- }
-}
-
-@Composable
-private fun ExtraContent(entry: PasswordEntry, modifier: Modifier = Modifier) {
- TextField(
- value = entry.extraContentString,
- onValueChange = {},
- label = { Text("Extra content") },
- modifier = modifier.fillMaxWidth(),
- )
-}
-
-@Preview
-@Composable
-private fun EditPasswordScreenPreview() {
- APSTheme {
- EditPasswordScreen(
- entryName = "Test Entry",
- entry = createTestEntry(),
- onNavigateUp = {},
- onSave = {},
- )
- }
-}
-
-private fun createTestEntry() =
- PasswordEntry(
- UserClock(),
- UriTotpFinder(),
- """
- |My Password
- |otpauth://totp/ACME%20Co:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30
- |login: msfjarvis
- |URL: example.com
- """
- .trimMargin()
- .encodeToByteArray(),
- )
diff --git a/app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt b/app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt
deleted file mode 100644
index 7652c98e..00000000
--- a/app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt
+++ /dev/null
@@ -1,510 +0,0 @@
-/*
- * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-package app.passwordstore.ui.crypto
-
-import android.content.Context
-import android.content.Intent
-import android.content.pm.PackageManager
-import android.graphics.Bitmap
-import android.graphics.ImageDecoder
-import android.os.Build
-import android.os.Bundle
-import android.provider.MediaStore
-import android.text.InputType
-import android.view.Menu
-import android.view.MenuItem
-import android.view.View
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
-import androidx.core.content.edit
-import androidx.core.view.isVisible
-import androidx.core.widget.doAfterTextChanged
-import androidx.lifecycle.lifecycleScope
-import app.passwordstore.R
-import app.passwordstore.data.passfile.PasswordEntry
-import app.passwordstore.databinding.PasswordCreationActivityBinding
-import app.passwordstore.ui.dialogs.DicewarePasswordGeneratorDialogFragment
-import app.passwordstore.ui.dialogs.OtpImportDialogFragment
-import app.passwordstore.ui.dialogs.PasswordGeneratorDialogFragment
-import app.passwordstore.util.autofill.AutofillPreferences
-import app.passwordstore.util.extensions.asLog
-import app.passwordstore.util.extensions.base64
-import app.passwordstore.util.extensions.commitChange
-import app.passwordstore.util.extensions.getString
-import app.passwordstore.util.extensions.isInsideRepository
-import app.passwordstore.util.extensions.snackbar
-import app.passwordstore.util.extensions.unsafeLazy
-import app.passwordstore.util.extensions.viewBinding
-import app.passwordstore.util.settings.DirectoryStructure
-import app.passwordstore.util.settings.PreferenceKeys
-import com.github.michaelbull.result.getOrElse
-import com.github.michaelbull.result.onFailure
-import com.github.michaelbull.result.onSuccess
-import com.github.michaelbull.result.runCatching
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import com.google.zxing.BinaryBitmap
-import com.google.zxing.LuminanceSource
-import com.google.zxing.RGBLuminanceSource
-import com.google.zxing.common.HybridBinarizer
-import com.google.zxing.integration.android.IntentIntegrator
-import com.google.zxing.integration.android.IntentIntegrator.QR_CODE
-import com.google.zxing.qrcode.QRCodeReader
-import dagger.hilt.android.AndroidEntryPoint
-import java.io.ByteArrayOutputStream
-import java.io.IOException
-import java.nio.file.Paths
-import javax.inject.Inject
-import kotlin.io.path.absolutePathString
-import kotlin.io.path.createDirectories
-import kotlin.io.path.deleteIfExists
-import kotlin.io.path.exists
-import kotlin.io.path.isSameFileAs
-import kotlin.io.path.nameWithoutExtension
-import kotlin.io.path.pathString
-import kotlin.io.path.relativeTo
-import kotlin.io.path.writeBytes
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import logcat.LogPriority.ERROR
-import logcat.asLog
-import logcat.logcat
-
-@AndroidEntryPoint
-class PasswordCreationActivity : BasePGPActivity() {
-
- private val binding by viewBinding(PasswordCreationActivityBinding::inflate)
- @Inject lateinit var passwordEntryFactory: PasswordEntry.Factory
-
- private val suggestedName by unsafeLazy { intent.getStringExtra(EXTRA_FILE_NAME) }
- private val suggestedUsername by unsafeLazy { intent.getStringExtra(EXTRA_USERNAME) }
- private val suggestedPass by unsafeLazy { intent.getStringExtra(EXTRA_PASSWORD) }
- private val suggestedExtra by unsafeLazy { intent.getStringExtra(EXTRA_EXTRA_CONTENT) }
- private val shouldGeneratePassword by unsafeLazy {
- intent.getBooleanExtra(EXTRA_GENERATE_PASSWORD, false)
- }
- private val editing by unsafeLazy { intent.getBooleanExtra(EXTRA_EDITING, false) }
- private var oldCategory: String? = null
- private var copy: Boolean = false
-
- private val otpImportAction =
- registerForActivityResult(StartActivityForResult()) { result ->
- if (result.resultCode == RESULT_OK) {
- binding.otpImportButton.isVisible = false
- val intentResult = IntentIntegrator.parseActivityResult(RESULT_OK, result.data)
- val contents = "${intentResult.contents}\n"
- val currentExtras = binding.extraContent.text.toString()
- if (currentExtras.isNotEmpty() && currentExtras.last() != '\n')
- binding.extraContent.append("\n$contents")
- else binding.extraContent.append(contents)
- snackbar(message = getString(R.string.otp_import_success))
- } else {
- snackbar(message = getString(R.string.otp_import_failure_generic))
- }
- }
-
- private val imageImportAction =
- registerForActivityResult(ActivityResultContracts.GetContent()) { imageUri ->
- if (imageUri == null) {
- snackbar(message = getString(R.string.otp_import_failure_no_selection))
- return@registerForActivityResult
- }
- val bitmap =
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- ImageDecoder.decodeBitmap(ImageDecoder.createSource(contentResolver, imageUri))
- .copy(Bitmap.Config.ARGB_8888, true)
- } else {
- @Suppress("DEPRECATION") MediaStore.Images.Media.getBitmap(contentResolver, imageUri)
- }
- val intArray = IntArray(bitmap.width * bitmap.height)
- // copy pixel data from the Bitmap into the 'intArray' array
- bitmap.getPixels(intArray, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height)
- val source: LuminanceSource = RGBLuminanceSource(bitmap.width, bitmap.height, intArray)
- val binaryBitmap = BinaryBitmap(HybridBinarizer(source))
-
- val reader = QRCodeReader()
- runCatching {
- val result = reader.decode(binaryBitmap)
- val text = result.text
- val currentExtras = binding.extraContent.text.toString()
- if (currentExtras.isNotEmpty() && currentExtras.last() != '\n')
- binding.extraContent.append("\n$text")
- else binding.extraContent.append(text)
- snackbar(message = getString(R.string.otp_import_success))
- binding.otpImportButton.isVisible = false
- }
- .onFailure { snackbar(message = getString(R.string.otp_import_failure_generic)) }
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- supportActionBar?.setDisplayHomeAsUpEnabled(true)
- title =
- if (editing) getString(R.string.edit_password) else getString(R.string.new_password_title)
- with(binding) {
- setContentView(root)
- generatePassword.setOnClickListener { generatePassword() }
- otpImportButton.setOnClickListener {
- supportFragmentManager.setFragmentResultListener(
- OTP_RESULT_REQUEST_KEY,
- this@PasswordCreationActivity,
- ) { requestKey, bundle ->
- if (requestKey == OTP_RESULT_REQUEST_KEY) {
- val contents = bundle.getString(RESULT)
- val currentExtras = binding.extraContent.text.toString()
- if (currentExtras.isNotEmpty() && currentExtras.last() != '\n')
- binding.extraContent.append("\n$contents")
- else binding.extraContent.append(contents)
- }
- }
- val hasCamera = packageManager?.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY) == true
- if (hasCamera) {
- val items =
- arrayOf(
- getString(R.string.otp_import_qr_code),
- getString(R.string.otp_import_from_file),
- getString(R.string.otp_import_manual_entry),
- )
- MaterialAlertDialogBuilder(this@PasswordCreationActivity)
- .setItems(items) { _, index ->
- when (index) {
- 0 ->
- otpImportAction.launch(
- IntentIntegrator(this@PasswordCreationActivity)
- .setOrientationLocked(false)
- .setBeepEnabled(false)
- .setDesiredBarcodeFormats(QR_CODE)
- .createScanIntent()
- )
- 1 -> imageImportAction.launch("image/*")
- 2 -> OtpImportDialogFragment().show(supportFragmentManager, "OtpImport")
- }
- }
- .show()
- } else {
- OtpImportDialogFragment().show(supportFragmentManager, "OtpImport")
- }
- }
-
- directoryInputLayout.apply {
- if (suggestedName != null || suggestedPass != null || shouldGeneratePassword) {
- isEnabled = true
- } else {
- setBackgroundColor(getColor(android.R.color.transparent))
- }
- val path = getRelativePath(fullPath, repoPath)
- // Keep empty path field visible if it is editable.
- if (path.isEmpty() && !isEnabled) visibility = View.GONE
- else {
- directory.setText(path)
- oldCategory = path
- }
- }
- if (suggestedName != null) {
- filename.setText(suggestedName)
- } else {
- filename.requestFocus()
- }
-
- if (
- AutofillPreferences.directoryStructure(this@PasswordCreationActivity) ==
- DirectoryStructure.EncryptedUsername || suggestedUsername != null
- ) {
- usernameInputLayout.visibility = View.VISIBLE
- if (suggestedUsername != null) username.setText(suggestedUsername)
- else if (suggestedName != null) username.requestFocus()
- }
-
- // Allow the user to quickly switch between storing the username as the filename or
- // in the encrypted extras. This only makes sense if the directory structure is
- // FileBased.
- if (
- suggestedName == null &&
- AutofillPreferences.directoryStructure(this@PasswordCreationActivity) ==
- DirectoryStructure.FileBased
- ) {
- encryptUsername.apply {
- visibility = View.VISIBLE
- setOnClickListener {
- if (isChecked) {
- // User wants to enable username encryption, so we use the filename
- // as username and insert it into the username input field.
- val login = filename.text.toString()
- filename.text?.clear()
- username.setText(login)
- usernameInputLayout.apply { visibility = View.VISIBLE }
- } else {
- // User wants to disable username encryption, so we take the username
- // from the username text field and insert it into the filename input field.
- val login = username.text.toString()
- username.text?.clear()
- filename.setText(login)
- usernameInputLayout.apply { visibility = View.GONE }
- }
- }
- }
- }
- suggestedPass?.let {
- password.setText(it)
- password.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
- }
- suggestedExtra?.let { extraContent.setText(it) }
- if (shouldGeneratePassword) {
- generatePassword()
- password.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
- }
- }
- listOf(binding.filename, binding.username, binding.extraContent).forEach {
- it.doAfterTextChanged { updateViewState() }
- }
- updateViewState()
- }
-
- override fun onCreateOptionsMenu(menu: Menu): Boolean {
- menuInflater.inflate(R.menu.pgp_handler_new_password, menu)
- return true
- }
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- when (item.itemId) {
- android.R.id.home -> {
- setResult(RESULT_CANCELED)
- onBackPressedDispatcher.onBackPressed()
- }
- R.id.save_password -> {
- copy = false
- requireKeysExist { encrypt() }
- }
- R.id.save_and_copy_password -> {
- copy = true
- requireKeysExist { encrypt() }
- }
- else -> return super.onOptionsItemSelected(item)
- }
- return true
- }
-
- private fun generatePassword() {
- supportFragmentManager.setFragmentResultListener(PASSWORD_RESULT_REQUEST_KEY, this) {
- requestKey,
- bundle ->
- if (requestKey == PASSWORD_RESULT_REQUEST_KEY) {
- binding.password.setText(bundle.getString(RESULT))
- }
- }
- when (settings.getString(PreferenceKeys.PREF_KEY_PWGEN_TYPE) ?: KEY_PWGEN_TYPE_DICEWARE) {
- KEY_PWGEN_TYPE_CLASSIC ->
- PasswordGeneratorDialogFragment().show(supportFragmentManager, "generator")
- KEY_PWGEN_TYPE_DICEWARE ->
- DicewarePasswordGeneratorDialogFragment().show(supportFragmentManager, "generator")
- }
- }
-
- private fun updateViewState() =
- with(binding) {
- encryptUsername.apply {
- if (visibility != View.VISIBLE) return@apply
- val hasUsernameInFileName = filename.text.toString().isNotBlank()
- val usernameIsEncrypted = username.text.toString().isNotEmpty()
- isEnabled = hasUsernameInFileName xor usernameIsEncrypted
- isChecked = usernameIsEncrypted
- }
- // Use PasswordEntry to parse extras for OTP
- val entry =
- passwordEntryFactory.create("PLACEHOLDER\n${extraContent.text}".encodeToByteArray())
- otpImportButton.isVisible = !entry.hasTotp()
- }
-
- /** Encrypts the password and the extra content */
- private fun encrypt() {
- with(binding) {
- val oldName = suggestedName
- val editName = filename.text.toString().trim()
- var editUsername = username.text.toString()
- val editPass = password.text.toString()
- val editExtra = extraContent.text.toString()
-
- if (editName.isEmpty()) {
- snackbar(message = resources.getString(R.string.file_toast_text))
- return@with
- } else if (editName.contains('/')) {
- snackbar(message = resources.getString(R.string.invalid_filename_text))
- return@with
- }
-
- if (!editUsername.isEmpty()) {
- editUsername = "\nusername: $editUsername"
- }
-
- if (editPass.isEmpty() && editExtra.isEmpty()) {
- snackbar(message = resources.getString(R.string.empty_toast_text))
- return@with
- }
-
- if (copy) {
- copyPasswordToClipboard(editPass)
- }
-
- // pass enters the key ID into `.gpg-id`.
- val gpgIdentifiers = getPGPIdentifiers(directory.text.toString()) ?: return@with
- val content = "$editPass$editUsername\n$editExtra"
- val path =
- when {
- // If we allowed the user to edit the relative path, we have to consider it here
- // instead of fullPath.
- directoryInputLayout.isEnabled -> {
- val editRelativePath = directory.text.toString().trim()
- if (editRelativePath.isEmpty()) {
- snackbar(message = resources.getString(R.string.path_toast_text))
- return
- }
- val passwordDirectory = Paths.get(repoPath, editRelativePath.trim('/'))
- passwordDirectory.createDirectories()
- if (!passwordDirectory.exists()) {
- snackbar(
- message =
- "Failed to create directory ${passwordDirectory.relativeTo(Paths.get(repoPath)).pathString}"
- )
- return
- }
-
- "${passwordDirectory.pathString}/$editName.gpg"
- }
- else -> "$fullPath/$editName.gpg"
- }
-
- lifecycleScope.launch(dispatcherProvider.main()) {
- runCatching {
- val result =
- withContext(dispatcherProvider.io()) {
- val outputStream = ByteArrayOutputStream()
- repository.encrypt(gpgIdentifiers, content.byteInputStream(), outputStream)
- }
- .getOrElse { e ->
- logcat(ERROR) { e.asLog("Failed to encrypt password file") }
- MaterialAlertDialogBuilder(this@PasswordCreationActivity)
- .setTitle(R.string.password_creation_file_fail_title)
- .setMessage(
- getString(R.string.password_creation_encrypt_fail_message, e.message)
- )
- .setPositiveButton(android.R.string.ok, null)
- .show()
- return@runCatching
- }
- val passwordFile = Paths.get(path)
- // If we're not editing, this file should not already exist!
- // Additionally, if we were editing and the incoming and outgoing
- // filenames differ, it means we renamed. Ensure that the target
- // doesn't already exist to prevent an accidental overwrite.
- if (
- (!editing || (editing && suggestedName != passwordFile.nameWithoutExtension)) &&
- passwordFile.exists()
- ) {
- snackbar(message = getString(R.string.password_creation_duplicate_error))
- return@runCatching
- }
-
- if (!passwordFile.toFile().isInsideRepository()) {
- snackbar(message = getString(R.string.message_error_destination_outside_repo))
- return@runCatching
- }
-
- withContext(dispatcherProvider.io()) { passwordFile.writeBytes(result.toByteArray()) }
-
- // associate the new password name with the last name's timestamp in history
- val preference = getSharedPreferences("recent_password_history", Context.MODE_PRIVATE)
- val oldFilePathHash = "$repoPath/${oldCategory?.trim('/')}/$suggestedName.gpg".base64()
- val timestamp = preference.getString(oldFilePathHash)
- if (timestamp != null) {
- preference.edit {
- remove(oldFilePathHash)
- putString(passwordFile.absolutePathString().base64(), timestamp)
- }
- }
-
- val returnIntent = Intent()
- returnIntent.putExtra(RETURN_EXTRA_CREATED_FILE, path)
- returnIntent.putExtra(RETURN_EXTRA_NAME, editName)
- returnIntent.putExtra(RETURN_EXTRA_LONG_NAME, getLongName(fullPath, repoPath, editName))
-
- if (shouldGeneratePassword) {
- val directoryStructure = AutofillPreferences.directoryStructure(applicationContext)
- val entry = passwordEntryFactory.create(content.encodeToByteArray())
- returnIntent.putExtra(RETURN_EXTRA_PASSWORD, entry.password)
- val username =
- entry.username ?: directoryStructure.getUsernameFor(passwordFile.toFile())
- returnIntent.putExtra(RETURN_EXTRA_USERNAME, username)
- }
-
- if (
- directoryInputLayout.isVisible &&
- directoryInputLayout.isEnabled &&
- oldName != editName
- ) {
- val oldPath = Paths.get(repoPath, oldCategory?.trim('/') ?: "", "$oldName.gpg")
- if (!oldPath.isSameFileAs(passwordFile) && !oldPath.deleteIfExists()) {
- setResult(RESULT_CANCELED)
- MaterialAlertDialogBuilder(this@PasswordCreationActivity)
- .setTitle(R.string.password_creation_file_fail_title)
- .setMessage(
- getString(R.string.password_creation_file_delete_fail_message, oldName)
- )
- .setCancelable(false)
- .setPositiveButton(android.R.string.ok) { _, _ -> finish() }
- .show()
- return@runCatching
- }
- }
-
- val commitMessageRes =
- if (editing) R.string.git_commit_edit_text else R.string.git_commit_add_text
- lifecycleScope.launch {
- commitChange(
- resources.getString(commitMessageRes, getLongName(fullPath, repoPath, editName))
- )
- .onSuccess {
- setResult(RESULT_OK, returnIntent)
- finish()
- }
- }
- }
- .onFailure { e ->
- if (e is IOException) {
- logcat(ERROR) { e.asLog("Failed to write password file") }
- setResult(RESULT_CANCELED)
- MaterialAlertDialogBuilder(this@PasswordCreationActivity)
- .setTitle(getString(R.string.password_creation_file_fail_title))
- .setMessage(getString(R.string.password_creation_file_write_fail_message))
- .setCancelable(false)
- .setPositiveButton(android.R.string.ok) { _, _ -> finish() }
- .show()
- } else {
- logcat(ERROR) { e.asLog() }
- }
- }
- }
- }
- }
-
- companion object {
-
- private const val KEY_PWGEN_TYPE_CLASSIC = "classic"
- private const val KEY_PWGEN_TYPE_DICEWARE = "diceware"
- const val PASSWORD_RESULT_REQUEST_KEY = "PASSWORD_GENERATOR"
- const val OTP_RESULT_REQUEST_KEY = "OTP_IMPORT"
- const val RESULT = "RESULT"
- const val RETURN_EXTRA_CREATED_FILE = "CREATED_FILE"
- const val RETURN_EXTRA_NAME = "NAME"
- const val RETURN_EXTRA_LONG_NAME = "LONG_NAME"
- const val RETURN_EXTRA_USERNAME = "USERNAME"
- const val RETURN_EXTRA_PASSWORD = "PASSWORD"
- const val EXTRA_FILE_NAME = "FILENAME"
- const val EXTRA_USERNAME = "USERNAME"
- const val EXTRA_PASSWORD = "PASSWORD"
- const val EXTRA_EXTRA_CONTENT = "EXTRA_CONTENT"
- const val EXTRA_GENERATE_PASSWORD = "GENERATE_PASSWORD"
- const val EXTRA_EDITING = "EDITING"
- }
-}
diff --git a/app/src/main/java/app/passwordstore/ui/dialogs/DicewarePasswordGeneratorDialogFragment.kt b/app/src/main/java/app/passwordstore/ui/dialogs/DicewarePasswordGeneratorDialogFragment.kt
deleted file mode 100644
index 2605e0ef..00000000
--- a/app/src/main/java/app/passwordstore/ui/dialogs/DicewarePasswordGeneratorDialogFragment.kt
+++ /dev/null
@@ -1,90 +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.app.AlertDialog
-import android.app.Dialog
-import android.content.SharedPreferences
-import android.graphics.Typeface
-import android.os.Bundle
-import androidx.core.content.edit
-import androidx.core.os.bundleOf
-import androidx.fragment.app.DialogFragment
-import androidx.fragment.app.setFragmentResult
-import androidx.lifecycle.lifecycleScope
-import app.passwordstore.R
-import app.passwordstore.databinding.FragmentPwgenDicewareBinding
-import app.passwordstore.injection.prefs.PasswordGeneratorPreferences
-import app.passwordstore.passgen.diceware.DicewarePassphraseGenerator
-import app.passwordstore.ui.crypto.PasswordCreationActivity
-import app.passwordstore.util.extensions.getString
-import app.passwordstore.util.settings.PreferenceKeys.DICEWARE_LENGTH
-import app.passwordstore.util.settings.PreferenceKeys.DICEWARE_SEPARATOR
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import dagger.hilt.android.AndroidEntryPoint
-import javax.inject.Inject
-import kotlinx.coroutines.flow.merge
-import kotlinx.coroutines.launch
-import reactivecircus.flowbinding.android.widget.afterTextChanges
-
-@AndroidEntryPoint
-class DicewarePasswordGeneratorDialogFragment : DialogFragment() {
-
- @Inject lateinit var dicewareGenerator: DicewarePassphraseGenerator
- @Inject @PasswordGeneratorPreferences lateinit var prefs: SharedPreferences
-
- override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
- val builder = MaterialAlertDialogBuilder(requireContext())
-
- val binding = FragmentPwgenDicewareBinding.inflate(layoutInflater)
- builder.setView(binding.root)
-
- binding.passwordSeparatorText.setText(prefs.getString(DICEWARE_SEPARATOR) ?: "-")
- binding.passwordLengthText.setText(prefs.getInt(DICEWARE_LENGTH, 5).toString())
- binding.passwordText.typeface = Typeface.MONOSPACE
-
- lifecycleScope.launch {
- merge(
- binding.passwordLengthText.afterTextChanges(),
- binding.passwordSeparatorText.afterTextChanges(),
- )
- .collect { _ -> generatePassword(binding) }
- }
- return builder
- .run {
- setTitle(R.string.pwgen_title)
- setPositiveButton(R.string.dialog_ok) { _, _ ->
- setFragmentResult(
- PasswordCreationActivity.PASSWORD_RESULT_REQUEST_KEY,
- bundleOf(PasswordCreationActivity.RESULT to "${binding.passwordText.text}"),
- )
- }
- setNeutralButton(R.string.dialog_cancel) { _, _ -> }
- setNegativeButton(R.string.pwgen_generate, null)
- create()
- }
- .apply {
- setOnShowListener {
- generatePassword(binding)
- getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener { generatePassword(binding) }
- }
- }
- }
-
- private fun generatePassword(binding: FragmentPwgenDicewareBinding) {
- val length = binding.passwordLengthText.text?.toString()?.toIntOrNull() ?: 5
- val separator = binding.passwordSeparatorText.text?.toString()?.getOrNull(0) ?: '-'
- setPreferences(length, separator)
- binding.passwordText.text = dicewareGenerator.generatePassphrase(length, separator)
- }
-
- private fun setPreferences(length: Int, separator: Char) {
- prefs.edit {
- putInt(DICEWARE_LENGTH, length)
- putString(DICEWARE_SEPARATOR, separator.toString())
- }
- }
-}
diff --git a/app/src/main/java/app/passwordstore/ui/dialogs/OtpImportDialogFragment.kt b/app/src/main/java/app/passwordstore/ui/dialogs/OtpImportDialogFragment.kt
deleted file mode 100644
index 1a75175b..00000000
--- a/app/src/main/java/app/passwordstore/ui/dialogs/OtpImportDialogFragment.kt
+++ /dev/null
@@ -1,47 +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.app.Dialog
-import android.net.Uri
-import android.os.Bundle
-import android.view.WindowManager
-import androidx.core.os.bundleOf
-import androidx.fragment.app.DialogFragment
-import androidx.fragment.app.setFragmentResult
-import app.passwordstore.databinding.FragmentManualOtpEntryBinding
-import app.passwordstore.ui.crypto.PasswordCreationActivity
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-
-class OtpImportDialogFragment : DialogFragment() {
-
- override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
- val builder = MaterialAlertDialogBuilder(requireContext())
- val binding = FragmentManualOtpEntryBinding.inflate(layoutInflater)
- builder.setView(binding.root)
- builder.setPositiveButton(android.R.string.ok) { _, _ ->
- setFragmentResult(
- PasswordCreationActivity.OTP_RESULT_REQUEST_KEY,
- bundleOf(PasswordCreationActivity.RESULT to getTOTPUri(binding)),
- )
- }
- val dialog = builder.create()
- dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
- return dialog
- }
-
- private fun getTOTPUri(binding: FragmentManualOtpEntryBinding): String {
- val secret = binding.secret.text.toString()
- val account = binding.account.text.toString()
- if (secret.isBlank()) return ""
- val builder = Uri.Builder()
- builder.scheme("otpauth")
- builder.authority("totp")
- builder.appendQueryParameter("secret", secret)
- if (account.isNotBlank()) builder.appendQueryParameter("issuer", account)
- return builder.build().toString()
- }
-}
diff --git a/app/src/main/java/app/passwordstore/ui/dialogs/PasswordGeneratorDialogFragment.kt b/app/src/main/java/app/passwordstore/ui/dialogs/PasswordGeneratorDialogFragment.kt
deleted file mode 100644
index d4a2372b..00000000
--- a/app/src/main/java/app/passwordstore/ui/dialogs/PasswordGeneratorDialogFragment.kt
+++ /dev/null
@@ -1,148 +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.app.AlertDialog
-import android.app.Dialog
-import android.content.Context
-import android.graphics.Typeface
-import android.os.Bundle
-import android.widget.CheckBox
-import android.widget.EditText
-import android.widget.Toast
-import androidx.annotation.IdRes
-import androidx.appcompat.widget.AppCompatTextView
-import androidx.core.content.edit
-import androidx.core.os.bundleOf
-import androidx.fragment.app.DialogFragment
-import androidx.fragment.app.setFragmentResult
-import androidx.lifecycle.lifecycleScope
-import app.passwordstore.R
-import app.passwordstore.databinding.FragmentPwgenBinding
-import app.passwordstore.passgen.random.MaxIterationsExceededException
-import app.passwordstore.passgen.random.NoCharactersIncludedException
-import app.passwordstore.passgen.random.PasswordGenerator
-import app.passwordstore.passgen.random.PasswordLengthTooShortException
-import app.passwordstore.passgen.random.PasswordOption
-import app.passwordstore.ui.crypto.PasswordCreationActivity
-import app.passwordstore.util.settings.PreferenceKeys
-import com.github.michaelbull.result.getOrElse
-import com.github.michaelbull.result.runCatching
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import kotlinx.coroutines.flow.merge
-import kotlinx.coroutines.launch
-import reactivecircus.flowbinding.android.widget.afterTextChanges
-import reactivecircus.flowbinding.android.widget.checkedChanges
-
-class PasswordGeneratorDialogFragment : DialogFragment() {
-
- override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
- val prefs = requireContext().getSharedPreferences("PasswordGenerator", Context.MODE_PRIVATE)
- val builder = MaterialAlertDialogBuilder(requireContext())
-
- val binding = FragmentPwgenBinding.inflate(layoutInflater)
- builder.setView(binding.root)
-
- binding.numerals.isChecked = !prefs.getBoolean(PasswordOption.NoDigits.key, false)
- binding.symbols.isChecked = prefs.getBoolean(PasswordOption.AtLeastOneSymbol.key, false)
- binding.uppercase.isChecked = !prefs.getBoolean(PasswordOption.NoUppercaseLetters.key, false)
- binding.lowercase.isChecked = !prefs.getBoolean(PasswordOption.NoLowercaseLetters.key, false)
- binding.ambiguous.isChecked = !prefs.getBoolean(PasswordOption.NoAmbiguousCharacters.key, false)
- binding.pronounceable.isChecked = !prefs.getBoolean(PasswordOption.FullyRandom.key, true)
- binding.lengthNumber.setText(prefs.getInt(PreferenceKeys.LENGTH, 20).toString())
- binding.passwordText.typeface = Typeface.MONOSPACE
-
- lifecycleScope.launch {
- merge(
- binding.numerals.checkedChanges().skipInitialValue(),
- binding.symbols.checkedChanges().skipInitialValue(),
- binding.uppercase.checkedChanges().skipInitialValue(),
- binding.lowercase.checkedChanges().skipInitialValue(),
- binding.ambiguous.checkedChanges().skipInitialValue(),
- binding.pronounceable.checkedChanges().skipInitialValue(),
- binding.lengthNumber.afterTextChanges().skipInitialValue(),
- )
- .collect { generate(binding.passwordText) }
- }
-
- return builder
- .run {
- setTitle(R.string.pwgen_title)
- setPositiveButton(R.string.dialog_ok) { _, _ ->
- setFragmentResult(
- PasswordCreationActivity.PASSWORD_RESULT_REQUEST_KEY,
- bundleOf(PasswordCreationActivity.RESULT to "${binding.passwordText.text}"),
- )
- }
- setNeutralButton(R.string.dialog_cancel) { _, _ -> }
- setNegativeButton(R.string.pwgen_generate, null)
- create()
- }
- .apply {
- setOnShowListener {
- generate(binding.passwordText)
- getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener {
- generate(binding.passwordText)
- }
- }
- }
- }
-
- private fun generate(passwordField: AppCompatTextView) {
- val passwordOptions = getSelectedOptions()
- val passwordLength = getLength()
- setPrefs(requireContext(), passwordOptions, passwordLength)
- passwordField.text =
- runCatching { PasswordGenerator.generate(passwordOptions, passwordLength) }
- .getOrElse { exception ->
- val errorText =
- when (exception) {
- is MaxIterationsExceededException ->
- requireContext().getString(R.string.pwgen_max_iterations_exceeded)
- is NoCharactersIncludedException ->
- requireContext().getString(R.string.pwgen_no_chars_error)
- is PasswordLengthTooShortException ->
- requireContext().getString(R.string.pwgen_length_too_short_error)
- else -> requireContext().getString(R.string.pwgen_some_error_occurred)
- }
- Toast.makeText(requireActivity(), errorText, Toast.LENGTH_SHORT).show()
- ""
- }
- }
-
- private fun isChecked(@IdRes id: Int): Boolean {
- return requireDialog().findViewById<CheckBox>(id).isChecked
- }
-
- private fun getSelectedOptions(): List<PasswordOption> {
- return listOfNotNull(
- PasswordOption.NoDigits.takeIf { !isChecked(R.id.numerals) },
- PasswordOption.AtLeastOneSymbol.takeIf { isChecked(R.id.symbols) },
- PasswordOption.NoUppercaseLetters.takeIf { !isChecked(R.id.uppercase) },
- PasswordOption.NoAmbiguousCharacters.takeIf { !isChecked(R.id.ambiguous) },
- PasswordOption.FullyRandom.takeIf { !isChecked(R.id.pronounceable) },
- PasswordOption.NoLowercaseLetters.takeIf { !isChecked(R.id.lowercase) },
- )
- }
-
- private fun getLength(): Int {
- val lengthText = requireDialog().findViewById<EditText>(R.id.lengthNumber).text.toString()
- return lengthText.toIntOrNull()?.takeIf { it >= 0 } ?: PasswordGenerator.DEFAULT_LENGTH
- }
-
- /**
- * Enables the [PasswordOption]s in [options] and sets [targetLength] as the length for generated
- * passwords.
- */
- private fun setPrefs(ctx: Context, options: List<PasswordOption>, targetLength: Int): Boolean {
- ctx.getSharedPreferences("PasswordGenerator", Context.MODE_PRIVATE).edit {
- for (possibleOption in PasswordOption.entries) {
- putBoolean(possibleOption.key, possibleOption in options)
- }
- putInt("length", targetLength)
- }
- return true
- }
-}
diff --git a/app/src/main/java/app/passwordstore/ui/folderselect/SelectFolderActivity.kt b/app/src/main/java/app/passwordstore/ui/folderselect/SelectFolderActivity.kt
deleted file mode 100644
index 2139d7d2..00000000
--- a/app/src/main/java/app/passwordstore/ui/folderselect/SelectFolderActivity.kt
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-package app.passwordstore.ui.folderselect
-
-import android.os.Bundle
-import android.view.Menu
-import android.view.MenuItem
-import androidx.appcompat.app.AppCompatActivity
-import androidx.fragment.app.FragmentManager
-import androidx.fragment.app.commit
-import app.passwordstore.R
-import dagger.hilt.android.AndroidEntryPoint
-
-@AndroidEntryPoint
-class SelectFolderActivity : AppCompatActivity(R.layout.select_folder_layout) {
-
- private lateinit var passwordList: SelectFolderFragment
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- passwordList = SelectFolderFragment()
- supportActionBar?.show()
- supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
- supportFragmentManager.commit { replace(R.id.pgp_handler_linearlayout, passwordList) }
- }
-
- override fun onCreateOptionsMenu(menu: Menu): Boolean {
- menuInflater.inflate(R.menu.pgp_handler_select_folder, menu)
- return true
- }
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- when (item.itemId) {
- android.R.id.home -> {
- setResult(RESULT_CANCELED)
- onBackPressedDispatcher.onBackPressed()
- }
- R.id.crypto_select -> selectFolder()
- else -> return super.onOptionsItemSelected(item)
- }
- return true
- }
-
- private fun selectFolder() {
- intent.putExtra("SELECTED_FOLDER_PATH", passwordList.currentDir.absolutePath)
- setResult(RESULT_OK, intent)
- finish()
- }
-}
diff --git a/app/src/main/java/app/passwordstore/ui/folderselect/SelectFolderFragment.kt b/app/src/main/java/app/passwordstore/ui/folderselect/SelectFolderFragment.kt
deleted file mode 100644
index acf4b56d..00000000
--- a/app/src/main/java/app/passwordstore/ui/folderselect/SelectFolderFragment.kt
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-package app.passwordstore.ui.folderselect
-
-import android.content.Context
-import android.os.Bundle
-import android.view.View
-import androidx.appcompat.app.AppCompatActivity
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.activityViewModels
-import androidx.lifecycle.flowWithLifecycle
-import androidx.lifecycle.lifecycleScope
-import androidx.recyclerview.widget.LinearLayoutManager
-import app.passwordstore.R
-import app.passwordstore.data.password.PasswordItem
-import app.passwordstore.data.repo.PasswordRepository
-import app.passwordstore.databinding.PasswordRecyclerViewBinding
-import app.passwordstore.ui.adapters.PasswordItemRecyclerAdapter
-import app.passwordstore.util.coroutines.DispatcherProvider
-import app.passwordstore.util.extensions.viewBinding
-import app.passwordstore.util.viewmodel.ListMode
-import app.passwordstore.util.viewmodel.SearchableRepositoryViewModel
-import com.github.michaelbull.result.onFailure
-import com.github.michaelbull.result.runCatching
-import dagger.hilt.android.AndroidEntryPoint
-import java.io.File
-import javax.inject.Inject
-import kotlinx.coroutines.launch
-import me.zhanghai.android.fastscroll.FastScrollerBuilder
-
-@AndroidEntryPoint
-class SelectFolderFragment : Fragment(R.layout.password_recycler_view) {
-
- @Inject lateinit var dispatcherProvider: DispatcherProvider
- private val binding by viewBinding(PasswordRecyclerViewBinding::bind)
- private lateinit var recyclerAdapter: PasswordItemRecyclerAdapter
- private lateinit var listener: OnFragmentInteractionListener
-
- private val model: SearchableRepositoryViewModel by activityViewModels()
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- binding.fab.hide()
- recyclerAdapter =
- PasswordItemRecyclerAdapter(lifecycleScope, dispatcherProvider).onItemClicked { _, item ->
- listener.onFragmentInteraction(item)
- }
- binding.passRecycler.apply {
- layoutManager = LinearLayoutManager(requireContext())
- itemAnimator = null
- adapter = recyclerAdapter
- }
-
- FastScrollerBuilder(binding.passRecycler).build()
- registerForContextMenu(binding.passRecycler)
-
- model.navigateTo(
- PasswordRepository.getRepositoryDirectory(),
- listMode = ListMode.DirectoriesOnly,
- pushPreviousLocation = false,
- )
- lifecycleScope.launch {
- model.searchResult.flowWithLifecycle(lifecycle).collect { result ->
- recyclerAdapter.submitList(result.passwordItems)
- }
- }
- }
-
- override fun onAttach(context: Context) {
- super.onAttach(context)
- runCatching {
- listener =
- object : OnFragmentInteractionListener {
- override fun onFragmentInteraction(item: PasswordItem) {
- if (item.type == PasswordItem.TYPE_CATEGORY) {
- model.navigateTo(item.file, listMode = ListMode.DirectoriesOnly)
- (requireActivity() as AppCompatActivity)
- .supportActionBar
- ?.setDisplayHomeAsUpEnabled(true)
- }
- }
- }
- }
- .onFailure {
- throw ClassCastException("$context must implement OnFragmentInteractionListener")
- }
- }
-
- val currentDir: File
- get() = model.currentDir.value
-
- interface OnFragmentInteractionListener {
-
- fun onFragmentInteraction(item: PasswordItem)
- }
-}
diff --git a/app/src/main/java/app/passwordstore/util/extensions/AndroidExtensions.kt b/app/src/main/java/app/passwordstore/util/extensions/AndroidExtensions.kt
index a13484c6..bad55de4 100644
--- a/app/src/main/java/app/passwordstore/util/extensions/AndroidExtensions.kt
+++ b/app/src/main/java/app/passwordstore/util/extensions/AndroidExtensions.kt
@@ -95,18 +95,13 @@ fun FragmentActivity.isPermissionGranted(permission: String): Boolean {
return ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
}
-/**
- * Show a [Snackbar] in a [FragmentActivity] and correctly anchor it to a
- * [com.google.android.material.floatingactionbutton.FloatingActionButton] if one exists in the
- * [view]
- */
+/** Show a [Snackbar] in a [FragmentActivity]. */
fun FragmentActivity.snackbar(
view: View = findViewById(android.R.id.content),
message: String,
length: Int = Snackbar.LENGTH_SHORT,
): Snackbar {
val snackbar = Snackbar.make(view, message, length)
- snackbar.anchorView = findViewById(R.id.fab)
snackbar.show()
return snackbar
}