From 7bdcb041de95c8d8d8d14f1dbd34a3380b326871 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 3 Nov 2026 08:59:12 +0100 Subject: 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 --- app/build.gradle.kts | 2 - app/lint-baseline.xml | 46 +- app/src/main/AndroidManifest.xml | 9 - .../injection/pwgen/DicewareModule.kt | 3 +- .../ui/adapters/PasswordItemRecyclerAdapter.kt | 99 ---- .../app/passwordstore/ui/crypto/BasePGPActivity.kt | 8 - .../passwordstore/ui/crypto/EditPasswordScreen.kt | 105 ----- .../ui/crypto/PasswordCreationActivity.kt | 510 --------------------- .../DicewarePasswordGeneratorDialogFragment.kt | 90 ---- .../ui/dialogs/OtpImportDialogFragment.kt | 47 -- .../ui/dialogs/PasswordGeneratorDialogFragment.kt | 148 ------ .../ui/folderselect/SelectFolderActivity.kt | 51 --- .../ui/folderselect/SelectFolderFragment.kt | 98 ---- .../util/extensions/AndroidExtensions.kt | 7 +- app/src/main/res/drawable/ic_done_24dp.xml | 15 - app/src/main/res/drawable/ic_save_24dp.xml | 15 - app/src/main/res/drawable/ic_save_copy_24dp.xml | 15 - .../main/res/layout/fragment_manual_otp_entry.xml | 53 --- app/src/main/res/layout/fragment_pwgen.xml | 115 ----- .../main/res/layout/fragment_pwgen_diceware.xml | 73 --- .../main/res/layout/password_creation_activity.xml | 147 ------ app/src/main/res/layout/password_recycler_view.xml | 38 -- app/src/main/res/layout/password_row_layout.xml | 50 -- app/src/main/res/layout/select_folder_layout.xml | 16 - app/src/main/res/menu/pgp_handler_new_password.xml | 20 - .../main/res/menu/pgp_handler_select_folder.xml | 15 - app/src/main/res/values-de/strings.xml | 8 - app/src/main/res/values-fr/strings.xml | 8 - app/src/main/res/values-gl/strings.xml | 8 - app/src/main/res/values-it/strings.xml | 8 - app/src/main/res/values-ko-rKR/strings.xml | 8 - app/src/main/res/values-pl-rPL/strings.xml | 8 - app/src/main/res/values-pt-rBR/strings.xml | 8 - app/src/main/res/values-ru/strings.xml | 8 - app/src/main/res/values-zh-rCN/strings.xml | 8 - app/src/main/res/values/strings.xml | 8 - .../specs/2026-11-02-compose-ui-overhaul-design.md | 8 +- gradle/libs.versions.toml | 3 - .../app/passwordstore/ui/compose/PasswordField.kt | 81 ---- 39 files changed, 9 insertions(+), 1958 deletions(-) delete mode 100644 app/src/main/java/app/passwordstore/ui/adapters/PasswordItemRecyclerAdapter.kt delete mode 100644 app/src/main/java/app/passwordstore/ui/crypto/EditPasswordScreen.kt delete mode 100644 app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt delete mode 100644 app/src/main/java/app/passwordstore/ui/dialogs/DicewarePasswordGeneratorDialogFragment.kt delete mode 100644 app/src/main/java/app/passwordstore/ui/dialogs/OtpImportDialogFragment.kt delete mode 100644 app/src/main/java/app/passwordstore/ui/dialogs/PasswordGeneratorDialogFragment.kt delete mode 100644 app/src/main/java/app/passwordstore/ui/folderselect/SelectFolderActivity.kt delete mode 100644 app/src/main/java/app/passwordstore/ui/folderselect/SelectFolderFragment.kt delete mode 100644 app/src/main/res/drawable/ic_done_24dp.xml delete mode 100644 app/src/main/res/drawable/ic_save_24dp.xml delete mode 100644 app/src/main/res/drawable/ic_save_copy_24dp.xml delete mode 100644 app/src/main/res/layout/fragment_manual_otp_entry.xml delete mode 100644 app/src/main/res/layout/fragment_pwgen.xml delete mode 100644 app/src/main/res/layout/fragment_pwgen_diceware.xml delete mode 100644 app/src/main/res/layout/password_creation_activity.xml delete mode 100644 app/src/main/res/layout/password_recycler_view.xml delete mode 100644 app/src/main/res/layout/password_row_layout.xml delete mode 100644 app/src/main/res/layout/select_folder_layout.xml delete mode 100644 app/src/main/res/menu/pgp_handler_new_password.xml delete mode 100644 app/src/main/res/menu/pgp_handler_select_folder.xml delete mode 100644 ui/compose/src/main/kotlin/app/passwordstore/ui/compose/PasswordField.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cc10d21e..5f7a8ee8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -73,7 +73,6 @@ dependencies { implementation(libs.androidx.preference) implementation(libs.androidx.recyclerview) implementation(libs.androidx.recyclerviewSelection) - implementation(libs.androidx.swiperefreshlayout) implementation(libs.compose.ui.tooling) implementation(libs.dagger.hilt.android) @@ -86,7 +85,6 @@ dependencies { implementation(libs.aps.zxingAndroidEmbedded) implementation(libs.thirdparty.fastscroll) - implementation(libs.thirdparty.flowbinding.android) implementation(libs.thirdparty.jgit) { exclude(group = "org.apache.httpcomponents", module = "httpclient") } diff --git a/app/lint-baseline.xml b/app/lint-baseline.xml index 20c4683f..a19d0ad9 100644 --- a/app/lint-baseline.xml +++ b/app/lint-baseline.xml @@ -36,29 +36,7 @@ errorLine2=" ~~~~~~~~~~~~~~"> - - - - - - - - @@ -197,26 +175,4 @@ column="4"/> - - - - - - - - diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b4859c0e..0429b2ac 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -95,12 +95,6 @@ android:label="@string/action_settings" android:parentActivityName=".ui.main.MainActivity" /> - - - ( - 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) -> 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? = 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() { - override fun getPosition() = absoluteAdapterPosition - - override fun getSelectionKey() = item.stableId - } - } - } - - class PasswordItemDetailsLookup(private val recyclerView: RecyclerView) : - ItemDetailsLookup() { - - override fun getItemDetails(event: MotionEvent): ItemDetails? { - 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(id).isChecked - } - - private fun getSelectedOptions(): List { - 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(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, 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 } diff --git a/app/src/main/res/drawable/ic_done_24dp.xml b/app/src/main/res/drawable/ic_done_24dp.xml deleted file mode 100644 index f8b6cef6..00000000 --- a/app/src/main/res/drawable/ic_done_24dp.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_save_24dp.xml b/app/src/main/res/drawable/ic_save_24dp.xml deleted file mode 100644 index 8a4276c4..00000000 --- a/app/src/main/res/drawable/ic_save_24dp.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_save_copy_24dp.xml b/app/src/main/res/drawable/ic_save_copy_24dp.xml deleted file mode 100644 index 06419aac..00000000 --- a/app/src/main/res/drawable/ic_save_copy_24dp.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - diff --git a/app/src/main/res/layout/fragment_manual_otp_entry.xml b/app/src/main/res/layout/fragment_manual_otp_entry.xml deleted file mode 100644 index 7d7ea832..00000000 --- a/app/src/main/res/layout/fragment_manual_otp_entry.xml +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_pwgen.xml b/app/src/main/res/layout/fragment_pwgen.xml deleted file mode 100644 index 02eadff5..00000000 --- a/app/src/main/res/layout/fragment_pwgen.xml +++ /dev/null @@ -1,115 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_pwgen_diceware.xml b/app/src/main/res/layout/fragment_pwgen_diceware.xml deleted file mode 100644 index b8627670..00000000 --- a/app/src/main/res/layout/fragment_pwgen_diceware.xml +++ /dev/null @@ -1,73 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/password_creation_activity.xml b/app/src/main/res/layout/password_creation_activity.xml deleted file mode 100644 index c0692753..00000000 --- a/app/src/main/res/layout/password_creation_activity.xml +++ /dev/null @@ -1,147 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/password_recycler_view.xml b/app/src/main/res/layout/password_recycler_view.xml deleted file mode 100644 index c2b5114f..00000000 --- a/app/src/main/res/layout/password_recycler_view.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - diff --git a/app/src/main/res/layout/password_row_layout.xml b/app/src/main/res/layout/password_row_layout.xml deleted file mode 100644 index 5c042839..00000000 --- a/app/src/main/res/layout/password_row_layout.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - diff --git a/app/src/main/res/layout/select_folder_layout.xml b/app/src/main/res/layout/select_folder_layout.xml deleted file mode 100644 index 78b03579..00000000 --- a/app/src/main/res/layout/select_folder_layout.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - diff --git a/app/src/main/res/menu/pgp_handler_new_password.xml b/app/src/main/res/menu/pgp_handler_new_password.xml deleted file mode 100644 index 6c61fb55..00000000 --- a/app/src/main/res/menu/pgp_handler_new_password.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - diff --git a/app/src/main/res/menu/pgp_handler_select_folder.xml b/app/src/main/res/menu/pgp_handler_select_folder.xml deleted file mode 100644 index d9505814..00000000 --- a/app/src/main/res/menu/pgp_handler_select_folder.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index d13ab19f..a9fa4b00 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -39,7 +39,6 @@ In die Zwischenablage kopiert Bitte setzen Sie einen Dateinamen. - Bitte setzen Sie einen Pfad. Sie können kein leeres Passwort oder leere Extra-Angaben setzen. Es ist ein Fehler während der Git-Operation aufgetreten. @@ -63,7 +62,6 @@ Name Passwort Zusätzliche Angaben - Benutzername verschlüsseln Auswählen Speichern Speichern und kopieren @@ -125,7 +123,6 @@ Passwort erstellen Erstellen - Einschließen Länge Nummern Symbole @@ -173,7 +170,6 @@ Commit-Log anzeigen Zeige das Passwort. Soll das entschlüsselte Passwort sichtbar sein? Dies deaktiviert nicht das Kopieren. - Erstellen Passwort unverschlüsselt senden an… App-Icon @@ -207,7 +203,6 @@ Autofill-Unterstützung mit installierten Browsern: Der private SSH-Schlüssel konnte nicht geöffnet werden. Bitte überprüfen Sie, ob die Datei existiert. - Neues Passwort Gespeicherte Passphrase für lokalen SSH-Schlüssel löschen Gespeichertes HTTPS-Passwort löschen Schlüsselpasswort merken @@ -276,7 +271,6 @@ .gpg-id wurde gefunden, enthält jedoch ungültige Schlüssel-ID, Fingerabdruck oder Benutzer-ID. .gpg-id wurde gefunden, enthält jedoch eine kurze Hex-ID, die nicht unterstützt wird. Dateiname darf kein \'/\' enthalten; Verzeichnis oben setzen - Ordner Unbekannter Fehler Pull fehlgeschlagen. Sie befinden sich in einem abgelösten Head. Speichern Sie die Änderungen in \"Einstellungen > Lokale Git-Konfiguration & Dienstprogramme\" auf das Remote-Repository in einem neuen Branch und lösen Sie den Konflikt auf Ihrem Computer auf. @@ -319,11 +313,9 @@ Host-Schlüssel erfolgreich gelöscht! QR-Code scannen Wählen Sie ein Bild. - Manuell eingeben Schlüssel Benutzer-Konto Verknüpfung auf dem Home-Screen erstellen - Neues Passwort oder neuen Ordner erstellen Fehler beim Import des PGP-Schlüssels Ein bestehender Schlüssel mit dieser ID wurde gefunden. Möchten Sie ihn ersetzen? PGP-Schlüssel erfolgreich importiert diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 0cfb345c..d41048f8 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -38,7 +38,6 @@ Copié dans le presse-papiers Veuillez fournir un nom de fichier - Veuillez fournir un chemin d\'accès au fichier Vous ne pouvez pas utiliser un mot de passe vide ou des données supplémentaires vide Une erreur c\'est produite lors d\'une opération Git @@ -62,7 +61,6 @@ Nom Mot de passe Extra - Chiffrer le nom d\'utilisateur Sélectionner Sauvegarder Enregistrer et copier @@ -113,7 +111,6 @@ Générer un mot de passe Générer - Inclure Longueur Chiffres Symboles @@ -161,7 +158,6 @@ Afficher l\'historique des commits Montrer le mot de passe Contrôle la visibilité des mots de passe une fois déchifrées, ceci n\'empêche pas de copier le mot de passe - Générer Envoyer le mot de passe en clair via… Icône de l\'application @@ -195,7 +191,6 @@ Prise en charge du remplissage automatique avec les navigateurs installés: Impossible d\'ouvrir la clef ssh, merci de vérifier que le ficher existe - Nouveau mot de passe Effacer le mot de passe enregistrée pour la clé SSH locale Effacer le mot de passe HTTPS enregistré Se rappeler de la phrase secrète dans la configuration de l\'application (peu sûr) @@ -261,7 +256,6 @@ Impossible d\'importer la configuration TOTP Exportation des mots de passe… Le nom du fichier ne doit pas contenir \'/\', définir le répertoire parent - Dossier Erreur inconnue Tirer a échoué, vous êtes dans un \'head\' détaché. En utilisant \"settings > git utils\", enregistrez vos modifications sur le dépôt distant dans une nouvelle branche et résolvez le conflit sur votre ordinateur. @@ -300,10 +294,8 @@ La clé d\'hôte a été effacée avec succès! Scanner le code QR Choisissez une image - Saisir manuellement Secret Compte - Créer un nouveau mot de passe ou dossier Impossible d\'importer la clé PGP Clé PGP importée avec succès La clé d\'identification de la clé importée est donné ci-dessous, veuillez la vérifier pour qu\'elle soit correcte :\n%1$s diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 589208ab..3fb321e7 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -39,7 +39,6 @@ Copiado ao portapapeis Debes proporcionar un nome de ficheiro - Por favor indica a ruta ao ficheiro Non podes deixar baleiro o contrasinal ou o contido extra Algo fallou na operación de Git @@ -63,7 +62,6 @@ Nome Contrasinal Contido extra - Cifrar identificador Seleccionar Gardar Gardar e copiar @@ -126,7 +124,6 @@ Crear contrasinal Crear - Incluír Lonxitude Números Símbolos @@ -174,7 +171,6 @@ Mostrar rexistro dos cambios Mostrar contrasinal Controlar a visibilidade do contrasinal unha vez descifrado, esto non desactiva o copiado do contrasinal - Crear Enviar contrasinal como texto plano usando… Icona da app @@ -209,7 +205,6 @@ a app desde unha fonte de confianza, como a Play Store, Amazon Appstore, F-Droid Soporte de autocompletado cos navegadores: Non se pode abrir a chave privada ssh, comproba que existe tal ficheiro - Novo contrasinal Baleirar a frase de paso gardada para a chave SSH local Baleirar contrasinal HTTPS gardado Lembrar frase de paso da chave @@ -278,7 +273,6 @@ a app desde unha fonte de confianza, como a Play Store, Amazon Appstore, F-Droid Atopouse .gpg-id, pero contén un ID de chave, impresión dixital ou ID de usuaria non válidos Atopouse .gpg-id, pero contén un ID hex curto, ao que non damos soporte O nome do ficheiro non pode conter \'/\', establece un directorio superior - Directorio Fallo descoñecido Fallou a acción pull, estás nun head diferente. Utiliza \"axustes > utilidades git\", garda os cambios no remoto nunha nova rama e resolve o conflicto nun ordenador. @@ -321,11 +315,9 @@ a app desde unha fonte de confianza, como a Play Store, Amazon Appstore, F-Droid Eliminouse correctamente a chave gardada do host! Escanear código QR Elixe unha imaxe - Escribir manualmente Segredo Conta Crear atallo na pantalla de inicio - Crear novo contrasinal ou cartafol Fallou a importación da chave PGP Atopouse unha chave existente con este ID, queres substituíla? Chave PGP importada correctamente diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 91cd28aa..c14efa33 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -38,7 +38,6 @@ Copiato negli appunti Inserisci un nome per il file - Inserisci il percorso del file Non puoi usare una password vuota o dei contenuti extra vuoti Si è verificato un errore durante un\'operazione Git @@ -62,7 +61,6 @@ Nome Password Contenuto extra - Crittografa nome utente Seleziona Salva Salva e Copia @@ -108,7 +106,6 @@ Genera Password Genera - Includi Lunghezza Numeri Simboli @@ -156,7 +153,6 @@ Mostra registro dei commit Mostra la password Controlla la visibilità delle password una volta decrittografate. Questo non disabilita la copia negli appunti. - Genera Invia password come testo semplice usando… Icona app @@ -190,7 +186,6 @@ Supporto alla compilazione automatica nei browser installati: Impossibile aprire la chiave privata SSH, controlla che il file esista - Nuova password Cancella la frase segreta salvata per la chiave SSH locale Cancella la password HTTPS salvata Ricorda la frase segreta della chiave @@ -256,7 +251,6 @@ Impossibile importare la configurazione di TOTP Esportazione delle password… Il nome del file non deve contenere \'/\', imposta la directory qui sopra - Directory Errore sconosciuto Pull non riuscito, sei in detached HEAD. Da \"Impostazioni > Utilità Git\" esegui il push delle modifiche sul remote in un nuovo branch e risolvi il conflitto dal computer. @@ -293,7 +287,6 @@ Cancella la chiave host salvata Chiave host cancellata con successo! Scegli un\'immagine - Crea nuova password o cartella Inizializza gli ID GPG in %1$s. Nome utente OTP (scade tra %ds) @@ -327,7 +320,6 @@ Il protocollo git di git-daemon non cifra il trasporto e non è adatto a operazioni sicure. Impostazioni proxy Scansiona codice QR - Inserisci manualmente Segreto Account Aggiungi scorciatoia alla schermata Home diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml index 7f63ad8b..3efbed68 100644 --- a/app/src/main/res/values-ko-rKR/strings.xml +++ b/app/src/main/res/values-ko-rKR/strings.xml @@ -37,7 +37,6 @@ 클립보드로 복사됨 파일 이름이 필요합니다 - 파일 경로가 필요합니다 비어있는 비밀번호나 추가 정보를 사용할 수 없습니다 Git 동작 중 문제가 발생하였습니다 @@ -61,7 +60,6 @@ 이름 비밀번호 추가 내용 - 암호화 계정 이름 선택 저장 저장 및 복사 @@ -121,7 +119,6 @@ 암호 생성 생성 - 포함 길이 숫자만 특수문자 @@ -169,7 +166,6 @@ 커밋 로그 보기 비밀번호 표시하기 암호가 해독된 후 비밀번호의 공개 여부를 제어합니다. 그렇다고 해서 클립보드 복사가 비활성화되지는 않습니다. - 생성 비밀번호를 일반 텍스트로 보내는 중… 앱 아이콘 @@ -203,7 +199,6 @@ 자동 완성 지원 하는 설치된 브라우저: SSH 개인 키를 열 수 없습니다. 파일이 존재하는지 확인하세요 - 새 암호 로컬 SSH 키의 저장된 비밀번호 지우기 저장된 HTTPS 비밀번호 지우기 키 암호 기억하기 @@ -274,7 +269,6 @@ username@example.com:… .gpg-id를 찾았지만 잘못된 키 ID, 지문 또는 사용자 ID가 포함되어 있습니다 .gpg-id를 찾았지만, 지원되지 않는 짧은 16진수 ID가 포함되어 있습니다. 파일 이름에 \'/\'가 포함되어서는 안됩니다. 위의 디렉터리를 설정하세요 - 디렉토리 알 수 없는 오류 풀이 실패하여 head가 분리된 상태입니다. \"설정 > git 기능\"을 사용하여 원격 저장소에 변경 내용을 새 브랜치에 저장하고 컴퓨터에서 충돌을 해결하세요. @@ -317,11 +311,9 @@ username@example.com:… 저장된 호스트 키를 성공적으로 지웠습니다! QR코드 스캔 이미지 선택 - 직접 입력 비밀 계정 홈 화면에 추가 - 새 비밀번호 또는 폴더 만들기 PGP 키를 가져오지 못했습니다 이 ID를 가진 기존 키가 발견되었습니다. 교체하시겠습니까? PGP 키를 성공적으로 가져왔습니다 diff --git a/app/src/main/res/values-pl-rPL/strings.xml b/app/src/main/res/values-pl-rPL/strings.xml index a2068e65..65f14ce5 100644 --- a/app/src/main/res/values-pl-rPL/strings.xml +++ b/app/src/main/res/values-pl-rPL/strings.xml @@ -37,7 +37,6 @@ Skopiowano do schowka Podaj nazwę pliku - Podaj ścieżkę do pliku Nie możesz użyć pustego hasła lub pustej dodatkowej zawartości Wystąpił błąd podczas operacji Git @@ -61,7 +60,6 @@ Nazwa Hasło Treść dodatkowa - Szyfruj nazwę użytkownika Wybierz Zapisz Zapisz i skopiuj @@ -122,7 +120,6 @@ Generuj hasło Wygeneruj - Załącz Długość Cyfry Symbole @@ -170,7 +167,6 @@ Pokaż dziennik commitów Pokaż hasło Kontroluj widoczność haseł po odszyfrowaniu. To nie wyłącza kopiowania do schowka. - Generuj Wyślij niezaszyfrowane hasło, używając… Ikona aplikacji @@ -202,7 +198,6 @@ Obsługa autouzupełniania w zainstalowanych przeglądarkach: Nie można otworzyć klucza prywatnego ssh, sprawdź czy plik istnieje - Nowe hasło Wyczyść zapisane hasło dla lokalnego klucza SSH Wyczyść zapisane hasło HTTPS Zapamiętaj hasło klucza @@ -268,7 +263,6 @@ Znaleziono .gpg-id, ale zawiera nieprawidłowy identyfikator klucza, odcisk palca lub identyfikator użytkownika Znaleziono .gpg-id, ale zawarty w nim skrócony identyfikator nie jest obsługiwany Nazwa pliku nie może zawierać \'/\', ustaw katalog powyżej - Katalog Nieznany błąd Zaciąganie zmian nie powiodło się, nie jesteś na żadnej gałęzi (detached head). Używając \"Ustawienia > narzędzia Git\", wyślij lokalne zmiany do zdalnego repozytorium na nowej gałęzi i rozwiąż konflikt na komputerze. @@ -309,11 +303,9 @@ Pomyślnie wyczyszczono zapisany klucz hosta! Skanuj kod QR Wybierz obraz - Wprowadź ręcznie Sekret Konto Utwórz skrót na pulpicie - Utwórz nowe hasło lub folder Nie udało się zaimportować klucza PGP Znaleziono istniejący klucz z tym identyfikatorem, czy chcesz go zastąpić? Pomyślnie zaimportowano klucz PGP diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 909fd33e..86ebfb24 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -39,7 +39,6 @@ Copiado para a área de transferência Por favor, informe um nome de arquivo - Por favor, forneça o caminho do arquivo Você não pode usar uma senha vazia ou conteúdo extra vazio Ocorreu um erro durante uma operação do Git @@ -63,7 +62,6 @@ Nome Senha Conteúdo extra - Criptografar nome de usuário Selecionar Salvar Salvar e Copiar @@ -125,7 +123,6 @@ Gerar Senha Gerar - Incluir Tamanho Numerais Símbolos @@ -173,7 +170,6 @@ Mostrar log de commit Mostrar a senha Controle a visibilidade das senhas quando descriptografadas. Isso não desativa a cópia para a área de transferência. - Gerar Enviar senha como texto simples usando… Ícone do aplicativo @@ -207,7 +203,6 @@ Suporte ao preenchimento automático com navegadores instalados: Não foi possível abrir a chave privada ssh, por favor verifique se o arquivo existe - Nova senha Limpar a frase secreta salva para chave SSH local Limpar senha HTTPS salva Lembrar senha da chave @@ -276,7 +271,6 @@ Encontrado .gpg-id, mas contém um ID de chave, fingerprint ou ID de usuário inválidos Encontrado .gpg-id, mas contém uma ID hexadecimal curta, que não é suportada Nome do arquivo não deve conter \'/\', defina o diretório acima - Diretório Erro desconhecido O pull falhou, você está em uma Head avulsa. Usando \"configurações > utils\" do git, salve suas alterações no remoto em uma nova branch e resolva o conflito no seu computador. @@ -319,11 +313,9 @@ Chave de host limpa com sucesso! Escanear QR code Selecione uma imagem - Inserir manualmente Segredo Conta Criar atalho na tela inicial - Criar nova senha ou pasta Falha ao importar a chave PGP Uma chave existente com esse ID foi encontrada, você deseja substituí-lo? Chave PGP importada com sucesso diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 2ef59d70..8873b34f 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -43,7 +43,6 @@ Скопировано в буфер обмена Пожалуйста, укажите имя файла - Пожалуйста, задайте путь к файлу Вы не можете использовать пустой пароль или пустое поле информации Произошла ошибка выполнения операции Git @@ -67,7 +66,6 @@ название Пароль Дополнительно - Зашифровать имя пользователя Выбрать Сохранить Сохранить и скопировать @@ -118,7 +116,6 @@ Сгенерировать пароль Сгенерировать - Включая Длина Цифры Символы @@ -166,7 +163,6 @@ Показать журнал изменений Показывать пароли Видимость расшифрованных паролей, не влияет на копирование в буфер. - Сгенерировать Поделиться паролем в открытом виде с помощью Иконка приложения @@ -200,7 +196,6 @@ Поддержка автозаполнения установленными браузерами: Невозможно открыть приватный ключ ssh, пожалуйста проверьте, что файл существует - Новый пароль Очистить сохраненную кодовую фразу для локального SSH ключа Удалить сохраненный пароль HTTPS Запомнить кодовую фразу @@ -268,7 +263,6 @@ Экспорт паролей… Найден .gpg-id, но он содержит неверный ID ключа, fingerprint или ID пользователя Имя файла не должно содержать \'/\', укажите директорию выше - Директория Неизвестная ошибка Не удалось получить изменения, вы находитесь в состоянии \"оторванной головы\". Используйте \"настройки > утилиты git\", сохраните ваши изменения в новую удаленную ветку и разрешите конфликты на своем компьютере. @@ -311,10 +305,8 @@ Сохраненный ключ хоста был успешно удален! Отсканировать QR-код Выберите изображение - Ввести вручную Учетная запись Разместить ярлык на рабочем столе - Создать новый пароль или папку Не удалось импортировать PGP-ключ PGP ключ успешно импортирован diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 55fbeefa..746b74d2 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -38,7 +38,6 @@ 复制到剪切板 请提供文件名 - 请提供文件路径 您不能使用空密码或空额外内容 Git操作过程中发生错误 @@ -62,7 +61,6 @@ 名称 密码 附加内容 - 加密用户名 选择 保存 保存并复制 @@ -121,7 +119,6 @@ 生成密码 生成 - 包含 长度 数字 标识 @@ -174,7 +171,6 @@ 显示提交日志 显示密码 控制密码解密后的可见性,这不会禁用复制到剪贴板 - 生成 使用明文发送密码… 应用图标 @@ -209,7 +205,6 @@ 已安装支持自动填充的浏览器: 无法打开SSH私钥,请检查文件是否存在 - 新密码 清除本地已保存的SSH密钥密码 清除保存的HTTPS密码 记住密钥密码 @@ -278,7 +273,6 @@ personal.com 找到了 .gpg-id,但它包含无效的密钥ID、指纹或用户ID 找到 .gpg-id 但它包含一个不受支持的短十六进制ID。 文件名不能包含\'/\' - 目录 未知错误 合并失败,你处于冲突状态。 @@ -324,11 +318,9 @@ GPG 已成功清除已保存的主机密钥! 扫描二维码 选择图像 - 手动输入 安全码 (Secret) 账户 在主屏幕上放置快捷方式 - 创建新密码或文件夹 导入 PGP 密钥失败 发现了 ID 相同的现有密钥,您要替换它吗? 成功导入 PGP 密钥 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 34b11662..239cb219 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -47,7 +47,6 @@ Copied to clipboard Please provide a file name - Please provide a file path You cannot use an empty password or empty extra content @@ -79,7 +78,6 @@ Username Password Extra content - Encrypt username Select Save Save and Copy @@ -148,7 +146,6 @@ Generate Password Generate - Include Length Numerals Symbols @@ -201,7 +198,6 @@ Show commit log Show the password Control the visibility of the passwords once decrypted. This does not disable copying to clipboard. - Generate Send password as plaintext using… App icon @@ -239,7 +235,6 @@ Unable to open the ssh private key, please check that the file exists - New password Clear saved passphrase for local SSH key Clear saved HTTPS password Remember key passphrase @@ -311,7 +306,6 @@ Found .gpg-id, but it contains an invalid key ID, fingerprint or user ID Found .gpg-id, but it contains a short hex ID, which is not supported File name must not contain \'/\', set directory above - Directory Unknown error @@ -360,11 +354,9 @@ Successfully cleared saved host key! Scan QR code Choose an image - Enter manually Secret Account Place shortcut on home screen - Create new password or folder Failed to import PGP key An existing key with this ID was found, do you want to replace it? Successfully imported PGP key diff --git a/docs/superpowers/specs/2026-11-02-compose-ui-overhaul-design.md b/docs/superpowers/specs/2026-11-02-compose-ui-overhaul-design.md index 75a599fa..7eab1692 100644 --- a/docs/superpowers/specs/2026-11-02-compose-ui-overhaul-design.md +++ b/docs/superpowers/specs/2026-11-02-compose-ui-overhaul-design.md @@ -46,7 +46,10 @@ the maintainer's phone stays on 0.1.4 until then. - Password with reveal and a generator button; the generator is a bottom sheet with random and diceware tabs. - Username field, free-text extra content (pass format kept as text, nothing hidden). -- "OTP from QR" and "OTP manual" actions. +- "OTP from QR" (camera or image) and "OTP manual" actions. +- "Save and copy" in the overflow. Saving returns to the list; closing returns to where the + editor was opened. The folder picker is a dialog, shared with the list's move action. The + autofill save flow (`AutofillSaveActivity`) hosts the same editor. ## Architecture @@ -94,7 +97,8 @@ needs it. ## Security - `FLAG_SECURE` is set on the window while the entry view or editor destination is shown and - cleared when leaving it, so secrets stay out of screenshots and recents. + cleared when leaving it, so secrets stay out of screenshots and recents. It is counted, so + moving between the two never clears it. - `MainActivity` is not exported. Routes come only from `LaunchActivity` after its checks; an entry route is still validated with `isFileInRepo`. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9d8f4111..13d866e4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,6 @@ agp = "9.5.0" androidxActivity = "1.9.2" bouncycastle = "1.86" coroutines = "1.9.0" -flowbinding = "1.2.0" hilt = "2.60.1" kotlin = "2.4.21" kotlinResult = "2.0.0" @@ -33,7 +32,6 @@ androidx-material = "com.google.android.material:material:1.12.0" androidx-preference = "androidx.preference:preference:1.2.1" androidx-recyclerview = "androidx.recyclerview:recyclerview:1.4.0" androidx-recyclerviewSelection = "androidx.recyclerview:recyclerview-selection:1.2.0" -androidx-swiperefreshlayout = "androidx.swiperefreshlayout:swiperefreshlayout:1.2.0" aps-sublimeFuzzy = "com.github.android-password-store:sublime-fuzzy:2.3.4" aps-zxingAndroidEmbedded = "com.github.android-password-store:zxing-android-embedded:4.2.1" build-agp = { module = "com.android.tools.build:gradle", version.ref = "agp" } @@ -77,7 +75,6 @@ thirdparty-bouncycastle-bcutil = { module = "org.bouncycastle:bcutil-jdk18on", v thirdparty-commons_codec = "commons-codec:commons-codec:1.14" thirdparty-compose-lints = "com.slack.lint.compose:compose-lint-checks:1.4.1" thirdparty-fastscroll = "me.zhanghai.android.fastscroll:library:1.3.0" -thirdparty-flowbinding-android = { module = "io.github.reactivecircus.flowbinding:flowbinding-android", version.ref = "flowbinding" } # JGit upgrades also raise its minimum Java runtime requirements that we cannot satisfy on Android # noinspection GradleDependency thirdparty-jgit = "org.eclipse.jgit:org.eclipse.jgit:5.13.5.202508271544-r" diff --git a/ui/compose/src/main/kotlin/app/passwordstore/ui/compose/PasswordField.kt b/ui/compose/src/main/kotlin/app/passwordstore/ui/compose/PasswordField.kt deleted file mode 100644 index 066a839c..00000000 --- a/ui/compose/src/main/kotlin/app/passwordstore/ui/compose/PasswordField.kt +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.ui.compose - -import androidx.annotation.StringRes -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -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.Modifier -import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.input.VisualTransformation - -@Composable -public fun PasswordField( - value: String, - label: String, - initialVisibility: Boolean, - modifier: Modifier = Modifier, - readOnly: Boolean = false, -) { - var visible by remember { mutableStateOf(initialVisibility) } - TextField( - value = value, - onValueChange = {}, - readOnly = readOnly, - label = { Text(label) }, - visualTransformation = - if (visible) VisualTransformation.None else PasswordVisualTransformation(), - trailingIcon = { - ToggleButton( - visible = visible, - contentDescription = "Toggle password visibility", - onButtonClick = { visible = !visible }, - ) - }, - modifier = modifier, - ) -} - -@Composable -private fun ToggleButton( - visible: Boolean, - contentDescription: String, - onButtonClick: () -> Unit, - modifier: Modifier = Modifier, -) { - IconButton(onClick = onButtonClick, modifier = modifier) { - val icon = - if (visible) painterResource(id = R.drawable.baseline_visibility_off_24) - else painterResource(id = R.drawable.baseline_visibility_24) - Icon(painter = icon, contentDescription = contentDescription) - } -} - -@Composable -public fun CopyButton( - textToCopy: String, - @StringRes buttonLabelRes: Int, - modifier: Modifier = Modifier, -) { - val clipboard = LocalClipboardManager.current - IconButton(onClick = { clipboard.setText(AnnotatedString(textToCopy)) }, modifier = modifier) { - Icon( - painter = painterResource(R.drawable.ic_content_copy), - contentDescription = stringResource(buttonLabelRes), - ) - } -} -- cgit v1.2.3